From 3cbdae750e52afa881060732446298f98131e834 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Mon, 28 Dec 2020 07:52:08 +0800 Subject: [PATCH] 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. --- sqlite3.go | 7 +++++++ sqlite3_go113_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/sqlite3.go b/sqlite3.go index d1ff406..552a2ab 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -2007,6 +2007,13 @@ func (s *SQLiteStmt) execSync(args []namedValue) (driver.Result, error) { 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. func (rc *SQLiteRows) Close() error { rc.s.mu.Lock() diff --git a/sqlite3_go113_test.go b/sqlite3_go113_test.go index 6f74e6b..a010cb7 100644 --- a/sqlite3_go113_test.go +++ b/sqlite3_go113_test.go @@ -11,6 +11,7 @@ import ( "context" "database/sql" "database/sql/driver" + "errors" "os" "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") + } +}