diff --git a/CONTRIBUTORS b/CONTRIBUTORS index cd31644..6e4d262 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -13,6 +13,7 @@ Gleb Sakhnov Jaap Aarts Jan Mercl <0xjnml@gmail.com> Logan Snow +Matthew Gabeler-Lee Ross Light Steffen Butzer Yaacov Akiba Slama diff --git a/all_test.go b/all_test.go index bb78a33..fdadde5 100644 --- a/all_test.go +++ b/all_test.go @@ -29,6 +29,7 @@ import ( "modernc.org/libc" "modernc.org/mathutil" + sqlite3 "modernc.org/sqlite/lib" ) func caller(s string, va ...interface{}) { @@ -2137,3 +2138,69 @@ func TestConstraintUniqueError(t *testing.T) { t.Fatalf("got error string %q, want %q", errs, want) } } + +// https://gitlab.com/cznic/sqlite/-/issues/92 +func TestBeginMode(t *testing.T) { + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + + defer func() { + os.RemoveAll(tempDir) + }() + + tests := []struct { + mode string + want int32 + }{ + {"deferred", sqlite3.SQLITE_TXN_NONE}, + {"immediate", sqlite3.SQLITE_TXN_WRITE}, + // TODO: how to verify "exclusive" is working differently from immediate, + // short of concurrently trying to open the database again? This is only + // different in non-WAL journal modes. + {"exclusive", sqlite3.SQLITE_TXN_WRITE}, + } + + for _, tt := range tests { + tt := tt + for _, jm := range []string{"delete", "wal"} { + jm := jm + t.Run(jm+"/"+tt.mode, func(t *testing.T) { + // t.Parallel() + + qs := fmt.Sprintf("?_txlock=%s&_pragma=journal_mode(%s)", tt.mode, jm) + db, err := sql.Open("sqlite", filepath.Join(tempDir, fmt.Sprintf("testbeginmode-%s.sqlite", tt.mode))+qs) + if err != nil { + t.Fatalf("Failed to open database: %v", err) + } + defer db.Close() + connection, err := db.Conn(context.Background()) + if err != nil { + t.Fatalf("Failed to open connection: %v", err) + } + + tx, err := connection.BeginTx(context.Background(), nil) + if err != nil { + t.Fatalf("Failed to begin transaction: %v", err) + } + defer tx.Rollback() + if err := connection.Raw(func(driverConn interface{}) error { + p, err := libc.CString("main") + if err != nil { + return err + } + c := driverConn.(*conn) + defer c.free(p) + got := sqlite3.Xsqlite3_txn_state(c.tls, c.db, p) + if got != tt.want { + return fmt.Errorf("in mode %s, got txn state %d, want %d", tt.mode, got, tt.want) + } + return nil + }); err != nil { + t.Fatalf("Failed to check txn state: %v", err) + } + }) + } + } +} diff --git a/sqlite.go b/sqlite.go index 5e03f15..e07d859 100644 --- a/sqlite.go +++ b/sqlite.go @@ -726,7 +726,13 @@ type tx struct { func newTx(c *conn) (*tx, error) { r := &tx{c: c} - if err := r.exec(context.Background(), "begin"); err != nil { + var sql string + if c.beginMode != "" { + sql = "begin " + c.beginMode + } else { + sql = "begin" + } + if err := r.exec(context.Background(), sql); err != nil { return nil, err } @@ -784,6 +790,7 @@ type conn struct { sync.Mutex writeTimeFormat string + beginMode string } func newConn(dsn string) (*conn, error) { @@ -866,6 +873,14 @@ func applyQueryParams(c *conn, query string) error { return nil } + if v := q.Get("_txlock"); v != "" { + lower := strings.ToLower(v) + if lower != "deferred" && lower != "immediate" && lower != "exclusive" { + return fmt.Errorf("unknown _txlock %q", v) + } + c.beginMode = v + } + return nil } @@ -1445,6 +1460,27 @@ func newDriver() *Driver { return &Driver{} } // efficient re-use. // // The returned connection is only used by one goroutine at a time. +// +// If name contains a '?', what follows is treated as a query string. This +// driver supports the following query parameters: +// +// _pragma: Each value will be run as a "PRAGMA ..." statement (with the PRAGMA +// keyword added for you). May be specified more than once. Example: +// "_pragma=foreign_keys(1)" will enable foreign key enforcement. More +// information on supported PRAGMAs is available from the SQLite documentation: +// https://www.sqlite.org/pragma.html +// +// _time_format: The name of a format to use when writing time values to the +// database. Currently the only supported value is "sqlite", which corresponds +// to format 7 from https://www.sqlite.org/lang_datefunc.html#time_values, +// including the timezone specifier. If this parameter is not specified, then +// the default String() format will be used. +// +// _txlock: The locking behavior to use when beginning a transaction. May be +// "deferred", "immediate", or "exclusive" (case insensitive). The default is to +// not specify one, which SQLite maps to "deferred". More information is +// available at +// https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions func (d *Driver) Open(name string) (driver.Conn, error) { if LogSqlStatements { log.Println("new connection")