Skip to content

Commit 3cbdae7

Browse files
authored
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.
1 parent 52436d4 commit 3cbdae7

File tree

2 files changed

+48
-0
lines changed

2 files changed

+48
-0
lines changed

sqlite3.go

+7
Original file line numberDiff line numberDiff line change
@@ -2007,6 +2007,13 @@ func (s *SQLiteStmt) execSync(args []namedValue) (driver.Result, error) {
20072007
return &SQLiteResult{id: int64(rowid), changes: int64(changes)}, nil
20082008
}
20092009

2010+
// Readonly reports if this statement is considered readonly by SQLite.
2011+
//
2012+
// See: https://sqlite.org/c3ref/stmt_readonly.html
2013+
func (s *SQLiteStmt) Readonly() bool {
2014+
return C.sqlite3_stmt_readonly(s.s) == 1
2015+
}
2016+
20102017
// Close the rows.
20112018
func (rc *SQLiteRows) Close() error {
20122019
rc.s.mu.Lock()

sqlite3_go113_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"context"
1212
"database/sql"
1313
"database/sql/driver"
14+
"errors"
1415
"os"
1516
"testing"
1617
)
@@ -76,3 +77,43 @@ func TestBeginTxCancel(t *testing.T) {
7677
}()
7778
}
7879
}
80+
81+
func TestStmtReadonly(t *testing.T) {
82+
db, err := sql.Open("sqlite3", ":memory:")
83+
if err != nil {
84+
t.Fatal(err)
85+
}
86+
87+
_, err = db.Exec("CREATE TABLE t (count INT)")
88+
if err != nil {
89+
t.Fatal(err)
90+
}
91+
92+
isRO := func(query string) bool {
93+
c, err := db.Conn(context.Background())
94+
if err != nil {
95+
return false
96+
}
97+
98+
var ro bool
99+
c.Raw(func(dc interface{}) error {
100+
stmt, err := dc.(*SQLiteConn).Prepare(query)
101+
if err != nil {
102+
return err
103+
}
104+
if stmt == nil {
105+
return errors.New("stmt is nil")
106+
}
107+
ro = stmt.(*SQLiteStmt).Readonly()
108+
return nil
109+
})
110+
return ro // On errors ro will remain false.
111+
}
112+
113+
if !isRO(`select * from t`) {
114+
t.Error("select not seen as read-only")
115+
}
116+
if isRO(`insert into t values (1), (2)`) {
117+
t.Error("insert seen as read-only")
118+
}
119+
}

0 commit comments

Comments
 (0)