mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-19 09:04:16 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d78239bfbf | ||
|
|
49852732b2 | ||
|
|
9b90d076cb | ||
|
|
15b94577b1 | ||
|
|
25557244cc | ||
|
|
c2d3bf0cfc | ||
|
|
58a5682084 | ||
|
|
1ed954e96f | ||
|
|
9e7a0a875d | ||
|
|
26adda4529 | ||
|
|
2f6cd8de1d | ||
|
|
e027e055ff | ||
|
|
63fdc141e5 | ||
|
|
0bbd145a49 | ||
|
|
c755ef96e6 | ||
|
|
9a69e407cc | ||
|
|
e9db0d8e84 | ||
|
|
dadf53e175 | ||
|
|
f536765206 | ||
|
|
12034c4f0b | ||
|
|
b4e5d1a213 | ||
|
|
b06c7dda6c | ||
|
|
5e1909a20e | ||
|
|
77d74baca5 | ||
|
|
4142680d5a |
26
README.md
26
README.md
@@ -65,17 +65,20 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
|
||||
This module replaces the SQLite [OS Interface](https://sqlite.org/vfs.html)
|
||||
(aka VFS) with a [pure Go](vfs/) implementation,
|
||||
which has advantages and disadvantages.
|
||||
|
||||
Read more about the Go VFS design [here](vfs/README.md).
|
||||
|
||||
Because each database connection executes within a Wasm sandboxed environment,
|
||||
memory usage will be higher than alternatives.
|
||||
|
||||
### Testing
|
||||
|
||||
This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3/wiki/Test-coverage-report).
|
||||
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
|
||||
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
|
||||
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach)
|
||||
thorough testing.
|
||||
|
||||
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) on
|
||||
Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64),
|
||||
Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (arm64/amd64),
|
||||
Windows (amd64), FreeBSD (amd64/arm64), OpenBSD (amd64), NetBSD (amd64/arm64),
|
||||
DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64).
|
||||
|
||||
@@ -84,12 +87,21 @@ The Go VFS is tested by running SQLite's
|
||||
|
||||
### Performance
|
||||
|
||||
Perfomance of the [`database/sql`](https://pkg.go.dev/database/sql) driver is
|
||||
Performance of the [`database/sql`](https://pkg.go.dev/database/sql) driver is
|
||||
[competitive](https://github.com/cvilsmeier/go-sqlite-bench) with alternatives.
|
||||
|
||||
The Wasm and VFS layers are also tested by running SQLite's
|
||||
The Wasm and VFS layers are also benchmarked by running SQLite's
|
||||
[speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c).
|
||||
|
||||
### Concurrency
|
||||
|
||||
This module behaves similarly to SQLite in [multi-thread](https://sqlite.org/threadsafe.html) mode:
|
||||
it is goroutine-safe, provided that no single database connection, or object derived from it,
|
||||
is used concurrently by multiple goroutines.
|
||||
|
||||
The [`database/sql`](https://pkg.go.dev/database/sql) API is safe to use concurrently,
|
||||
according to its documentation.
|
||||
|
||||
### FAQ, issues, new features
|
||||
|
||||
For questions, please see [Discussions](https://github.com/ncruces/go-sqlite3/discussions/categories/q-a).
|
||||
@@ -98,7 +110,7 @@ Also, post there if you used this driver for something interesting
|
||||
([_"Show and tell"_](https://github.com/ncruces/go-sqlite3/discussions/categories/show-and-tell)),
|
||||
have an [idea](https://github.com/ncruces/go-sqlite3/discussions/categories/ideas)…
|
||||
|
||||
The [Issue](https://github.com/ncruces/go-sqlite3/issues) tracker is for bugs we want fixed,
|
||||
The [Issue](https://github.com/ncruces/go-sqlite3/issues) tracker is for bugs,
|
||||
and features we're working on, planning to work on, or asking for help with.
|
||||
|
||||
### Alternatives
|
||||
@@ -106,4 +118,4 @@ and features we're working on, planning to work on, or asking for help with.
|
||||
- [`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite)
|
||||
- [`crawshaw.io/sqlite`](https://pkg.go.dev/crawshaw.io/sqlite)
|
||||
- [`github.com/mattn/go-sqlite3`](https://pkg.go.dev/github.com/mattn/go-sqlite3)
|
||||
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)
|
||||
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)
|
||||
10
config.go
10
config.go
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
|
||||
@@ -48,6 +49,15 @@ func (c *Conn) Config(op DBConfig, arg ...bool) (bool, error) {
|
||||
return util.ReadBool(c.mod, argsPtr), c.error(rc)
|
||||
}
|
||||
|
||||
var defaultLogger atomic.Pointer[func(code ExtendedErrorCode, msg string)]
|
||||
|
||||
// ConfigLog sets up the default error logging callback for new connections.
|
||||
//
|
||||
// https://sqlite.org/errlog.html
|
||||
func ConfigLog(cb func(code ExtendedErrorCode, msg string)) {
|
||||
defaultLogger.Store(&cb)
|
||||
}
|
||||
|
||||
// ConfigLog sets up the error logging callback for the connection.
|
||||
//
|
||||
// https://sqlite.org/errlog.html
|
||||
|
||||
20
conn.go
20
conn.go
@@ -3,6 +3,7 @@ package sqlite3
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
@@ -48,7 +49,7 @@ func Open(filename string) (*Conn, error) {
|
||||
}
|
||||
|
||||
// OpenContext is like [Open] but includes a context,
|
||||
// which is used to interrupt the process of opening the connectiton.
|
||||
// which is used to interrupt the process of opening the connection.
|
||||
func OpenContext(ctx context.Context, filename string) (*Conn, error) {
|
||||
return newConn(ctx, filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
|
||||
}
|
||||
@@ -91,6 +92,9 @@ func newConn(ctx context.Context, filename string, flags OpenFlag) (ret *Conn, _
|
||||
}()
|
||||
|
||||
c.ctx = context.WithValue(c.ctx, connKey{}, c)
|
||||
if logger := defaultLogger.Load(); logger != nil {
|
||||
c.ConfigLog(*logger)
|
||||
}
|
||||
c.arena = c.newArena()
|
||||
c.handle, err = c.openDB(filename, flags)
|
||||
if err == nil {
|
||||
@@ -503,10 +507,16 @@ func (c *Conn) error(rc res_t, sql ...string) error {
|
||||
return c.sqlite.error(rc, c.handle, sql...)
|
||||
}
|
||||
|
||||
func (c *Conn) stmtsIter(yield func(*Stmt) bool) {
|
||||
for _, s := range c.stmts {
|
||||
if !yield(s) {
|
||||
break
|
||||
// Stmts returns an iterator for the prepared statements
|
||||
// associated with the database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/next_stmt.html
|
||||
func (c *Conn) Stmts() iter.Seq[*Stmt] {
|
||||
return func(yield func(*Stmt) bool) {
|
||||
for _, s := range c.stmts {
|
||||
if !yield(s) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
conn_iter.go
11
conn_iter.go
@@ -1,11 +0,0 @@
|
||||
//go:build go1.23
|
||||
|
||||
package sqlite3
|
||||
|
||||
import "iter"
|
||||
|
||||
// Stmts returns an iterator for the prepared statements
|
||||
// associated with the database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/next_stmt.html
|
||||
func (c *Conn) Stmts() iter.Seq[*Stmt] { return c.stmtsIter }
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build !go1.23
|
||||
|
||||
package sqlite3
|
||||
|
||||
// Stmts returns an iterator for the prepared statements
|
||||
// associated with the database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/next_stmt.html
|
||||
func (c *Conn) Stmts() func(func(*Stmt) bool) { return c.stmtsIter }
|
||||
7
const.go
7
const.go
@@ -11,10 +11,9 @@ const (
|
||||
_ROW = 100 /* sqlite3_step() has another row ready */
|
||||
_DONE = 101 /* sqlite3_step() has finished executing */
|
||||
|
||||
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
|
||||
_MAX_LENGTH = 1e9
|
||||
_MAX_SQL_LENGTH = 1e9
|
||||
_MAX_FUNCTION_ARG = 100
|
||||
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
|
||||
_MAX_LENGTH = 1e9
|
||||
_MAX_SQL_LENGTH = 1e9
|
||||
|
||||
ptrlen = util.PtrLen
|
||||
intlen = util.IntLen
|
||||
|
||||
Binary file not shown.
@@ -17,6 +17,7 @@ cp "$ROOT"/sqlite3/*.patch build/
|
||||
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=c09656c6 | tar xz
|
||||
|
||||
cd sqlite
|
||||
cat ../repro.patch | patch -p0 --no-backup-if-mismatch
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c "OPTS=-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT -DSQLITE_ENABLE_ORDERED_SET_AGGREGATES"
|
||||
else
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
module github.com/ncruces/go-sqlite3/embed/bcw2
|
||||
|
||||
go 1.22.0
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
|
||||
require github.com/ncruces/go-sqlite3 v0.23.0
|
||||
require github.com/ncruces/go-sqlite3 v0.24.0
|
||||
|
||||
require (
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/ncruces/sort v0.1.5 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.2 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
github.com/ncruces/go-sqlite3 v0.23.0 h1:90j/ar8Ywu2AtsfDl5WhO9sgP/rNk76BcKGIzAHO8AQ=
|
||||
github.com/ncruces/go-sqlite3 v0.23.0/go.mod h1:gq2nriHSczOs11SqGW5+0X+SgLdkdj4K+j4F/AhQ+8g=
|
||||
github.com/ncruces/go-sqlite3 v0.24.0 h1:Z4jfmzu2NCd4SmyFwLT2OmF3EnTZbqwATvdiuNHNhLA=
|
||||
github.com/ncruces/go-sqlite3 v0.24.0/go.mod h1:/Vs8ACZHjJ1SA6E9RZUn3EyB1OP3nDQ4z/ar+0fplTQ=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/ncruces/sort v0.1.5 h1:fiFWXXAqKI8QckPf/6hu/bGFwcEPrirIOFaJqWujs4k=
|
||||
github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
|
||||
23
embed/bcw2/repro.patch
Normal file
23
embed/bcw2/repro.patch
Normal file
@@ -0,0 +1,23 @@
|
||||
# https://sqlite.org/src/vpatch?from=67809715977a5bad&to=3f57584710d61174
|
||||
--- tool/mkpragmatab.tcl
|
||||
+++ tool/mkpragmatab.tcl
|
||||
@@ -526,14 +526,17 @@
|
||||
puts $fd [format {#define PragFlg_%-10s 0x%02x /* %s */} \
|
||||
$f $fv $flagMeaning($f)]
|
||||
set fv [expr {$fv*2}]
|
||||
}
|
||||
|
||||
-# Sort the column lists so that longer column lists occur first
|
||||
+# Sort the column lists so that longer column lists occur first.
|
||||
+# In the event of a tie, sort column lists lexicographically.
|
||||
#
|
||||
proc colscmp {a b} {
|
||||
- return [expr {[llength $b] - [llength $a]}]
|
||||
+ set rc [expr {[llength $b] - [llength $a]}]
|
||||
+ if {$rc} {return $rc}
|
||||
+ return [string compare $a $b]
|
||||
}
|
||||
set cols_list [lsort -command colscmp $cols_list]
|
||||
|
||||
# Generate the array of column names used by pragmas that act like
|
||||
# queries.
|
||||
@@ -80,6 +80,7 @@ sqlite3_interrupt
|
||||
sqlite3_invoke_busy_handler_go
|
||||
sqlite3_last_insert_rowid
|
||||
sqlite3_limit
|
||||
sqlite3_log_go
|
||||
sqlite3_malloc64
|
||||
sqlite3_open_v2
|
||||
sqlite3_overload_function
|
||||
|
||||
Binary file not shown.
@@ -268,13 +268,13 @@ func (b *bloom) Open() (sqlite3.VTabCursor, error) {
|
||||
|
||||
type cursor struct {
|
||||
*bloom
|
||||
arg *sqlite3.Value
|
||||
arg sqlite3.Value
|
||||
eof bool
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
c.eof = false
|
||||
c.arg = &arg[0]
|
||||
c.arg = arg[0]
|
||||
blob := arg[0].RawBlob()
|
||||
|
||||
f, err := c.db.OpenBlob(c.schema, c.storage, "data", 1, false)
|
||||
@@ -312,7 +312,7 @@ func (c *cursor) Column(ctx sqlite3.Context, n int) error {
|
||||
case 0:
|
||||
ctx.ResultBool(true)
|
||||
case 1:
|
||||
ctx.ResultValue(*c.arg)
|
||||
ctx.ResultValue(c.arg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
//go:build !go1.23
|
||||
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Adapted from: https://research.swtch.com/coro
|
||||
|
||||
const errCoroCanceled = util.ErrorString("coroutine canceled")
|
||||
|
||||
func coroNew[In, Out any](f func(In, func(Out) In) Out) (resume func(In) (Out, bool), cancel func()) {
|
||||
type msg[T any] struct {
|
||||
panic any
|
||||
val T
|
||||
}
|
||||
|
||||
cin := make(chan msg[In])
|
||||
cout := make(chan msg[Out])
|
||||
running := true
|
||||
resume = func(in In) (out Out, ok bool) {
|
||||
if !running {
|
||||
return
|
||||
}
|
||||
cin <- msg[In]{val: in}
|
||||
m := <-cout
|
||||
if m.panic != nil {
|
||||
panic(m.panic)
|
||||
}
|
||||
return m.val, running
|
||||
}
|
||||
cancel = func() {
|
||||
if !running {
|
||||
return
|
||||
}
|
||||
e := fmt.Errorf("%w", errCoroCanceled)
|
||||
cin <- msg[In]{panic: e}
|
||||
m := <-cout
|
||||
if m.panic != nil && m.panic != e {
|
||||
panic(m.panic)
|
||||
}
|
||||
}
|
||||
yield := func(out Out) In {
|
||||
cout <- msg[Out]{val: out}
|
||||
m := <-cin
|
||||
if m.panic != nil {
|
||||
panic(m.panic)
|
||||
}
|
||||
return m.val
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if running {
|
||||
running = false
|
||||
cout <- msg[Out]{panic: recover()}
|
||||
}
|
||||
}()
|
||||
var out Out
|
||||
m := <-cin
|
||||
if m.panic == nil {
|
||||
out = f(m.val, yield)
|
||||
}
|
||||
running = false
|
||||
cout <- msg[Out]{val: out}
|
||||
}()
|
||||
return resume, cancel
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func lsmode(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultText(fs.FileMode(arg[0].Int()).String())
|
||||
}
|
||||
|
||||
func readfile(fsys fs.FS) func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
func readfile(fsys fs.FS) sqlite3.ScalarFunction {
|
||||
return func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
var err error
|
||||
var data []byte
|
||||
|
||||
@@ -2,6 +2,7 @@ package fileio
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"iter"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -62,12 +63,12 @@ func (d fsdir) Open() (sqlite3.VTabCursor, error) {
|
||||
|
||||
type cursor struct {
|
||||
fsdir
|
||||
base string
|
||||
resume resume
|
||||
cancel func()
|
||||
curr entry
|
||||
eof bool
|
||||
rowID int64
|
||||
base string
|
||||
next func() (entry, bool)
|
||||
stop func()
|
||||
curr entry
|
||||
eof bool
|
||||
rowID int64
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
@@ -77,8 +78,8 @@ type entry struct {
|
||||
}
|
||||
|
||||
func (c *cursor) Close() error {
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
if c.stop != nil {
|
||||
c.stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -101,14 +102,26 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
c.base = base
|
||||
}
|
||||
|
||||
c.resume, c.cancel = pull(c, root)
|
||||
c.next, c.stop = iter.Pull(func(yield func(entry) bool) {
|
||||
walkDir := func(path string, d fs.DirEntry, err error) error {
|
||||
if yield(entry{d, err, path}) {
|
||||
return nil
|
||||
}
|
||||
return fs.SkipAll
|
||||
}
|
||||
if c.fsys != nil {
|
||||
fs.WalkDir(c.fsys, root, walkDir)
|
||||
} else {
|
||||
filepath.WalkDir(root, walkDir)
|
||||
}
|
||||
})
|
||||
c.eof = false
|
||||
c.rowID = 0
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (c *cursor) Next() error {
|
||||
curr, ok := next(c)
|
||||
curr, ok := c.next()
|
||||
c.curr = curr
|
||||
c.eof = !ok
|
||||
c.rowID++
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
//go:build !go1.23
|
||||
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type resume = func(struct{}) (entry, bool)
|
||||
|
||||
func next(c *cursor) (entry, bool) {
|
||||
return c.resume(struct{}{})
|
||||
}
|
||||
|
||||
func pull(c *cursor, root string) (resume, func()) {
|
||||
return coroNew(func(_ struct{}, yield func(entry) struct{}) entry {
|
||||
walkDir := func(path string, d fs.DirEntry, err error) error {
|
||||
yield(entry{d, err, path})
|
||||
return nil
|
||||
}
|
||||
if c.fsys != nil {
|
||||
fs.WalkDir(c.fsys, root, walkDir)
|
||||
} else {
|
||||
filepath.WalkDir(root, walkDir)
|
||||
}
|
||||
return entry{}
|
||||
})
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
//go:build go1.23
|
||||
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"iter"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type resume = func() (entry, bool)
|
||||
|
||||
func next(c *cursor) (entry, bool) {
|
||||
return c.resume()
|
||||
}
|
||||
|
||||
func pull(c *cursor, root string) (resume, func()) {
|
||||
return iter.Pull(func(yield func(entry) bool) {
|
||||
walkDir := func(path string, d fs.DirEntry, err error) error {
|
||||
if yield(entry{d, err, path}) {
|
||||
return nil
|
||||
}
|
||||
return fs.SkipAll
|
||||
}
|
||||
if c.fsys != nil {
|
||||
fs.WalkDir(c.fsys, root, walkDir)
|
||||
} else {
|
||||
filepath.WalkDir(root, walkDir)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -111,22 +111,24 @@ loop2:
|
||||
return glob.String()
|
||||
}
|
||||
|
||||
func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
|
||||
func load(ctx sqlite3.Context, arg []sqlite3.Value, i int) (*regexp.Regexp, error) {
|
||||
re, ok := ctx.GetAuxData(i).(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
re, ok = arg[i].Pointer().(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(arg[i].Text())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
re = r
|
||||
}
|
||||
re = r
|
||||
ctx.SetAuxData(0, r)
|
||||
ctx.SetAuxData(i, re)
|
||||
}
|
||||
return re, nil
|
||||
}
|
||||
|
||||
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
_ = arg[1] // bounds check
|
||||
re, err := load(ctx, 0, arg[0].Text())
|
||||
re, err := load(ctx, arg, 0)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
@@ -136,18 +138,17 @@ func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
func regexLike(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
re, err := load(ctx, arg, 1)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
|
||||
text := arg[0].RawText()
|
||||
ctx.ResultBool(re.Match(text))
|
||||
}
|
||||
|
||||
func regexCount(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
re, err := load(ctx, arg, 1)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
@@ -162,7 +163,7 @@ func regexCount(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
re, err := load(ctx, arg, 1)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
@@ -187,7 +188,7 @@ func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
func regexInstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
re, err := load(ctx, arg, 1)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
@@ -215,16 +216,14 @@ func regexInstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
func regexReplace(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
_ = arg[2] // bounds check
|
||||
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
re, err := load(ctx, arg, 1)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
|
||||
text := arg[0].RawText()
|
||||
repl := arg[2].RawText()
|
||||
text := arg[0].RawText()
|
||||
var pos, n int
|
||||
if len(arg) > 3 {
|
||||
pos = arg[3].Int()
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
@@ -104,6 +105,27 @@ func TestRegister_errors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_pointer(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var got int
|
||||
err = db.QueryRow(`SELECT regexp_count('ABCABCAXYaxy', ?, 1)`,
|
||||
sqlite3.Pointer(regexp.MustCompile(`(?i)A.`))).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != 4 {
|
||||
t.Errorf("got %d, want %d", got, 4)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
re string
|
||||
|
||||
@@ -159,8 +159,6 @@ func (c *cursor) Close() error {
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
c.arg = arg
|
||||
c.rowID = 0
|
||||
err := errors.Join(
|
||||
c.stmt.Reset(),
|
||||
c.stmt.ClearBindings())
|
||||
@@ -187,6 +185,8 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.arg = append(c.arg[:0], arg...)
|
||||
c.rowID = 0
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const (
|
||||
some
|
||||
)
|
||||
|
||||
func newBoolean(kind int) func() sqlite3.AggregateFunction {
|
||||
func newBoolean(kind int) sqlite3.AggregateConstructor {
|
||||
return func() sqlite3.AggregateFunction { return &boolean{kind: kind} }
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const (
|
||||
percentile_disc
|
||||
)
|
||||
|
||||
func newPercentile(kind int) func() sqlite3.AggregateFunction {
|
||||
func newPercentile(kind int) sqlite3.AggregateConstructor {
|
||||
return func() sqlite3.AggregateFunction { return &percentile{kind: kind} }
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ func special(kind int, n int64) (null, zero bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func newVariance(kind int) func() sqlite3.AggregateFunction {
|
||||
func newVariance(kind int) sqlite3.AggregateConstructor {
|
||||
return func() sqlite3.AggregateFunction { return &variance{kind: kind} }
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ func (fn *variance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
}
|
||||
|
||||
func newCovariance(kind int) func() sqlite3.AggregateFunction {
|
||||
func newCovariance(kind int) sqlite3.AggregateConstructor {
|
||||
return func() sqlite3.AggregateFunction { return &covariance{kind: kind} }
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ func (fn *covariance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
}
|
||||
|
||||
func newMoments(kind int) func() sqlite3.AggregateFunction {
|
||||
func newMoments(kind int) sqlite3.AggregateConstructor {
|
||||
return func() sqlite3.AggregateFunction { return &momentfn{kind: kind} }
|
||||
}
|
||||
|
||||
|
||||
@@ -113,9 +113,8 @@ func upper(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
c := cases.Upper(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
cs = c
|
||||
cs = cases.Upper(t)
|
||||
ctx.SetAuxData(1, cs)
|
||||
}
|
||||
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
|
||||
}
|
||||
@@ -132,9 +131,8 @@ func lower(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
c := cases.Lower(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
cs = c
|
||||
cs = cases.Lower(t)
|
||||
ctx.SetAuxData(1, cs)
|
||||
}
|
||||
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
|
||||
}
|
||||
@@ -151,9 +149,8 @@ func initcap(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
c := cases.Title(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
cs = c
|
||||
cs = cases.Title(t)
|
||||
ctx.SetAuxData(1, cs)
|
||||
}
|
||||
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
|
||||
}
|
||||
@@ -200,13 +197,16 @@ func normalize(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, ok := ctx.GetAuxData(0).(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
re, ok = arg[0].Pointer().(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
re = r
|
||||
}
|
||||
re = r
|
||||
ctx.SetAuxData(0, r)
|
||||
ctx.SetAuxData(0, re)
|
||||
}
|
||||
ctx.ResultBool(re.Match(arg[1].RawText()))
|
||||
}
|
||||
|
||||
180
func.go
180
func.go
@@ -2,7 +2,10 @@ package sqlite3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"iter"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
|
||||
@@ -44,7 +47,7 @@ func (c Conn) AnyCollationNeeded() error {
|
||||
// CreateCollation defines a new collating sequence.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_collation.html
|
||||
func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
|
||||
func (c *Conn) CreateCollation(name string, fn CollatingFunction) error {
|
||||
var funcPtr ptr_t
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
@@ -56,6 +59,10 @@ func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
|
||||
return c.error(rc)
|
||||
}
|
||||
|
||||
// Collating function is the type of a collation callback.
|
||||
// Implementations must not retain a or b.
|
||||
type CollatingFunction func(a, b []byte) int
|
||||
|
||||
// CreateFunction defines a new scalar SQL function.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
@@ -76,28 +83,67 @@ func (c *Conn) CreateFunction(name string, nArg int, flag FunctionFlag, fn Scala
|
||||
// Implementations must not retain arg.
|
||||
type ScalarFunction func(ctx Context, arg ...Value)
|
||||
|
||||
// CreateWindowFunction defines a new aggregate or aggregate window SQL function.
|
||||
// If fn returns a [WindowFunction], then an aggregate window function is created.
|
||||
// If fn returns an [io.Closer], it will be called to free resources.
|
||||
// CreateAggregateFunction defines a new aggregate SQL function.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn func() AggregateFunction) error {
|
||||
func (c *Conn) CreateAggregateFunction(name string, nArg int, flag FunctionFlag, fn AggregateSeqFunction) error {
|
||||
var funcPtr ptr_t
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
if fn != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, fn)
|
||||
funcPtr = util.AddHandle(c.ctx, AggregateConstructor(func() AggregateFunction {
|
||||
var a aggregateFunc
|
||||
coro := func(yieldCoro func(struct{}) bool) {
|
||||
seq := func(yieldSeq func([]Value) bool) {
|
||||
for yieldSeq(a.arg) {
|
||||
if !yieldCoro(struct{}{}) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
fn(&a.ctx, seq)
|
||||
}
|
||||
a.next, a.stop = iter.Pull(coro)
|
||||
return &a
|
||||
}))
|
||||
}
|
||||
call := "sqlite3_create_aggregate_function_go"
|
||||
if _, ok := fn().(WindowFunction); ok {
|
||||
call = "sqlite3_create_window_function_go"
|
||||
}
|
||||
rc := res_t(c.call(call,
|
||||
rc := res_t(c.call("sqlite3_create_aggregate_function_go",
|
||||
stk_t(c.handle), stk_t(namePtr), stk_t(nArg),
|
||||
stk_t(flag), stk_t(funcPtr)))
|
||||
return c.error(rc)
|
||||
}
|
||||
|
||||
// AggregateSeqFunction is the type of an aggregate SQL function.
|
||||
// Implementations must not retain the slices yielded by seq.
|
||||
type AggregateSeqFunction func(ctx *Context, seq iter.Seq[[]Value])
|
||||
|
||||
// CreateWindowFunction defines a new aggregate or aggregate window SQL function.
|
||||
// If fn returns a [WindowFunction], an aggregate window function is created.
|
||||
// If fn returns an [io.Closer], it will be called to free resources.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn AggregateConstructor) error {
|
||||
var funcPtr ptr_t
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
if fn != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, AggregateConstructor(func() AggregateFunction {
|
||||
agg := fn()
|
||||
if win, ok := agg.(WindowFunction); ok {
|
||||
return win
|
||||
}
|
||||
return windowFunc{agg, name}
|
||||
}))
|
||||
}
|
||||
rc := res_t(c.call("sqlite3_create_window_function_go",
|
||||
stk_t(c.handle), stk_t(namePtr), stk_t(nArg),
|
||||
stk_t(flag), stk_t(funcPtr)))
|
||||
return c.error(rc)
|
||||
}
|
||||
|
||||
// AggregateConstructor is a an [AggregateFunction] constructor.
|
||||
type AggregateConstructor func() AggregateFunction
|
||||
|
||||
// AggregateFunction is the interface an aggregate function should implement.
|
||||
//
|
||||
// https://sqlite.org/appfunc.html
|
||||
@@ -146,51 +192,52 @@ func collationCallback(ctx context.Context, mod api.Module, pArg, pDB ptr_t, eTe
|
||||
}
|
||||
|
||||
func compareCallback(ctx context.Context, mod api.Module, pApp ptr_t, nKey1 int32, pKey1 ptr_t, nKey2 int32, pKey2 ptr_t) uint32 {
|
||||
fn := util.GetHandle(ctx, pApp).(func(a, b []byte) int)
|
||||
fn := util.GetHandle(ctx, pApp).(CollatingFunction)
|
||||
return uint32(fn(util.View(mod, pKey1, int64(nKey1)), util.View(mod, pKey2, int64(nKey2))))
|
||||
}
|
||||
|
||||
func funcCallback(ctx context.Context, mod api.Module, pCtx, pApp ptr_t, nArg int32, pArg ptr_t) {
|
||||
args := getFuncArgs()
|
||||
defer putFuncArgs(args)
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
args := callbackArgs(db, nArg, pArg)
|
||||
defer returnArgs(args)
|
||||
fn := util.GetHandle(db.ctx, pApp).(ScalarFunction)
|
||||
callbackArgs(db, args[:nArg], pArg)
|
||||
fn(Context{db, pCtx}, args[:nArg]...)
|
||||
fn(Context{db, pCtx}, *args...)
|
||||
}
|
||||
|
||||
func stepCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp ptr_t, nArg int32, pArg ptr_t) {
|
||||
args := getFuncArgs()
|
||||
defer putFuncArgs(args)
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
callbackArgs(db, args[:nArg], pArg)
|
||||
args := callbackArgs(db, nArg, pArg)
|
||||
defer returnArgs(args)
|
||||
fn, _ := callbackAggregate(db, pAgg, pApp)
|
||||
fn.Step(Context{db, pCtx}, args[:nArg]...)
|
||||
fn.Step(Context{db, pCtx}, *args...)
|
||||
}
|
||||
|
||||
func finalCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp ptr_t) {
|
||||
func valueCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp ptr_t, final int32) {
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
fn, handle := callbackAggregate(db, pAgg, pApp)
|
||||
fn.Value(Context{db, pCtx})
|
||||
if err := util.DelHandle(ctx, handle); err != nil {
|
||||
Context{db, pCtx}.ResultError(err)
|
||||
return // notest
|
||||
|
||||
// Cleanup.
|
||||
if final != 0 {
|
||||
var err error
|
||||
if handle != 0 {
|
||||
err = util.DelHandle(ctx, handle)
|
||||
} else if c, ok := fn.(io.Closer); ok {
|
||||
err = c.Close()
|
||||
}
|
||||
if err != nil {
|
||||
Context{db, pCtx}.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func valueCallback(ctx context.Context, mod api.Module, pCtx, pAgg ptr_t) {
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
fn := util.GetHandle(db.ctx, pAgg).(AggregateFunction)
|
||||
fn.Value(Context{db, pCtx})
|
||||
}
|
||||
|
||||
func inverseCallback(ctx context.Context, mod api.Module, pCtx, pAgg ptr_t, nArg int32, pArg ptr_t) {
|
||||
args := getFuncArgs()
|
||||
defer putFuncArgs(args)
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
callbackArgs(db, args[:nArg], pArg)
|
||||
args := callbackArgs(db, nArg, pArg)
|
||||
defer returnArgs(args)
|
||||
fn := util.GetHandle(db.ctx, pAgg).(WindowFunction)
|
||||
fn.Inverse(Context{db, pCtx}, args[:nArg]...)
|
||||
fn.Inverse(Context{db, pCtx}, *args...)
|
||||
}
|
||||
|
||||
func callbackAggregate(db *Conn, pAgg, pApp ptr_t) (AggregateFunction, ptr_t) {
|
||||
@@ -200,7 +247,7 @@ func callbackAggregate(db *Conn, pAgg, pApp ptr_t) (AggregateFunction, ptr_t) {
|
||||
}
|
||||
|
||||
// We need to create the aggregate.
|
||||
fn := util.GetHandle(db.ctx, pApp).(func() AggregateFunction)()
|
||||
fn := util.GetHandle(db.ctx, pApp).(AggregateConstructor)()
|
||||
if pAgg != 0 {
|
||||
handle := util.AddHandle(db.ctx, fn)
|
||||
util.Write32(db.mod, pAgg, handle)
|
||||
@@ -209,25 +256,64 @@ func callbackAggregate(db *Conn, pAgg, pApp ptr_t) (AggregateFunction, ptr_t) {
|
||||
return fn, 0
|
||||
}
|
||||
|
||||
func callbackArgs(db *Conn, arg []Value, pArg ptr_t) {
|
||||
for i := range arg {
|
||||
arg[i] = Value{
|
||||
var (
|
||||
valueArgsPool sync.Pool
|
||||
valueArgsLen atomic.Int32
|
||||
)
|
||||
|
||||
func callbackArgs(db *Conn, nArg int32, pArg ptr_t) *[]Value {
|
||||
arg, ok := valueArgsPool.Get().(*[]Value)
|
||||
if !ok || cap(*arg) < int(nArg) {
|
||||
max := valueArgsLen.Or(nArg) | nArg
|
||||
lst := make([]Value, max)
|
||||
arg = &lst
|
||||
}
|
||||
lst := (*arg)[:nArg]
|
||||
for i := range lst {
|
||||
lst[i] = Value{
|
||||
c: db,
|
||||
handle: util.Read32[ptr_t](db.mod, pArg+ptr_t(i)*ptrlen),
|
||||
}
|
||||
}
|
||||
*arg = lst
|
||||
return arg
|
||||
}
|
||||
|
||||
var funcArgsPool sync.Pool
|
||||
|
||||
func putFuncArgs(p *[_MAX_FUNCTION_ARG]Value) {
|
||||
funcArgsPool.Put(p)
|
||||
func returnArgs(p *[]Value) {
|
||||
valueArgsPool.Put(p)
|
||||
}
|
||||
|
||||
func getFuncArgs() *[_MAX_FUNCTION_ARG]Value {
|
||||
if p := funcArgsPool.Get(); p == nil {
|
||||
return new([_MAX_FUNCTION_ARG]Value)
|
||||
} else {
|
||||
return p.(*[_MAX_FUNCTION_ARG]Value)
|
||||
type aggregateFunc struct {
|
||||
ctx Context
|
||||
arg []Value
|
||||
next func() (struct{}, bool)
|
||||
stop func()
|
||||
}
|
||||
|
||||
func (a *aggregateFunc) Step(ctx Context, arg ...Value) {
|
||||
a.ctx = ctx
|
||||
a.arg = append(a.arg[:0], arg...)
|
||||
if _, more := a.next(); !more {
|
||||
a.stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *aggregateFunc) Value(ctx Context) {
|
||||
a.ctx = ctx
|
||||
a.stop()
|
||||
}
|
||||
|
||||
func (a *aggregateFunc) Close() error {
|
||||
a.stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
type windowFunc struct {
|
||||
AggregateFunction
|
||||
name string
|
||||
}
|
||||
|
||||
func (w windowFunc) Inverse(ctx Context, arg ...Value) {
|
||||
// Implementing inverse allows certain queries that don't really need it to succeed.
|
||||
ctx.ResultError(util.ErrorString(w.name + ": may not be used as a window function"))
|
||||
}
|
||||
|
||||
57
func_seq_test.go
Normal file
57
func_seq_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package sqlite3_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"log"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
func ExampleConn_CreateAggregateFunction() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO test VALUES (1), (2), (3)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.CreateAggregateFunction("seq_avg", 1, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS,
|
||||
func(ctx *sqlite3.Context, seq iter.Seq[[]sqlite3.Value]) {
|
||||
count := 0
|
||||
total := 0.0
|
||||
for arg := range seq {
|
||||
total += arg[0].Float()
|
||||
count++
|
||||
}
|
||||
ctx.ResultFloat(total / float64(count))
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT seq_avg(col) FROM test`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for stmt.Step() {
|
||||
fmt.Println(stmt.ColumnFloat(0))
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// 2
|
||||
}
|
||||
15
go.mod
15
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/ncruces/go-sqlite3
|
||||
|
||||
go 1.22.0
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
|
||||
@@ -8,17 +8,20 @@ require (
|
||||
github.com/ncruces/julianday v1.0.0
|
||||
github.com/ncruces/sort v0.1.5
|
||||
github.com/tetratelabs/wazero v1.9.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/sys v0.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dchest/siphash v1.2.3 // ext/bloom
|
||||
github.com/google/uuid v1.6.0 // ext/uuid
|
||||
github.com/psanford/httpreadat v0.1.0 // example
|
||||
golang.org/x/sync v0.11.0 // test
|
||||
golang.org/x/text v0.22.0 // ext/unicode
|
||||
golang.org/x/sync v0.12.0 // test
|
||||
golang.org/x/text v0.23.0 // ext/unicode
|
||||
lukechampine.com/adiantum v1.1.1 // vfs/adiantum
|
||||
)
|
||||
|
||||
retract v0.4.0 // tagged from the wrong branch
|
||||
retract (
|
||||
v0.23.2 // tagged from the wrong branch
|
||||
v0.4.0 // tagged from the wrong branch
|
||||
)
|
||||
|
||||
16
go.sum
16
go.sum
@@ -10,13 +10,13 @@ github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIw
|
||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
|
||||
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
module github.com/ncruces/go-sqlite3/gormlite
|
||||
|
||||
go 1.22.0
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
|
||||
require (
|
||||
github.com/ncruces/go-sqlite3 v0.23.0
|
||||
github.com/ncruces/go-sqlite3 v0.24.0
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.2 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,12 +2,12 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/ncruces/go-sqlite3 v0.23.0 h1:90j/ar8Ywu2AtsfDl5WhO9sgP/rNk76BcKGIzAHO8AQ=
|
||||
github.com/ncruces/go-sqlite3 v0.23.0/go.mod h1:gq2nriHSczOs11SqGW5+0X+SgLdkdj4K+j4F/AhQ+8g=
|
||||
github.com/ncruces/go-sqlite3 v0.24.0 h1:Z4jfmzu2NCd4SmyFwLT2OmF3EnTZbqwATvdiuNHNhLA=
|
||||
github.com/ncruces/go-sqlite3 v0.24.0/go.mod h1:/Vs8ACZHjJ1SA6E9RZUn3EyB1OP3nDQ4z/ar+0fplTQ=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
|
||||
@@ -317,8 +317,7 @@ func exportCallbacks(env wazero.HostModuleBuilder) wazero.HostModuleBuilder {
|
||||
util.ExportFuncVI(env, "go_destroy", destroyCallback)
|
||||
util.ExportFuncVIIII(env, "go_func", funcCallback)
|
||||
util.ExportFuncVIIIII(env, "go_step", stepCallback)
|
||||
util.ExportFuncVIII(env, "go_final", finalCallback)
|
||||
util.ExportFuncVII(env, "go_value", valueCallback)
|
||||
util.ExportFuncVIIII(env, "go_value", valueCallback)
|
||||
util.ExportFuncVIIII(env, "go_inverse", inverseCallback)
|
||||
util.ExportFuncVIIII(env, "go_collation_needed", collationCallback)
|
||||
util.ExportFuncIIIIII(env, "go_compare", compareCallback)
|
||||
|
||||
@@ -11,9 +11,8 @@ int go_compare(go_handle, int, const void *, int, const void *);
|
||||
void go_func(sqlite3_context *, go_handle, int, sqlite3_value **);
|
||||
|
||||
void go_step(sqlite3_context *, go_handle *, go_handle, int, sqlite3_value **);
|
||||
void go_final(sqlite3_context *, go_handle, go_handle);
|
||||
void go_value(sqlite3_context *, go_handle);
|
||||
void go_inverse(sqlite3_context *, go_handle *, int, sqlite3_value **);
|
||||
void go_value(sqlite3_context *, go_handle *, go_handle, bool);
|
||||
void go_inverse(sqlite3_context *, go_handle, int, sqlite3_value **);
|
||||
|
||||
void go_func_wrapper(sqlite3_context *ctx, int nArg, sqlite3_value **pArg) {
|
||||
go_func(ctx, sqlite3_user_data(ctx), nArg, pArg);
|
||||
@@ -28,22 +27,26 @@ void go_step_wrapper(sqlite3_context *ctx, int nArg, sqlite3_value **pArg) {
|
||||
go_step(ctx, agg, data, nArg, pArg);
|
||||
}
|
||||
|
||||
void go_value_wrapper(sqlite3_context *ctx) {
|
||||
go_handle *agg = sqlite3_aggregate_context(ctx, 4);
|
||||
go_handle data = NULL;
|
||||
if (agg == NULL || *agg == NULL) {
|
||||
data = sqlite3_user_data(ctx);
|
||||
}
|
||||
go_value(ctx, agg, data, /*final=*/false);
|
||||
}
|
||||
|
||||
void go_final_wrapper(sqlite3_context *ctx) {
|
||||
go_handle *agg = sqlite3_aggregate_context(ctx, 0);
|
||||
go_handle data = NULL;
|
||||
if (agg == NULL || *agg == NULL) {
|
||||
data = sqlite3_user_data(ctx);
|
||||
}
|
||||
go_final(ctx, agg, data);
|
||||
}
|
||||
|
||||
void go_value_wrapper(sqlite3_context *ctx) {
|
||||
go_handle *agg = sqlite3_aggregate_context(ctx, 4);
|
||||
go_value(ctx, *agg);
|
||||
go_value(ctx, agg, data, /*final=*/true);
|
||||
}
|
||||
|
||||
void go_inverse_wrapper(sqlite3_context *ctx, int nArg, sqlite3_value **pArg) {
|
||||
go_handle *agg = sqlite3_aggregate_context(ctx, 4);
|
||||
go_handle *agg = sqlite3_aggregate_context(ctx, 0);
|
||||
go_inverse(ctx, *agg, nArg, pArg);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ void go_log(void *, int, const char *);
|
||||
unsigned int go_autovacuum_pages(void *, const char *, unsigned int,
|
||||
unsigned int, unsigned int);
|
||||
|
||||
void sqlite3_log_go(int iErrCode, const char *zMsg) {
|
||||
sqlite3_log(iErrCode, "%s", zMsg);
|
||||
}
|
||||
|
||||
void sqlite3_progress_handler_go(sqlite3 *db, int n) {
|
||||
sqlite3_progress_handler(db, n, go_progress_handler, /*arg=*/NULL);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
@@ -10,7 +9,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
@@ -24,15 +22,18 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(func(c *sqlite3.Conn) error {
|
||||
return c.ConfigLog(func(code sqlite3.ExtendedErrorCode, msg string) {
|
||||
// Having to do journal recovery is unexpected.
|
||||
if errors.Is(code, sqlite3.NOTICE) {
|
||||
log.Panicf("%v (%d): %s", code, code, msg)
|
||||
} else {
|
||||
log.Printf("%v (%d): %s", code, code, msg)
|
||||
}
|
||||
})
|
||||
sqlite3.Initialize()
|
||||
sqlite3.ConfigLog(func(code sqlite3.ExtendedErrorCode, msg string) {
|
||||
switch code {
|
||||
case sqlite3.NOTICE_RECOVER_WAL:
|
||||
// Wal "recovery" is expected.
|
||||
break
|
||||
case sqlite3.NOTICE_RECOVER_ROLLBACK:
|
||||
// Rollback journal recovery is an error.
|
||||
log.Panicf("%v (%d): %s", code, code, msg)
|
||||
default:
|
||||
log.Printf("%v (%d): %s", code, code, msg)
|
||||
}
|
||||
})
|
||||
m.Run()
|
||||
}
|
||||
@@ -54,6 +55,7 @@ func Test_parallel(t *testing.T) {
|
||||
"?_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(truncate)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
createDB(t, name)
|
||||
testParallel(t, name, iter)
|
||||
testIntegrity(t, name)
|
||||
}
|
||||
@@ -67,7 +69,7 @@ func Test_wal(t *testing.T) {
|
||||
if testing.Short() {
|
||||
iter = 1000
|
||||
} else {
|
||||
iter = 2500
|
||||
iter = 5000
|
||||
}
|
||||
|
||||
name := "file:" +
|
||||
@@ -75,6 +77,7 @@ func Test_wal(t *testing.T) {
|
||||
"?_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(wal)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
createDB(t, name)
|
||||
testParallel(t, name, iter)
|
||||
testIntegrity(t, name)
|
||||
}
|
||||
@@ -90,6 +93,7 @@ func Test_memdb(t *testing.T) {
|
||||
name := memdb.TestDB(t, url.Values{
|
||||
"_pragma": {"busy_timeout(10000)"},
|
||||
})
|
||||
createDB(t, name)
|
||||
testParallel(t, name, iter)
|
||||
testIntegrity(t, name)
|
||||
}
|
||||
@@ -113,6 +117,7 @@ func Test_adiantum(t *testing.T) {
|
||||
"&_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(truncate)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
createDB(t, name)
|
||||
testParallel(t, name, iter)
|
||||
testIntegrity(t, name)
|
||||
}
|
||||
@@ -136,6 +141,7 @@ func Test_xts(t *testing.T) {
|
||||
"&_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(truncate)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
createDB(t, name)
|
||||
testParallel(t, name, iter)
|
||||
testIntegrity(t, name)
|
||||
}
|
||||
@@ -155,14 +161,17 @@ func Test_MultiProcess_rollback(t *testing.T) {
|
||||
"?_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(truncate)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
createDB(t, name)
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe, append(os.Args[1:], "-test.v", "-test.run=Test_ChildProcess_rollback")...)
|
||||
cmd := exec.Command(exe, append(os.Args[1:],
|
||||
"-test.v", "-test.count=1", "-test.run=Test_ChildProcess_rollback")...)
|
||||
out, err := cmd.StdoutPipe()
|
||||
cmd.Stderr = os.Stderr
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -214,14 +223,17 @@ func Test_MultiProcess_wal(t *testing.T) {
|
||||
"?_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(wal)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
createDB(t, name)
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe, append(os.Args[1:], "-test.v", "-test.run=Test_ChildProcess_wal")...)
|
||||
cmd := exec.Command(exe, append(os.Args[1:],
|
||||
"-test.v", "-test.count=1", "-test.run=Test_ChildProcess_wal")...)
|
||||
out, err := cmd.StdoutPipe()
|
||||
cmd.Stderr = os.Stderr
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -263,14 +275,14 @@ func Benchmark_parallel(b *testing.B) {
|
||||
b.Skip("skipping without shared memory")
|
||||
}
|
||||
|
||||
sqlite3.Initialize()
|
||||
b.ResetTimer()
|
||||
|
||||
name := "file:" +
|
||||
filepath.Join(b.TempDir(), "test.db") +
|
||||
"?_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(truncate)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
createDB(b, name)
|
||||
|
||||
b.ResetTimer()
|
||||
testParallel(b, name, b.N)
|
||||
}
|
||||
|
||||
@@ -279,55 +291,51 @@ func Benchmark_wal(b *testing.B) {
|
||||
b.Skip("skipping without shared memory")
|
||||
}
|
||||
|
||||
sqlite3.Initialize()
|
||||
b.ResetTimer()
|
||||
|
||||
name := "file:" +
|
||||
filepath.Join(b.TempDir(), "test.db") +
|
||||
"?_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(wal)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
createDB(b, name)
|
||||
|
||||
b.ResetTimer()
|
||||
testParallel(b, name, b.N)
|
||||
}
|
||||
|
||||
func Benchmark_memdb(b *testing.B) {
|
||||
sqlite3.Initialize()
|
||||
b.ResetTimer()
|
||||
|
||||
name := memdb.TestDB(b, url.Values{
|
||||
"_pragma": {"busy_timeout(10000)"},
|
||||
})
|
||||
createDB(b, name)
|
||||
|
||||
b.ResetTimer()
|
||||
testParallel(b, name, b.N)
|
||||
}
|
||||
|
||||
func createDB(t testing.TB, name string) {
|
||||
db, err := sqlite3.Open(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func testParallel(t testing.TB, name string, n int) {
|
||||
writer := func() error {
|
||||
db, err := sqlite3.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("writer: open: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.BusyHandler(func(ctx context.Context, count int) (retry bool) {
|
||||
select {
|
||||
case <-time.After(time.Millisecond):
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO users (id, name) VALUES (0, 'go'), (1, 'zig'), (2, 'whatever')`)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("writer: insert: %w", err)
|
||||
}
|
||||
|
||||
return db.Close()
|
||||
@@ -336,13 +344,13 @@ func testParallel(t testing.TB, name string, n int) {
|
||||
reader := func() error {
|
||||
db, err := sqlite3.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("reader: open: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT id, name FROM users`)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("reader: select: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
@@ -351,15 +359,15 @@ func testParallel(t testing.TB, name string, n int) {
|
||||
row++
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("reader: step: %w", err)
|
||||
}
|
||||
if row%3 != 0 {
|
||||
t.Errorf("got %d rows, want multiple of 3", row)
|
||||
return fmt.Errorf("reader: got %d rows, want multiple of 3", row)
|
||||
}
|
||||
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("reader: close: %w", err)
|
||||
}
|
||||
|
||||
return db.Close()
|
||||
|
||||
@@ -6,22 +6,30 @@ It replaces the default SQLite VFS with a **pure Go** implementation,
|
||||
and exposes [interfaces](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs#VFS)
|
||||
that should allow you to implement your own [custom VFSes](#custom-vfses).
|
||||
|
||||
Since it is a from scratch reimplementation,
|
||||
there are naturally some ways it deviates from the original.
|
||||
See the [support matrix](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix)
|
||||
for the list of supported OS and CPU architectures.
|
||||
|
||||
The main differences are [file locking](#file-locking) and [WAL mode](#write-ahead-logging) support.
|
||||
Since this is a from scratch reimplementation,
|
||||
there are naturally some ways it deviates from the original.
|
||||
It's also not as battle tested as the original.
|
||||
|
||||
The main differences to be aware of are
|
||||
[file locking](#file-locking) and
|
||||
[WAL mode](#write-ahead-logging) support.
|
||||
|
||||
### File Locking
|
||||
|
||||
POSIX advisory locks, which SQLite uses on Unix, are
|
||||
[broken by design](https://github.com/sqlite/sqlite/blob/b74eb0/src/os_unix.c#L1073-L1161).
|
||||
POSIX advisory locks,
|
||||
which SQLite uses on [Unix](https://github.com/sqlite/sqlite/blob/5d60f4/src/os_unix.c#L13-L14),
|
||||
are [broken by design](https://github.com/sqlite/sqlite/blob/5d60f4/src/os_unix.c#L1074-L1162).
|
||||
Instead, on Linux and macOS, this package uses
|
||||
[OFD locks](https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html)
|
||||
to synchronize access to database files.
|
||||
|
||||
This package can also use
|
||||
[BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2),
|
||||
albeit with reduced concurrency (`BEGIN IMMEDIATE` behaves like `BEGIN EXCLUSIVE`).
|
||||
albeit with reduced concurrency (`BEGIN IMMEDIATE` behaves like `BEGIN EXCLUSIVE`,
|
||||
[docs](https://sqlite.org/lang_transaction.html#immediate)).
|
||||
BSD locks are the default on BSD and illumos,
|
||||
but you can opt into them with the `sqlite3_flock` build tag.
|
||||
|
||||
@@ -44,11 +52,11 @@ to check if your build supports file locking.
|
||||
|
||||
### Write-Ahead Logging
|
||||
|
||||
On Unix, this package may use `mmap` to implement
|
||||
On Unix, this package uses `mmap` to implement
|
||||
[shared-memory for the WAL-index](https://sqlite.org/wal.html#implementation_of_shared_memory_for_the_wal_index),
|
||||
like SQLite.
|
||||
|
||||
On Windows, this package may use `MapViewOfFile`, like SQLite.
|
||||
On Windows, this package uses `MapViewOfFile`, like SQLite.
|
||||
|
||||
You can also opt into a cross-platform, in-process, memory sharing implementation
|
||||
with the `sqlite3_dotlk` build tag.
|
||||
@@ -63,6 +71,11 @@ you must disable connection pooling by calling
|
||||
You can use [`vfs.SupportsSharedMemory`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs#SupportsSharedMemory)
|
||||
to check if your build supports shared memory.
|
||||
|
||||
### Blocking Locks
|
||||
|
||||
On Windows and macOS, this package implements
|
||||
[Wal-mode blocking locks](https://sqlite.org/src/doc/tip/doc/wal-lock.md).
|
||||
|
||||
### Batch-Atomic Write
|
||||
|
||||
On Linux, this package may support
|
||||
@@ -94,8 +107,10 @@ The VFS can be customized with a few build tags:
|
||||
> [`unix-flock` VFS](https://sqlite.org/compile.html#enable_locking_style);
|
||||
> `sqlite3_dotlk` builds are compatible with the
|
||||
> [`unix-dotfile` VFS](https://sqlite.org/compile.html#enable_locking_style).
|
||||
> If incompatible file locking is used, accessing databases concurrently with
|
||||
> _other_ SQLite libraries will eventually corrupt data.
|
||||
|
||||
> [!CAUTION]
|
||||
> Concurrently accessing databases using incompatible VFSes
|
||||
> will eventually corrupt data.
|
||||
|
||||
### Custom VFSes
|
||||
|
||||
|
||||
11
vfs/cksm.go
11
vfs/cksm.go
@@ -47,11 +47,12 @@ type cksmFlags struct {
|
||||
|
||||
func (c cksmFile) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
n, err = c.File.ReadAt(p, off)
|
||||
p = p[:n]
|
||||
|
||||
// SQLite is reading the header of a database file.
|
||||
if c.isDB && off == 0 && len(p) >= 100 &&
|
||||
bytes.HasPrefix(p, []byte("SQLite format 3\000")) {
|
||||
c.init(p)
|
||||
c.init((*[100]byte)(p))
|
||||
}
|
||||
|
||||
// Verify checksums.
|
||||
@@ -69,7 +70,7 @@ func (c cksmFile) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
// SQLite is writing the first page of a database file.
|
||||
if c.isDB && off == 0 && len(p) >= 100 &&
|
||||
bytes.HasPrefix(p, []byte("SQLite format 3\000")) {
|
||||
c.init(p)
|
||||
c.init((*[100]byte)(p))
|
||||
}
|
||||
|
||||
// Compute checksums.
|
||||
@@ -122,12 +123,16 @@ func (c cksmFile) fileControl(ctx context.Context, mod api.Module, op _FcntlOpco
|
||||
return vfsFileControlImpl(ctx, mod, c.File, op, pArg)
|
||||
}
|
||||
|
||||
func (f *cksmFlags) init(header []byte) {
|
||||
func (f *cksmFlags) init(header *[100]byte) {
|
||||
f.pageSize = 256 * int(binary.LittleEndian.Uint16(header[16:18]))
|
||||
if r := header[20] == 8; r != f.computeCksm {
|
||||
f.computeCksm = r
|
||||
f.verifyCksm = r
|
||||
}
|
||||
if !sql3util.ValidPageSize(f.pageSize) {
|
||||
f.computeCksm = false
|
||||
f.verifyCksm = false
|
||||
}
|
||||
}
|
||||
|
||||
func cksmCompute(a []byte) (cksm [8]byte) {
|
||||
|
||||
@@ -31,6 +31,7 @@ const (
|
||||
_READONLY _ErrorCode = util.READONLY
|
||||
_IOERR _ErrorCode = util.IOERR
|
||||
_NOTFOUND _ErrorCode = util.NOTFOUND
|
||||
_FULL _ErrorCode = util.FULL
|
||||
_CANTOPEN _ErrorCode = util.CANTOPEN
|
||||
_IOERR_READ _ErrorCode = util.IOERR_READ
|
||||
_IOERR_SHORT_READ _ErrorCode = util.IOERR_SHORT_READ
|
||||
@@ -57,10 +58,12 @@ const (
|
||||
_IOERR_COMMIT_ATOMIC _ErrorCode = util.IOERR_COMMIT_ATOMIC
|
||||
_IOERR_ROLLBACK_ATOMIC _ErrorCode = util.IOERR_ROLLBACK_ATOMIC
|
||||
_IOERR_DATA _ErrorCode = util.IOERR_DATA
|
||||
_IOERR_CORRUPTFS _ErrorCode = util.IOERR_CORRUPTFS
|
||||
_BUSY_SNAPSHOT _ErrorCode = util.BUSY_SNAPSHOT
|
||||
_CANTOPEN_FULLPATH _ErrorCode = util.CANTOPEN_FULLPATH
|
||||
_CANTOPEN_ISDIR _ErrorCode = util.CANTOPEN_ISDIR
|
||||
_READONLY_CANTINIT _ErrorCode = util.READONLY_CANTINIT
|
||||
_READONLY_DIRECTORY _ErrorCode = util.READONLY_DIRECTORY
|
||||
_OK_SYMLINK _ErrorCode = util.OK_SYMLINK
|
||||
)
|
||||
|
||||
|
||||
27
vfs/file.go
27
vfs/file.go
@@ -88,10 +88,13 @@ func (vfsOS) OpenFilename(name *Filename, flags OpenFlag) (File, OpenFlag, error
|
||||
oflags |= os.O_RDWR
|
||||
}
|
||||
|
||||
isCreate := flags&(OPEN_CREATE) != 0
|
||||
isJournl := flags&(OPEN_MAIN_JOURNAL|OPEN_SUPER_JOURNAL|OPEN_WAL) != 0
|
||||
|
||||
var err error
|
||||
var f *os.File
|
||||
if name == nil {
|
||||
f, err = os.CreateTemp("", "*.db")
|
||||
f, err = os.CreateTemp(os.Getenv("SQLITE_TMPDIR"), "*.db")
|
||||
} else {
|
||||
f, err = osutil.OpenFile(name.String(), oflags, 0666)
|
||||
}
|
||||
@@ -102,6 +105,10 @@ func (vfsOS) OpenFilename(name *Filename, flags OpenFlag) (File, OpenFlag, error
|
||||
if errors.Is(err, syscall.EISDIR) {
|
||||
return nil, flags, _CANTOPEN_ISDIR
|
||||
}
|
||||
if isCreate && isJournl && errors.Is(err, fs.ErrPermission) &&
|
||||
osAccess(name.String(), ACCESS_EXISTS) != nil {
|
||||
return nil, flags, _READONLY_DIRECTORY
|
||||
}
|
||||
return nil, flags, err
|
||||
}
|
||||
|
||||
@@ -118,11 +125,10 @@ func (vfsOS) OpenFilename(name *Filename, flags OpenFlag) (File, OpenFlag, error
|
||||
file := vfsFile{
|
||||
File: f,
|
||||
psow: true,
|
||||
atomic: osBatchAtomic(f),
|
||||
readOnly: flags&OPEN_READONLY != 0,
|
||||
syncDir: canSyncDirs &&
|
||||
flags&(OPEN_MAIN_JOURNAL|OPEN_SUPER_JOURNAL|OPEN_WAL) != 0 &&
|
||||
flags&(OPEN_CREATE) != 0,
|
||||
shm: NewSharedMemory(name.String()+"-shm", flags),
|
||||
syncDir: canSyncDirs && isCreate && isJournl,
|
||||
shm: NewSharedMemory(name.String()+"-shm", flags),
|
||||
}
|
||||
return &file, flags, nil
|
||||
}
|
||||
@@ -134,6 +140,7 @@ type vfsFile struct {
|
||||
readOnly bool
|
||||
keepWAL bool
|
||||
syncDir bool
|
||||
atomic bool
|
||||
psow bool
|
||||
}
|
||||
|
||||
@@ -154,6 +161,14 @@ func (f *vfsFile) Close() error {
|
||||
return f.File.Close()
|
||||
}
|
||||
|
||||
func (f *vfsFile) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
return osReadAt(f.File, p, off)
|
||||
}
|
||||
|
||||
func (f *vfsFile) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
return osWriteAt(f.File, p, off)
|
||||
}
|
||||
|
||||
func (f *vfsFile) Sync(flags SyncFlag) error {
|
||||
dataonly := (flags & SYNC_DATAONLY) != 0
|
||||
fullsync := (flags & 0x0f) == SYNC_FULL
|
||||
@@ -187,7 +202,7 @@ func (f *vfsFile) SectorSize() int {
|
||||
|
||||
func (f *vfsFile) DeviceCharacteristics() DeviceCharacteristic {
|
||||
ret := IOCAP_SUBPAGE_READ
|
||||
if osBatchAtomic(f.File) {
|
||||
if f.atomic {
|
||||
ret |= IOCAP_BATCH_ATOMIC
|
||||
}
|
||||
if f.psow {
|
||||
|
||||
@@ -50,11 +50,15 @@ func osDowngradeLock(file *os.File, _ LockLevel) _ErrorCode {
|
||||
}
|
||||
|
||||
func osReleaseLock(file *os.File, _ LockLevel) _ErrorCode {
|
||||
err := unix.Flock(int(file.Fd()), unix.LOCK_UN)
|
||||
if err != nil {
|
||||
return _IOERR_UNLOCK
|
||||
for {
|
||||
err := unix.Flock(int(file.Fd()), unix.LOCK_UN)
|
||||
if err == nil {
|
||||
return _OK
|
||||
}
|
||||
if err != unix.EINTR {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
}
|
||||
return _OK
|
||||
}
|
||||
|
||||
func osCheckReservedLock(file *os.File) (bool, _ErrorCode) {
|
||||
@@ -89,13 +93,18 @@ func osLock(file *os.File, typ int16, start, len int64, def _ErrorCode) _ErrorCo
|
||||
}
|
||||
|
||||
func osUnlock(file *os.File, start, len int64) _ErrorCode {
|
||||
err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{
|
||||
lock := unix.Flock_t{
|
||||
Type: unix.F_UNLCK,
|
||||
Start: start,
|
||||
Len: len,
|
||||
})
|
||||
if err != nil {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
return _OK
|
||||
for {
|
||||
err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &lock)
|
||||
if err == nil {
|
||||
return _OK
|
||||
}
|
||||
if err != unix.EINTR {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,12 @@ func osSync(file *os.File, fullsync, _ /*dataonly*/ bool) error {
|
||||
if fullsync {
|
||||
return file.Sync()
|
||||
}
|
||||
return unix.Fsync(int(file.Fd()))
|
||||
for {
|
||||
err := unix.Fsync(int(file.Fd()))
|
||||
if err != unix.EINTR {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func osAllocate(file *os.File, size int64) error {
|
||||
@@ -85,13 +90,18 @@ func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, d
|
||||
}
|
||||
|
||||
func osUnlock(file *os.File, start, len int64) _ErrorCode {
|
||||
err := unix.FcntlFlock(file.Fd(), _F_OFD_SETLK, &unix.Flock_t{
|
||||
lock := unix.Flock_t{
|
||||
Type: unix.F_UNLCK,
|
||||
Start: start,
|
||||
Len: len,
|
||||
})
|
||||
if err != nil {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
return _OK
|
||||
for {
|
||||
err := unix.FcntlFlock(file.Fd(), _F_OFD_SETLK, &lock)
|
||||
if err == nil {
|
||||
return _OK
|
||||
}
|
||||
if err != unix.EINTR {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -11,14 +12,36 @@ import (
|
||||
|
||||
func osSync(file *os.File, _ /*fullsync*/, _ /*dataonly*/ bool) error {
|
||||
// SQLite trusts Linux's fdatasync for all fsync's.
|
||||
return unix.Fdatasync(int(file.Fd()))
|
||||
for {
|
||||
err := unix.Fdatasync(int(file.Fd()))
|
||||
if err != unix.EINTR {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func osAllocate(file *os.File, size int64) error {
|
||||
if size == 0 {
|
||||
return nil
|
||||
}
|
||||
return unix.Fallocate(int(file.Fd()), 0, 0, size)
|
||||
for {
|
||||
err := unix.Fallocate(int(file.Fd()), 0, 0, size)
|
||||
if err == unix.EOPNOTSUPP {
|
||||
break
|
||||
}
|
||||
if err != unix.EINTR {
|
||||
return err
|
||||
}
|
||||
}
|
||||
off, err := file.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if size <= off {
|
||||
return nil
|
||||
}
|
||||
return file.Truncate(size)
|
||||
|
||||
}
|
||||
|
||||
func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
|
||||
@@ -46,13 +69,18 @@ func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, d
|
||||
}
|
||||
|
||||
func osUnlock(file *os.File, start, len int64) _ErrorCode {
|
||||
err := unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &unix.Flock_t{
|
||||
lock := unix.Flock_t{
|
||||
Type: unix.F_UNLCK,
|
||||
Start: start,
|
||||
Len: len,
|
||||
})
|
||||
if err != nil {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
return _OK
|
||||
for {
|
||||
err := unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock)
|
||||
if err == nil {
|
||||
return _OK
|
||||
}
|
||||
if err != unix.EINTR {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
vfs/os_std_rw.go
Normal file
13
vfs/os_std_rw.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !unix && (!windows || sqlite3_dotlk)
|
||||
|
||||
package vfs
|
||||
|
||||
import "os"
|
||||
|
||||
func osReadAt(file *os.File, p []byte, off int64) (int, error) {
|
||||
return file.ReadAt(p, off)
|
||||
}
|
||||
|
||||
func osWriteAt(file *os.File, p []byte, off int64) (int, error) {
|
||||
return file.WriteAt(p, off)
|
||||
}
|
||||
@@ -25,6 +25,28 @@ func osAccess(path string, flags AccessFlag) error {
|
||||
return unix.Access(path, access)
|
||||
}
|
||||
|
||||
func osReadAt(file *os.File, p []byte, off int64) (int, error) {
|
||||
n, err := file.ReadAt(p, off)
|
||||
if errno, ok := err.(unix.Errno); ok {
|
||||
switch errno {
|
||||
case
|
||||
unix.ERANGE,
|
||||
unix.EIO,
|
||||
unix.ENXIO:
|
||||
return n, _IOERR_CORRUPTFS
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func osWriteAt(file *os.File, p []byte, off int64) (int, error) {
|
||||
n, err := file.WriteAt(p, off)
|
||||
if errno, ok := err.(unix.Errno); ok && errno == unix.ENOSPC {
|
||||
return n, _FULL
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func osSetMode(file *os.File, modeof string) error {
|
||||
fi, err := os.Stat(modeof)
|
||||
if err != nil {
|
||||
@@ -43,10 +65,15 @@ func osTestLock(file *os.File, start, len int64) (int16, _ErrorCode) {
|
||||
Start: start,
|
||||
Len: len,
|
||||
}
|
||||
if unix.FcntlFlock(file.Fd(), unix.F_GETLK, &lock) != nil {
|
||||
return 0, _IOERR_CHECKRESERVEDLOCK
|
||||
for {
|
||||
err := unix.FcntlFlock(file.Fd(), unix.F_GETLK, &lock)
|
||||
if err == nil {
|
||||
return lock.Type, _OK
|
||||
}
|
||||
if err != unix.EINTR {
|
||||
return 0, _IOERR_CHECKRESERVEDLOCK
|
||||
}
|
||||
}
|
||||
return lock.Type, _OK
|
||||
}
|
||||
|
||||
func osLockErrorCode(err error, def _ErrorCode) _ErrorCode {
|
||||
|
||||
@@ -9,6 +9,23 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func osReadAt(file *os.File, p []byte, off int64) (int, error) {
|
||||
return file.ReadAt(p, off)
|
||||
}
|
||||
|
||||
func osWriteAt(file *os.File, p []byte, off int64) (int, error) {
|
||||
n, err := file.WriteAt(p, off)
|
||||
if errno, ok := err.(windows.Errno); ok {
|
||||
switch errno {
|
||||
case
|
||||
windows.ERROR_HANDLE_DISK_FULL,
|
||||
windows.ERROR_DISK_FULL:
|
||||
return n, _FULL
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func osGetSharedLock(file *os.File) _ErrorCode {
|
||||
// Acquire the PENDING lock temporarily before acquiring a new SHARED lock.
|
||||
rc := osReadLock(file, _PENDING_BYTE, 1, 0)
|
||||
|
||||
@@ -68,16 +68,11 @@ func (s *vfsShm) Close() error {
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
|
||||
func (s *vfsShm) shmOpen() _ErrorCode {
|
||||
func (s *vfsShm) shmOpen() (rc _ErrorCode) {
|
||||
if s.vfsShmParent != nil {
|
||||
return _OK
|
||||
}
|
||||
|
||||
var f *os.File
|
||||
// Close file on error.
|
||||
// Keep this here to avoid confusing checklocks.
|
||||
defer func() { f.Close() }()
|
||||
|
||||
vfsShmListMtx.Lock()
|
||||
defer vfsShmListMtx.Unlock()
|
||||
|
||||
@@ -98,11 +93,16 @@ func (s *vfsShm) shmOpen() _ErrorCode {
|
||||
}
|
||||
|
||||
// Always open file read-write, as it will be shared.
|
||||
f, err = os.OpenFile(s.path,
|
||||
f, err := os.OpenFile(s.path,
|
||||
os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666)
|
||||
if err != nil {
|
||||
return _CANTOPEN
|
||||
}
|
||||
defer func() {
|
||||
if rc != _OK {
|
||||
f.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Dead man's switch.
|
||||
if lock, rc := osTestLock(f, _SHM_DMS, 1); rc != _OK {
|
||||
@@ -131,7 +131,6 @@ func (s *vfsShm) shmOpen() _ErrorCode {
|
||||
File: f,
|
||||
info: fi,
|
||||
}
|
||||
f = nil // Don't close the file.
|
||||
for i, g := range vfsShmList {
|
||||
if g == nil {
|
||||
vfsShmList[i] = s.vfsShmParent
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
@@ -39,9 +38,7 @@ var (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ctx := context.Background()
|
||||
cfg := wazero.NewRuntimeConfig().
|
||||
WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads).
|
||||
WithMemoryLimitPages(512)
|
||||
cfg := wazero.NewRuntimeConfig().WithMemoryLimitPages(512)
|
||||
rt = wazero.NewRuntimeWithConfig(ctx, cfg)
|
||||
wasi_snapshot_preview1.MustInstantiate(ctx, rt)
|
||||
env := vfs.ExportHostFunctions(rt.NewHostModuleBuilder("env"))
|
||||
|
||||
@@ -15,8 +15,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
@@ -37,9 +35,7 @@ func TestMain(m *testing.M) {
|
||||
initFlags()
|
||||
|
||||
ctx := context.Background()
|
||||
cfg := wazero.NewRuntimeConfig().
|
||||
WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads).
|
||||
WithMemoryLimitPages(512)
|
||||
cfg := wazero.NewRuntimeConfig().WithMemoryLimitPages(512)
|
||||
rt = wazero.NewRuntimeWithConfig(ctx, cfg)
|
||||
wasi_snapshot_preview1.MustInstantiate(ctx, rt)
|
||||
env := vfs.ExportHostFunctions(rt.NewHostModuleBuilder("env"))
|
||||
|
||||
22
vtab.go
22
vtab.go
@@ -162,6 +162,7 @@ type VTabDestroyer interface {
|
||||
}
|
||||
|
||||
// A VTabUpdater allows a virtual table to be updated.
|
||||
// Implementations must not retain arg.
|
||||
type VTabUpdater interface {
|
||||
VTab
|
||||
// https://sqlite.org/vtab.html#xupdate
|
||||
@@ -241,6 +242,7 @@ type VTabSavepointer interface {
|
||||
// to loop through the virtual table.
|
||||
// A VTabCursor may optionally implement
|
||||
// [io.Closer] to free resources.
|
||||
// Implementations of Filter must not retain arg.
|
||||
//
|
||||
// https://sqlite.org/c3ref/vtab_cursor.html
|
||||
type VTabCursor interface {
|
||||
@@ -489,12 +491,12 @@ func vtabBestIndexCallback(ctx context.Context, mod api.Module, pVTab, pIdxInfo
|
||||
}
|
||||
|
||||
func vtabUpdateCallback(ctx context.Context, mod api.Module, pVTab ptr_t, nArg int32, pArg, pRowID ptr_t) res_t {
|
||||
vtab := vtabGetHandle(ctx, mod, pVTab).(VTabUpdater)
|
||||
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
args := make([]Value, nArg)
|
||||
callbackArgs(db, args, pArg)
|
||||
rowID, err := vtab.Update(args...)
|
||||
args := callbackArgs(db, nArg, pArg)
|
||||
defer returnArgs(args)
|
||||
|
||||
vtab := vtabGetHandle(ctx, mod, pVTab).(VTabUpdater)
|
||||
rowID, err := vtab.Update(*args...)
|
||||
if err == nil {
|
||||
util.Write64(mod, pRowID, rowID)
|
||||
}
|
||||
@@ -593,15 +595,17 @@ func cursorCloseCallback(ctx context.Context, mod api.Module, pCur ptr_t) res_t
|
||||
}
|
||||
|
||||
func cursorFilterCallback(ctx context.Context, mod api.Module, pCur ptr_t, idxNum int32, idxStr ptr_t, nArg int32, pArg ptr_t) res_t {
|
||||
cursor := vtabGetHandle(ctx, mod, pCur).(VTabCursor)
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
args := make([]Value, nArg)
|
||||
callbackArgs(db, args, pArg)
|
||||
args := callbackArgs(db, nArg, pArg)
|
||||
defer returnArgs(args)
|
||||
|
||||
var idxName string
|
||||
if idxStr != 0 {
|
||||
idxName = util.ReadString(mod, idxStr, _MAX_LENGTH)
|
||||
}
|
||||
err := cursor.Filter(int(idxNum), idxName, args...)
|
||||
|
||||
cursor := vtabGetHandle(ctx, mod, pCur).(VTabCursor)
|
||||
err := cursor.Filter(int(idxNum), idxName, *args...)
|
||||
return vtabError(ctx, mod, pCur, _CURSOR_ERROR, err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user