diff --git a/README.md b/README.md index b965711..94be128 100644 --- a/README.md +++ b/README.md @@ -15,20 +15,36 @@ and uses [wazero](https://wazero.io/) to provide `cgo`-free SQLite bindings. ([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package)). - [`github.com/ncruces/go-sqlite3/embed`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/embed) embeds a build of SQLite into your application. -- [`github.com/ncruces/go-sqlite3/ext/blob`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blob) - simplifies incremental BLOB I/O. -- [`github.com/ncruces/go-sqlite3/ext/stats`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats) - registers [statistics functions](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html). -- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode) - registers Unicode aware functions. - [`github.com/ncruces/go-sqlite3/vfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs) wraps the [C SQLite VFS API](https://sqlite.org/vfs.html) and provides a pure Go implementation. +- [`github.com/ncruces/go-sqlite3/gormlite`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/gormlite) + provides a [GORM](https://gorm.io) driver. + +### Loadable extensions + +- [`github.com/ncruces/go-sqlite3/ext/array`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blob) + provides the [`array`](https://sqlite.org/carray.html) table-valued function. +- [`github.com/ncruces/go-sqlite3/ext/blob`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blob) + simplifies [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html). +- [`github.com/ncruces/go-sqlite3/ext/stats`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats) + provides [statistics functions](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html). +- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode) + provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions. - [`github.com/ncruces/go-sqlite3/vfs/memdb`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb) implements an in-memory VFS. - [`github.com/ncruces/go-sqlite3/vfs/readervfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs) implements a VFS for immutable databases. -- [`github.com/ncruces/go-sqlite3/gormlite`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/gormlite) - provides a [GORM](https://gorm.io) driver. + +### Advanced features + +- [x] [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html) +- [x] [nested transactions](https://sqlite.org/lang_savepoint.html) +- [x] [custom functions](https://sqlite.org/c3ref/create_function.html) +- [x] [virtual tables](https://sqlite.org/vtab.html) +- [x] [custom VFSes](https://sqlite.org/vfs.html) +- [x] [online backup](https://sqlite.org/backup.html) +- [x] [JSON support](https://www.sqlite.org/json1.html) +- [x] [Unicode support](https://sqlite.org/src/dir/ext/icu) ### Caveats @@ -81,20 +97,6 @@ on Linux, macOS, Windows and FreeBSD. Performance is tested by running [speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c). -### Features - -- [x] advanced SQLite features - - [x] incremental BLOB I/O - - [x] nested transactions - - [x] custom functions - - [x] virtual tables - - [x] online backup - - [x] JSON support -- [x] custom VFSes - - [x] custom VFS API - - [x] in-memory VFS - - [x] read-only VFS - ### Alternatives - [`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite) diff --git a/context.go b/context.go index 6266936..2d5bfbd 100644 --- a/context.go +++ b/context.go @@ -204,13 +204,14 @@ func (ctx Context) ResultError(err error) { return } - str := err.Error() - ptr := ctx.c.newString(str) - ctx.c.call(ctx.c.api.resultError, - uint64(ctx.handle), uint64(ptr), uint64(len(str))) - ctx.c.free(ptr) - - if code := errorCode(err, _OK); code != _OK { + msg, code := errorCode(err, _OK) + if msg != "" { + ptr := ctx.c.newString(msg) + ctx.c.call(ctx.c.api.resultError, + uint64(ctx.handle), uint64(ptr), uint64(len(msg))) + ctx.c.free(ptr) + } + if code != _OK { ctx.c.call(ctx.c.api.resultErrorCode, uint64(ctx.handle), uint64(code)) } diff --git a/error.go b/error.go index 899c67c..9048272 100644 --- a/error.go +++ b/error.go @@ -137,17 +137,25 @@ func (e ExtendedErrorCode) Timeout() bool { return e == BUSY_TIMEOUT } -func errorCode(err error, def ErrorCode) (code uint32) { +func errorCode(err error, def ErrorCode) (msg string, code uint32) { + switch code := err.(type) { + case ErrorCode: + return "", uint32(code) + case ExtendedErrorCode: + return "", uint32(code) + case nil: + return "", _OK + } + var ecode ErrorCode var xcode xErrorCode switch { case errors.As(err, &xcode): - return uint32(xcode) + code = uint32(xcode) case errors.As(err, &ecode): - return uint32(ecode) + code = uint32(ecode) + default: + code = uint32(def) } - if err != nil { - return uint32(def) - } - return _OK + return err.Error(), code } diff --git a/ext/array/array.go b/ext/array/array.go new file mode 100644 index 0000000..68625ff --- /dev/null +++ b/ext/array/array.go @@ -0,0 +1,140 @@ +// Package array provides the array table-valued SQL function. +package array + +import ( + "fmt" + "reflect" + + "github.com/ncruces/go-sqlite3" +) + +// Register registers the single-argument array table-valued SQL function. +// The argument must be an [sqlite3.Pointer] to a Go slice or array +// of ints, floats, bools, strings or blobs. +// +// https://sqlite.org/carray.html +func Register(db *sqlite3.Conn) { + sqlite3.CreateModule(db, "array", array{}) +} + +type array struct{} + +func (array) Connect(c *sqlite3.Conn, arg ...string) (_ array, err error) { + err = c.DeclareVtab(`CREATE TABLE x(value, array HIDDEN)`) + return +} + +func (array) Disconnect() error { + return nil +} + +func (array) BestIndex(idx *sqlite3.IndexInfo) error { + for i, cst := range idx.Constraint { + if cst.Column == 1 && cst.Op == sqlite3.INDEX_CONSTRAINT_EQ && cst.Usable { + idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{ + Omit: true, + ArgvIndex: 1, + } + idx.EstimatedCost = 1 + idx.EstimatedRows = 100 + return nil + } + } + return sqlite3.CONSTRAINT +} + +func (array) Open() (sqlite3.VTabCursor, error) { + return &cursor{}, nil +} + +type cursor struct { + array reflect.Value + rowID int +} + +func (c *cursor) EOF() bool { + return c.rowID >= c.array.Len() +} + +func (c *cursor) Next() error { + c.rowID++ + return nil +} + +func (c *cursor) RowID() (int64, error) { + return int64(c.rowID), nil +} + +func (c *cursor) Column(ctx *sqlite3.Context, n int) error { + if n != 0 { + return nil + } + + v := c.array.Index(c.rowID) + k := v.Kind() + + if k == reflect.Interface { + if v.IsNil() { + ctx.ResultNull() + return nil + } + v = v.Elem() + k = v.Kind() + } + + switch { + case v.CanInt(): + ctx.ResultInt64(v.Int()) + + case v.CanUint(): + i64 := int64(v.Uint()) + if i64 < 0 { + return fmt.Errorf("array: integer element overflow:%.0w %d", sqlite3.MISMATCH, v.Uint()) + } + ctx.ResultInt64(i64) + + case v.CanFloat(): + ctx.ResultFloat(v.Float()) + + case k == reflect.Bool: + ctx.ResultBool(v.Bool()) + + case k == reflect.String: + ctx.ResultText(v.String()) + + case (k == reflect.Slice || k == reflect.Array) && + v.Type().Elem().Kind() == reflect.Uint8: + ctx.ResultBlob(v.Bytes()) + + default: + return fmt.Errorf("array: unsupported element:%.0w %v", sqlite3.MISMATCH, v.Type()) + } + return nil +} + +func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error { + array := reflect.ValueOf(arg[0].Pointer()) + array, err := sliceable(array) + if err != nil { + return err + } + + c.array = array + c.rowID = 0 + return nil +} + +func sliceable(v reflect.Value) (_ reflect.Value, err error) { + if v.Kind() == reflect.Slice { + return v, nil + } + if v.Kind() == reflect.Array { + return v, nil + } + if v.Kind() == reflect.Pointer { + if v := v.Elem(); v.Kind() == reflect.Array { + return v, nil + } + } + return v, fmt.Errorf("array: unsupported argument:%.0w %v", sqlite3.MISMATCH, v.Type()) +} diff --git a/ext/array/array_test.go b/ext/array/array_test.go new file mode 100644 index 0000000..acae086 --- /dev/null +++ b/ext/array/array_test.go @@ -0,0 +1,49 @@ +package array_test + +import ( + "fmt" + "log" + + "github.com/ncruces/go-sqlite3" + "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" + "github.com/ncruces/go-sqlite3/ext/array" +) + +func Example() { + db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error { + array.Register(c) + return nil + }) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + rows, err := db.Query(` + SELECT name + FROM pragma_function_list + WHERE name like 'geopoly%' AND narg IN array(?)`, + sqlite3.Pointer([]int{2, 3, 4})) + if err != nil { + log.Fatal(err) + } + defer rows.Close() + + for rows.Next() { + var name string + err := rows.Scan(&name) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", name) + } + if err := rows.Err(); err != nil { + log.Fatal(err) + } + // Unordered output: + // geopoly_regular + // geopoly_overlap + // geopoly_contains_point + // geopoly_within +} diff --git a/ext/blob/blob.go b/ext/blob/blob.go index f12dd13..111166b 100644 --- a/ext/blob/blob.go +++ b/ext/blob/blob.go @@ -5,13 +5,14 @@ import ( "errors" "github.com/ncruces/go-sqlite3" + "github.com/ncruces/go-sqlite3/internal/util" ) // Register registers the blob_open SQL function: // // blob_open(schema, table, column, rowid, flags, callback, args...) // -// The callback must be a [sqlite3.Pointer] to an [OpenCallback]. +// The callback must be an [sqlite3.Pointer] to an [OpenCallback]. // Any optional args will be passed to the callback, // along with the [sqlite3.Blob] handle. // @@ -23,7 +24,7 @@ func Register(db *sqlite3.Conn) { func openBlob(ctx sqlite3.Context, arg ...sqlite3.Value) { if len(arg) < 6 { - ctx.ResultError(errors.New("wrong number of arguments to function blob_open()")) + ctx.ResultError(util.ErrorString("wrong number of arguments to function blob_open()")) return } diff --git a/vfs/readervfs/example_test.go b/vfs/readervfs/example_test.go index 42ab50e..e4bc623 100644 --- a/vfs/readervfs/example_test.go +++ b/vfs/readervfs/example_test.go @@ -34,8 +34,8 @@ func Example_http() { } rows, err := db.Query(` SELECT period, data_value, magntude, units FROM csv - WHERE period > '2010' - LIMIT 10`) + WHERE period > '2010' + LIMIT 10`) if err != nil { log.Fatal(err) } diff --git a/vtab.go b/vtab.go index 1eb1597..bbc11c5 100644 --- a/vtab.go +++ b/vtab.go @@ -372,14 +372,14 @@ func vtabDisconnectCallback(ctx context.Context, mod api.Module, pVTab uint32) u vtab := vtabGetHandle(ctx, mod, pVTab).(VTab) err := vtab.Disconnect() vtabDelHandle(ctx, mod, pVTab) - return errorCode(err, ERROR) + return vtabError(ctx, mod, 0, _PTR_ERROR, err) } func vtabDestroyCallback(ctx context.Context, mod api.Module, pVTab uint32) uint32 { vtab := vtabGetHandle(ctx, mod, pVTab).(VTabDestroyer) err := vtab.Destroy() vtabDelHandle(ctx, mod, pVTab) - return errorCode(err, ERROR) + return vtabError(ctx, mod, 0, _PTR_ERROR, err) } func vtabBestIndexCallback(ctx context.Context, mod api.Module, pVTab, pIdxInfo uint32) uint32 { @@ -435,7 +435,8 @@ func vtabIntegrityCallback(ctx context.Context, mod api.Module, pVTab, zSchema, // xIntegrity should return OK - even if it finds problems in the content of the virtual table. // https://sqlite.org/vtab.html#xintegrity vtabError(ctx, mod, pzErr, _PTR_ERROR, err) - return errorCode(err, _OK) + _, code := errorCode(err, _OK) + return code } func vtabBeginCallback(ctx context.Context, mod api.Module, pVTab uint32) uint32 { @@ -493,7 +494,7 @@ func cursorOpenCallback(ctx context.Context, mod api.Module, pVTab, ppCur uint32 func cursorCloseCallback(ctx context.Context, mod api.Module, pCur uint32) uint32 { err := vtabDelHandle(ctx, mod, pCur) - return errorCode(err, ERROR) + return vtabError(ctx, mod, 0, _VTAB_ERROR, err) } func cursorFilterCallback(ctx context.Context, mod api.Module, pCur, idxNum, idxStr, argc, argv uint32) uint32 { @@ -547,18 +548,18 @@ const ( ) func vtabError(ctx context.Context, mod api.Module, ptr, kind uint32, err error) uint32 { - if err == nil { - return _OK + msg, code := errorCode(err, ERROR) + if msg != "" && ptr != 0 { + switch kind { + case _VTAB_ERROR: + ptr = ptr + 8 + case _CURSOR_ERROR: + ptr = util.ReadUint32(mod, ptr) + 8 + } + db := ctx.Value(connKey{}).(*Conn) + util.WriteUint32(mod, ptr, db.newString(msg)) } - switch kind { - case _VTAB_ERROR: - ptr = ptr + 8 - case _CURSOR_ERROR: - ptr = util.ReadUint32(mod, ptr) + 8 - } - db := ctx.Value(connKey{}).(*Conn) - util.WriteUint32(mod, ptr, db.newString(err.Error())) - return errorCode(err, ERROR) + return code } func vtabGetHandle(ctx context.Context, mod api.Module, ptr uint32) any {