Export sqlite3_stmt_readonly() via SQLiteStmt.Readonly() (#895)

This can be used like in the test; I wrote a little wrapper around
sql.DB which uses this, and allows concurrent reads but just one single
write. This is perhaps a better generic "table locked"-solution than
setting the connections to 1 and/or cache=shared (although even better
would be to design your app in such a way that this doesn't happpen in
the first place, but even then a little seat belt isn't a bad thing).

The parsing adds about 0.1ms to 0.2ms of overhead in the wrapper, which
isn't too bad (and it caches the results, so only needs to do this
once).

At any rate, I can't really access functions from sqlite3-binding.c from
my application, so expose it via SQLiteStmt.
This commit is contained in:
Martin Tournoij 2020-12-28 07:52:08 +08:00 committed by GitHub
parent 52436d4074
commit 3cbdae750e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 48 additions and 0 deletions

View File

@ -2007,6 +2007,13 @@ func (s *SQLiteStmt) execSync(args []namedValue) (driver.Result, error) {
return &SQLiteResult{id: int64(rowid), changes: int64(changes)}, nil return &SQLiteResult{id: int64(rowid), changes: int64(changes)}, nil
} }
// Readonly reports if this statement is considered readonly by SQLite.
//
// See: https://sqlite.org/c3ref/stmt_readonly.html
func (s *SQLiteStmt) Readonly() bool {
return C.sqlite3_stmt_readonly(s.s) == 1
}
// Close the rows. // Close the rows.
func (rc *SQLiteRows) Close() error { func (rc *SQLiteRows) Close() error {
rc.s.mu.Lock() rc.s.mu.Lock()

View File

@ -11,6 +11,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"database/sql/driver" "database/sql/driver"
"errors"
"os" "os"
"testing" "testing"
) )
@ -76,3 +77,43 @@ func TestBeginTxCancel(t *testing.T) {
}() }()
} }
} }
func TestStmtReadonly(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
_, err = db.Exec("CREATE TABLE t (count INT)")
if err != nil {
t.Fatal(err)
}
isRO := func(query string) bool {
c, err := db.Conn(context.Background())
if err != nil {
return false
}
var ro bool
c.Raw(func(dc interface{}) error {
stmt, err := dc.(*SQLiteConn).Prepare(query)
if err != nil {
return err
}
if stmt == nil {
return errors.New("stmt is nil")
}
ro = stmt.(*SQLiteStmt).Readonly()
return nil
})
return ro // On errors ro will remain false.
}
if !isRO(`select * from t`) {
t.Error("select not seen as read-only")
}
if isRO(`insert into t values (1), (2)`) {
t.Error("insert seen as read-only")
}
}