diff --git a/compile.go b/compile.go index d5c1ce2..9f2d10b 100644 --- a/compile.go +++ b/compile.go @@ -49,6 +49,10 @@ func (s *sqlite3Runtime) compileModule(ctx context.Context) { return } } + if bin == nil { + s.err = binaryErr + return + } s.compiled, s.err = s.runtime.CompileModule(ctx, bin) } diff --git a/conn_test.go b/conn_test.go index 41b27ca..f8832d4 100644 --- a/conn_test.go +++ b/conn_test.go @@ -40,6 +40,10 @@ func TestConn_Close_BUSY(t *testing.T) { if rc := serr.Code(); rc != BUSY { t.Errorf("got %d, want sqlite3.BUSY", rc) } + var terr interface{ Temporary() bool } + if !errors.As(err, &terr) || !terr.Temporary() { + t.Error("not temporary", err) + } if got := err.Error(); got != `sqlite3: database is locked: unable to close due to unfinalized statements or unfinished backups` { t.Error("got message: ", got) } diff --git a/driver/driver.go b/driver/driver.go index e20e790..85c2bfe 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -26,14 +26,18 @@ func (sqlite) Open(name string) (driver.Conn, error) { return nil, err } - var txBegin = "BEGIN " + var txBegin string var pragmas strings.Builder if _, after, ok := strings.Cut(name, "?"); ok { query, _ := url.ParseQuery(after) - switch v := query.Get("_txlock"); v { + switch s := query.Get("_txlock"); s { + case "": + txBegin = "BEGIN" case "deferred", "immediate", "exclusive": - txBegin += v + txBegin = "BEGIN " + s + default: + return nil, fmt.Errorf("sqlite3: invalid _txlock: %s", s) } for _, p := range query["_pragma"] { @@ -219,6 +223,8 @@ func (s stmt) Query(args []driver.Value) (driver.Rows, error) { } func (s stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { + // Use QueryContext to setup bindings. + // No need to close rows: that simply resets the statement, exec does the same. _, err := s.QueryContext(ctx, args) if err != nil { return nil, err diff --git a/driver/driver_test.go b/driver/driver_test.go index 3d37f1f..aa1e285 100644 --- a/driver/driver_test.go +++ b/driver/driver_test.go @@ -78,7 +78,8 @@ func Test_Open_pragma_invalid(t *testing.T) { } func Test_Open_txLock(t *testing.T) { - db, err := sql.Open("sqlite3", filepath.Join(t.TempDir(), "test.db")+ + db, err := sql.Open("sqlite3", "file:"+ + filepath.Join(t.TempDir(), "test.db")+ "?_txlock=exclusive&_pragma=busy_timeout(0)") if err != nil { t.Fatal(err) @@ -101,6 +102,10 @@ func Test_Open_txLock(t *testing.T) { if rc := serr.Code(); rc != sqlite3.BUSY { t.Errorf("got %d, want sqlite3.BUSY", rc) } + var terr interface{ Temporary() bool } + if !errors.As(err, &terr) || !terr.Temporary() { + t.Error("not temporary", err) + } if got := err.Error(); got != `sqlite3: database is locked` { t.Error("got message: ", got) } @@ -111,6 +116,22 @@ func Test_Open_txLock(t *testing.T) { } } +func Test_Open_txLock_invalid(t *testing.T) { + db, err := sql.Open("sqlite3", "file::memory:?_txlock=xclusive") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + _, err = db.Conn(context.TODO()) + if err == nil { + t.Fatal("want error") + } + if got := err.Error(); got != `sqlite3: invalid _txlock: xclusive` { + t.Error("got message: ", got) + } +} + func Test_BeginTx(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -122,7 +143,7 @@ func Test_BeginTx(t *testing.T) { defer db.Close() _, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) - if err != isolationErr { + if err.Error() != string(isolationErr) { t.Error("want isolationErr") } @@ -205,7 +226,7 @@ func Test_Prepare(t *testing.T) { } _, err = db.Prepare(`SELECT 1; SELECT 2`) - if err != tailErr { + if err.Error() != string(tailErr) { t.Error("want tailErr") } } diff --git a/error.go b/error.go index 686c71e..fb4eb1d 100644 --- a/error.go +++ b/error.go @@ -50,6 +50,11 @@ func (e *Error) Error() string { return b.String() } +// Temporary returns true for [BUSY] errors. +func (e *Error) Temporary() bool { + return e.Code() == BUSY +} + // SQL returns the SQL starting at the token that triggered a syntax error. func (e *Error) SQL() string { return e.sql @@ -60,6 +65,7 @@ type errorString string func (e errorString) Error() string { return string(e) } const ( + binaryErr = errorString("sqlite3: no SQLite binary embed/set/loaded") nilErr = errorString("sqlite3: invalid memory address or null pointer dereference") oomErr = errorString("sqlite3: out of memory") rangeErr = errorString("sqlite3: index out of range") diff --git a/tests/compile/nil/compile_test.go b/tests/compile/nil/compile_test.go new file mode 100644 index 0000000..5e3a8cc --- /dev/null +++ b/tests/compile/nil/compile_test.go @@ -0,0 +1,14 @@ +package compile_empty + +import ( + "testing" + + "github.com/ncruces/go-sqlite3" +) + +func TestCompile_empty(t *testing.T) { + _, err := sqlite3.Open(":memory:") + if err == nil { + t.Error("want error") + } +}