From 78fd0cbee566e17ea0f83944dae4136460ed39d0 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Tue, 14 Feb 2023 18:21:18 +0000 Subject: [PATCH] Towards database/sql. --- api.go | 16 +++++++++-- conn.go | 58 ++++++++++++++++++++++++++++++++++++++- conn_test.go | 58 ++++++++++++++++++++++++++++++++++++--- driver/driver.go | 44 +++++++++++++++++++++++++++++ embed/build.sh | 8 +++++- embed/sqlite3.wasm | Bin 726214 -> 726666 bytes {cmd => example}/main.go | 0 sqlite3/sqlite_cfg.h | 13 +++++++++ stmt.go | 12 ++++++++ stmt_test.go | 4 +++ 10 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 driver/driver.go rename {cmd => example}/main.go (100%) diff --git a/api.go b/api.go index 5a0b075..294a367 100644 --- a/api.go +++ b/api.go @@ -44,18 +44,24 @@ func newConn(ctx context.Context, module api.Module) (_ *Conn, err error) { step: getFun("sqlite3_step"), exec: getFun("sqlite3_exec"), clearBindings: getFun("sqlite3_clear_bindings"), + bindCount: getFun("sqlite3_bind_parameter_count"), bindInteger: getFun("sqlite3_bind_int64"), bindFloat: getFun("sqlite3_bind_double"), bindText: getFun("sqlite3_bind_text64"), bindBlob: getFun("sqlite3_bind_blob64"), bindZeroBlob: getFun("sqlite3_bind_zeroblob64"), bindNull: getFun("sqlite3_bind_null"), + columnCount: getFun("sqlite3_column_count"), + columnName: getFun("sqlite3_column_name"), + columnType: getFun("sqlite3_column_type"), columnInteger: getFun("sqlite3_column_int64"), columnFloat: getFun("sqlite3_column_double"), columnText: getFun("sqlite3_column_text"), columnBlob: getFun("sqlite3_column_blob"), columnBytes: getFun("sqlite3_column_bytes"), - columnType: getFun("sqlite3_column_type"), + lastRowid: getFun("sqlite3_last_insert_rowid"), + changes: getFun("sqlite3_changes64"), + interrupt: getFun("sqlite3_interrupt"), }, } if err != nil { @@ -80,16 +86,22 @@ type sqliteAPI struct { step api.Function exec api.Function clearBindings api.Function + bindCount api.Function bindInteger api.Function bindFloat api.Function bindText api.Function bindBlob api.Function bindZeroBlob api.Function bindNull api.Function + columnCount api.Function + columnName api.Function + columnType api.Function columnInteger api.Function columnFloat api.Function columnText api.Function columnBlob api.Function columnBytes api.Function - columnType api.Function + lastRowid api.Function + changes api.Function + interrupt api.Function } diff --git a/conn.go b/conn.go index 9e28c3c..1ff2f1c 100644 --- a/conn.go +++ b/conn.go @@ -14,6 +14,9 @@ type Conn struct { mem memory arena arena handle uint32 + + waiter chan struct{} + done <-chan struct{} } // Open calls [OpenFlags] with [OPEN_READWRITE] and [OPEN_CREATE]. @@ -70,6 +73,8 @@ func (c *Conn) Close() error { return nil } + c.SetInterrupt(nil) + r, err := c.api.close.Call(c.ctx, uint64(c.handle)) if err != nil { return err @@ -83,6 +88,57 @@ func (c *Conn) Close() error { return c.mem.mod.Close(c.ctx) } +// SetInterrupt interrupts a long-running query when done is closed. +// +// Subsequent uses of the connection will return [INTERRUPT] +// until done is reset by another call to SetInterrupt. +// +// Typically, done is provided by [context.Context.Done]: +// +// ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond) +// conn.SetInterrupt(ctx.Done()) +// defer cancel() +// +// https://www.sqlite.org/c3ref/interrupt.html +func (c *Conn) SetInterrupt(done <-chan struct{}) (old <-chan struct{}) { + // Is a waiter running? + if c.waiter != nil { + c.waiter <- struct{}{} // Cancel the waiter. + <-c.waiter // Wait for it to finish. + c.waiter = nil + } + + old = c.done + c.done = done + if done == nil { + return old + } + + waiter := make(chan struct{}) + c.waiter = waiter + go func() { + select { + case <-waiter: + // Waiter was cancelled. + case <-done: + // Done was closed. + + // Because it doesn't touch the C stack, + // sqlite3_interrupt is safe to call from a goroutine. + _, err := c.api.interrupt.Call(c.ctx, uint64(c.handle)) + if err != nil { + panic(err) + } + + // Wait for the next call to SetInterrupt. + <-waiter // Waiter was cancelled. + } + // Signal that the waiter is finished. + waiter <- struct{}{} + }() + return old +} + // Exec is a convenience function that allows an application to run // multiple statements of SQL without having to use a lot of code. // @@ -111,9 +167,9 @@ func (c *Conn) Prepare(sql string) (stmt *Stmt, tail string, err error) { // https://www.sqlite.org/c3ref/prepare.html func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail string, err error) { defer c.arena.reset() - sqlPtr := c.arena.string(sql) stmtPtr := c.arena.new(ptrlen) tailPtr := c.arena.new(ptrlen) + sqlPtr := c.arena.string(sql) r, err := c.api.prepare.Call(c.ctx, uint64(c.handle), uint64(sqlPtr), uint64(len(sql)+1), uint64(flags), diff --git a/conn_test.go b/conn_test.go index 26fd106..4029529 100644 --- a/conn_test.go +++ b/conn_test.go @@ -2,9 +2,11 @@ package sqlite3 import ( "bytes" + "context" "errors" "math" "testing" + "time" ) func TestConn_Close(t *testing.T) { @@ -19,7 +21,7 @@ func TestConn_Close_BUSY(t *testing.T) { } defer db.Close() - stmt, _, err := db.Prepare("BEGIN") + stmt, _, err := db.Prepare(`BEGIN`) if err != nil { t.Fatal(err) } @@ -41,6 +43,54 @@ func TestConn_Close_BUSY(t *testing.T) { } } +func TestConn_Interrupt(t *testing.T) { + db, err := Open(":memory:") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + stmt, _, err := db.Prepare(` + WITH RECURSIVE + fibonacci (curr, next) + AS ( + SELECT 0, 1 + UNION ALL + SELECT next, curr + next FROM fibonacci + LIMIT 10e6 + ) + SELECT min(curr) FROM fibonacci + `) + if err != nil { + t.Fatal(err) + } + defer stmt.Close() + + ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond) + db.SetInterrupt(ctx.Done()) + defer cancel() + + for stmt.Step() { + } + + err = stmt.Err() + if err == nil { + t.Fatal("want error") + } + var serr *Error + if !errors.As(err, &serr) { + t.Fatalf("got %T, want sqlite3.Error", err) + } + if rc := serr.Code(); rc != INTERRUPT { + t.Errorf("got %d, want sqlite3.INTERRUPT", rc) + } + if got := err.Error(); got != `sqlite3: interrupted` { + t.Error("got message: ", got) + } + + db.SetInterrupt(nil) +} + func TestConn_Prepare_Empty(t *testing.T) { db, err := Open(":memory:") if err != nil { @@ -48,7 +98,7 @@ func TestConn_Prepare_Empty(t *testing.T) { } defer db.Close() - stmt, _, err := db.Prepare("") + stmt, _, err := db.Prepare(``) if err != nil { t.Fatal(err) } @@ -68,7 +118,7 @@ func TestConn_Prepare_Invalid(t *testing.T) { var serr *Error - _, _, err = db.Prepare("SELECT") + _, _, err = db.Prepare(`SELECT`) if err == nil { t.Fatal("want error") } @@ -82,7 +132,7 @@ func TestConn_Prepare_Invalid(t *testing.T) { t.Error("got message: ", got) } - _, _, err = db.Prepare("SELECT * FRM sqlite_schema") + _, _, err = db.Prepare(`SELECT * FRM sqlite_schema`) if err == nil { t.Fatal("want error") } diff --git a/driver/driver.go b/driver/driver.go new file mode 100644 index 0000000..1578975 --- /dev/null +++ b/driver/driver.go @@ -0,0 +1,44 @@ +//go:build todo + +// Package driver provides a database/sql driver for SQLite. +package driver + +import ( + "database/sql" + "database/sql/driver" + + "github.com/ncruces/go-sqlite3" +) + +func init() { + sql.Register("sqlite3", sqlite{}) +} + +type sqlite struct{} + +func (sqlite) Open(name string) (driver.Conn, error) { + c, err := sqlite3.OpenFlags(name, sqlite3.OPEN_READWRITE|sqlite3.OPEN_CREATE|sqlite3.OPEN_URI) + if err != nil { + return nil, err + } + return conn{c}, nil +} + +type conn struct{ *sqlite3.Conn } +type stmt struct{ *sqlite3.Stmt } + +func (c conn) Begin() (driver.Tx, error) + +func (c conn) Prepare(query string) (driver.Stmt, error) { + s, _, err := c.Conn.Prepare(query) + if err != nil { + return nil, err + } + return stmt{s}, nil +} + +func (s stmt) NumInput() int + +func (s stmt) Exec(args []driver.Value) (driver.Result, error) + +func (s stmt) Query(args []driver.Value) (driver.Rows, error) diff --git a/embed/build.sh b/embed/build.sh index 3109b5f..ee8b186 100755 --- a/embed/build.sh +++ b/embed/build.sh @@ -28,15 +28,21 @@ zig cc --target=wasm32-wasi -flto -g0 -Os \ -Wl,--export=sqlite3_step \ -Wl,--export=sqlite3_exec \ -Wl,--export=sqlite3_clear_bindings \ + -Wl,--export=sqlite3_bind_parameter_count \ -Wl,--export=sqlite3_bind_int64 \ -Wl,--export=sqlite3_bind_double \ -Wl,--export=sqlite3_bind_text64 \ -Wl,--export=sqlite3_bind_blob64 \ -Wl,--export=sqlite3_bind_zeroblob64 \ -Wl,--export=sqlite3_bind_null \ + -Wl,--export=sqlite3_column_count \ + -Wl,--export=sqlite3_column_name \ + -Wl,--export=sqlite3_column_type \ -Wl,--export=sqlite3_column_int64 \ -Wl,--export=sqlite3_column_double \ -Wl,--export=sqlite3_column_text \ -Wl,--export=sqlite3_column_blob \ -Wl,--export=sqlite3_column_bytes \ - -Wl,--export=sqlite3_column_type \ + -Wl,--export=sqlite3_last_insert_rowid \ + -Wl,--export=sqlite3_changes64 \ + -Wl,--export=sqlite3_interrupt \ diff --git a/embed/sqlite3.wasm b/embed/sqlite3.wasm index dbde2a2332cb3a76dd5463af7ca8a3507bac6069..59fae1433f53e87c3c0f78a04be136d163cb0ad9 100755 GIT binary patch delta 1119 zcmZXS%TE(g6vprEw7|?6q@7YI$}2z#iNqK&CL|_;absel?lHBG3pSa~)R`$F3whH> zTxgo~YU0A^$`}pNXbo}U!Vnf11 z*ek9W=uh&dP*Jr6*3;lagXm~E`AG2ER)9X863T7pZ}NjsF|Z15iWRpPRYHcl$gI~k$47HvZ13ACSR>d}>3 zkje_)fx`v2K{PqiM?z=^PUXKfEvomR-NGm_jl=>P@%Of&J*YA{IiMTLM0K)<`-G|jQ>%#qywbr!XC zy0EX7NlEOPr9KH7@dmvvK?Ao54fJ+Ka62t6TlAR(E!;Mor8p~8dJb%4VRq=01T}b< z{*s^$@6ipWdn*Sm=6?S<#?n71)|+VFKNe(2q&04+@eLQyyoW64h)6HHq3P$YH#UdJz~aL1x^2mxd? zV?Qj@O&tfIN@F@EVTI;&OwcPG4O-GshuXX$=N~#Eoii{Fb-H87t5tFgJOgVqCpD~7 z(ZI8?K~WQXp+SQt_Cb^Gn%J*wB7KpmvW4>&CX{WYq=kLT4$^H4Pr*!SRL1ZRayrdK diff --git a/cmd/main.go b/example/main.go similarity index 100% rename from cmd/main.go rename to example/main.go diff --git a/sqlite3/sqlite_cfg.h b/sqlite3/sqlite_cfg.h index 664377d..2b00285 100644 --- a/sqlite3/sqlite_cfg.h +++ b/sqlite3/sqlite_cfg.h @@ -5,6 +5,9 @@ #define SQLITE_OS_OTHER 1 #define SQLITE_BYTEORDER 1234 +#define HAVE_STDINT_H 1 +#define HAVE_INTTYPES_H 1 + #define HAVE_ISNAN 1 #define HAVE_USLEEP 1 #define HAVE_LOCALTIME_S 1 @@ -25,6 +28,16 @@ #define SQLITE_OMIT_AUTOINIT #define SQLITE_USE_ALLOCA +// Recommended Extensions + +// #define SQLITE_ENABLE_MATH_FUNCTIONS 1 +// #define SQLITE_ENABLE_FTS3 1 +// #define SQLITE_ENABLE_FTS3_PARENTHESIS 1 +// #define SQLITE_ENABLE_FTS4 1 +// #define SQLITE_ENABLE_FTS5 1 +// #define SQLITE_ENABLE_RTREE 1 +// #define SQLITE_ENABLE_GEOPOLY 1 + // Need this to access WAL databases without the use of shared memory. #define SQLITE_DEFAULT_LOCKING_MODE 1 diff --git a/stmt.go b/stmt.go index 2d75ff3..3b18e75 100644 --- a/stmt.go +++ b/stmt.go @@ -95,6 +95,18 @@ func (s *Stmt) Exec() error { return s.Reset() } +// BindCount gets the number of SQL parameters in a prepared statement. +// +// https://www.sqlite.org/c3ref/bind_parameter_count.html +func (s *Stmt) BindCount() int { + r, err := s.c.api.bindCount.Call(s.c.ctx, + uint64(s.handle)) + if err != nil { + panic(err) + } + return int(r[0]) +} + // BindBool binds a bool to the prepared statement. // The leftmost SQL parameter has an index of 1. // SQLite does not have a separate boolean storage class. diff --git a/stmt_test.go b/stmt_test.go index 54d7fa0..0afc4ee 100644 --- a/stmt_test.go +++ b/stmt_test.go @@ -23,6 +23,10 @@ func TestStmt(t *testing.T) { } defer stmt.Close() + if got := stmt.BindCount(); got != 1 { + t.Errorf("got %d, want 1", got) + } + err = stmt.BindBool(1, false) if err != nil { t.Fatal(err)