mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-11 21:49:13 +00:00
Array extension.
This commit is contained in:
46
README.md
46
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)
|
||||
|
||||
15
context.go
15
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))
|
||||
}
|
||||
|
||||
22
error.go
22
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
|
||||
}
|
||||
|
||||
140
ext/array/array.go
Normal file
140
ext/array/array.go
Normal file
@@ -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())
|
||||
}
|
||||
49
ext/array/array_test.go
Normal file
49
ext/array/array_test.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
31
vtab.go
31
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 {
|
||||
|
||||
Reference in New Issue
Block a user