Compare commits

..

36 Commits

Author SHA1 Message Date
Nuno Cruces
d78239bfbf More EINTR. 2025-03-14 00:07:09 +00:00
Nuno Cruces
49852732b2 Optimize. 2025-03-12 17:29:12 +00:00
Nuno Cruces
9b90d076cb Update README.md 2025-03-12 12:01:13 +00:00
Nuno Cruces
15b94577b1 Tweak. 2025-03-11 20:15:53 +00:00
Nuno Cruces
25557244cc Global ConfigLog. 2025-03-11 17:07:56 +00:00
Nuno Cruces
c2d3bf0cfc Reduce flakyness. 2025-03-11 14:57:48 +00:00
Nuno Cruces
58a5682084 Handle EINTR. 2025-03-11 12:07:14 +00:00
Nuno Cruces
1ed954e96f Fix #243. 2025-03-10 14:54:34 +00:00
Nuno Cruces
9e7a0a875d Improved arg reuse. 2025-03-10 12:01:15 +00:00
Nuno Cruces
26adda4529 Seq aggregate functions (#229) 2025-03-08 14:07:43 +00:00
Nuno Cruces
2f6cd8de1d Docs. 2025-03-07 11:47:02 +00:00
dependabot[bot]
e027e055ff Bump golang.org/x/crypto from 0.35.0 to 0.36.0 (#239)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/crypto/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 23:11:20 +00:00
dependabot[bot]
63fdc141e5 Bump golang.org/x/text from 0.22.0 to 0.23.0 (#240)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 22:57:38 +00:00
Nuno Cruces
0bbd145a49 Update modules. 2025-02-28 16:57:25 +00:00
Nuno Cruces
c755ef96e6 Export logging. 2025-02-28 14:50:22 +00:00
Nuno Cruces
9a69e407cc Fix #235. 2025-02-28 00:33:45 +00:00
Nuno Cruces
e9db0d8e84 Issue #233. 2025-02-27 00:07:49 +00:00
dependabot[bot]
dadf53e175 Bump golang.org/x/crypto from 0.33.0 to 0.35.0 (#231)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.33.0 to 0.35.0.
- [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 23:15:52 +00:00
Nuno Cruces
f536765206 Go 1.23. 2025-02-24 14:09:55 +00:00
Nuno Cruces
12034c4f0b Retract. 2025-02-24 14:09:55 +00:00
Nuno Cruces
b4e5d1a213 Issue #230. 2025-02-24 13:13:25 +00:00
Nuno Cruces
b06c7dda6c Checksum robustness. 2025-02-24 13:13:25 +00:00
Nuno Cruces
5e1909a20e Issue #230. 2025-02-24 13:13:25 +00:00
Nuno Cruces
77d74baca5 Fix potential leak. 2025-02-22 12:48:41 +00:00
Nuno Cruces
4142680d5a Updated modules. 2025-02-20 13:36:02 +00:00
Nuno Cruces
9f4fe6f27c SQLite 3.49.1. 2025-02-18 18:03:20 +00:00
Nuno Cruces
7870ce0690 wazero v1.9.0. 2025-02-18 16:36:22 +00:00
Nuno Cruces
ec3226e16e Fix CI. 2025-02-17 12:21:53 +00:00
Nuno Cruces
4dd7bd0ff2 More type safe. 2025-02-17 12:00:55 +00:00
Nuno Cruces
975feb2fd4 Issue #228. 2025-02-16 18:09:42 +00:00
Nuno Cruces
58f8c2d33e Ignore. 2025-02-15 01:12:38 +00:00
Nuno Cruces
019660eed6 Fix warning. 2025-02-12 09:58:58 +00:00
Nuno Cruces
30c1bcdbe9 Serdes robustness. 2025-02-12 00:41:16 +00:00
Nuno Cruces
9b4002f5ac Add missing consts. 2025-02-11 18:24:05 +00:00
Nuno Cruces
2a78d4bc2b Updated modules. 2025-02-11 18:15:14 +00:00
Nuno Cruces
c09623a903 binaryen-version_122. 2025-02-11 18:07:30 +00:00
72 changed files with 813 additions and 596 deletions

View File

@@ -1,11 +1,6 @@
name: VM Actions matrix
description: VM Actions matrix template
inputs:
run:
description: The CI command to run
required: true
runs:
using: composite
steps:
@@ -13,4 +8,4 @@ runs:
with:
usesh: true
copyback: false
run: ${{inputs.run}}
run: . ./test.sh

View File

@@ -3,13 +3,13 @@ set -euo pipefail
if [[ "$OSTYPE" == "linux"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-x86_64-linux.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-x86_64-linux.tar.gz"
elif [[ "$OSTYPE" == "darwin"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-arm64-macos.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-arm64-macos.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-arm64-macos.tar.gz"
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-windows.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-x86_64-windows.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-x86_64-windows.tar.gz"
fi
# Download tools

View File

@@ -7,12 +7,14 @@ on:
- '**.go'
- '**.mod'
- '**.wasm'
- '**.yml'
pull_request:
branches: [ 'main' ]
paths:
- '**.go'
- '**.mod'
- '**.wasm'
- '**.yml'
workflow_dispatch:
jobs:
@@ -70,7 +72,9 @@ jobs:
if: matrix.os != 'windows-latest'
- name: Collect coverage
run: go run github.com/dave/courtney@latest
run: |
go get -tool github.com/dave/courtney@v0.4.4
go tool courtney
if: |
github.event_name == 'push' &&
matrix.os == 'ubuntu-latest'
@@ -158,10 +162,6 @@ jobs:
- name: Test
uses: ./.github/actions/vmactions
with:
usesh: true
copyback: false
run: . ./test.sh
test-wasip1:
runs-on: ubuntu-latest
@@ -174,7 +174,7 @@ jobs:
with: { go-version: stable }
- name: Set path
run: echo "$(go env GOROOT)/misc/wasm" >> "$GITHUB_PATH"
run: echo "$(go env GOROOT)/lib/wasm" >> "$GITHUB_PATH"
- name: Test wasmtime
env:

12
.gitignore vendored
View File

@@ -5,9 +5,6 @@
*.so
*.dylib
# Go workspace
go.work*
# Test binary, built with `go test -c`
*.test
@@ -16,4 +13,11 @@ go.work*
# Dependency directories (remove the comment below to include it)
# vendor/
tools
tools
# Go workspace file
go.work
go.work.sum
# env file
.env

View File

@@ -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)

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"sync/atomic"
"github.com/tetratelabs/wazero/api"
@@ -45,7 +46,16 @@ func (c *Conn) Config(op DBConfig, arg ...bool) (bool, error) {
rc := res_t(c.call("sqlite3_db_config", stk_t(c.handle),
stk_t(op), stk_t(argsPtr)))
return util.Read32[uint32](c.mod, argsPtr) != 0, c.error(rc)
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.
@@ -116,7 +126,7 @@ func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, erro
rc = res_t(c.call("sqlite3_file_control",
stk_t(c.handle), stk_t(schemaPtr),
stk_t(op), stk_t(ptr)))
ret = util.Read32[uint32](c.mod, ptr) != 0
ret = util.ReadBool(c.mod, ptr)
case FCNTL_CHUNK_SIZE:
util.Write32(c.mod, ptr, int32(arg[0].(int)))

26
conn.go
View File

@@ -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 {
@@ -492,9 +496,9 @@ func (c *Conn) TableColumnMetadata(schema, table, column string) (declType, coll
if ptr := util.Read32[ptr_t](c.mod, collSeqPtr); ptr != 0 {
collSeq = util.ReadString(c.mod, ptr, _MAX_NAME)
}
notNull = util.Read32[uint32](c.mod, notNullPtr) != 0
autoInc = util.Read32[uint32](c.mod, autoIncPtr) != 0
primaryKey = util.Read32[uint32](c.mod, primaryKeyPtr) != 0
notNull = util.ReadBool(c.mod, notNullPtr)
autoInc = util.ReadBool(c.mod, autoIncPtr)
primaryKey = util.ReadBool(c.mod, primaryKeyPtr)
}
return
}
@@ -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
}
}
}
}

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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
@@ -259,7 +258,10 @@ const (
DBCONFIG_TRUSTED_SCHEMA DBConfig = 1017
DBCONFIG_STMT_SCANSTATUS DBConfig = 1018
DBCONFIG_REVERSE_SCANORDER DBConfig = 1019
// DBCONFIG_MAX DBConfig = 1019
DBCONFIG_ENABLE_ATTACH_CREATE DBConfig = 1020
DBCONFIG_ENABLE_ATTACH_WRITE DBConfig = 1021
DBCONFIG_ENABLE_COMMENTS DBConfig = 1022
// DBCONFIG_MAX DBConfig = 1022
)
// FcntlOpcode are the available opcodes for [Conn.FileControl].

View File

@@ -1,6 +1,6 @@
# Embeddable Wasm build of SQLite
This folder includes an embeddable Wasm build of SQLite 3.49.0 for use with
This folder includes an embeddable Wasm build of SQLite 3.49.1 for use with
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
The following optional features are compiled in:

Binary file not shown.

View File

@@ -53,7 +53,7 @@ func Test_bcw2(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if version != "3.49.0" {
if version != "3.50.0" {
t.Error(version)
}
}

View File

@@ -13,10 +13,11 @@ mkdir -p build/ext/
cp "$ROOT"/sqlite3/*.[ch] build/
cp "$ROOT"/sqlite3/*.patch build/
# https://sqlite.org/src/info/cc3ce784b0feea2f
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=cc3ce784 | tar xz
# https://sqlite.org/src/info/c09656c62155a6e8
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

View File

@@ -1,14 +1,14 @@
module github.com/ncruces/go-sqlite3/embed/bcw2
go 1.22
go 1.23.0
toolchain go1.23.0
toolchain go1.24.0
require github.com/ncruces/go-sqlite3 v0.22.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.2 // indirect
github.com/tetratelabs/wazero v1.8.2 // indirect
github.com/ncruces/sort v0.1.5 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
golang.org/x/sys v0.30.0 // indirect
)

View File

@@ -1,12 +1,12 @@
github.com/ncruces/go-sqlite3 v0.22.0 h1:FkGSBhd0TY6e66k1LVhyEpA+RnG/8QkQNed5pjIk4cs=
github.com/ncruces/go-sqlite3 v0.22.0/go.mod h1:ueXOZXYZS2OFQirCU3mHneDwJm5fGKHrtccYBeGEV7M=
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.2 h1:zKQ9CA4fpHPF6xsUhRTfi5EEryspuBpe/QA4VWQOV1U=
github.com/ncruces/sort v0.1.2/go.mod h1:vEJUTBJtebIuCMmXD18GKo5GJGhsay+xZFOoBEIXFmE=
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/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.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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=

23
embed/bcw2/repro.patch Normal file
View 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.

View File

@@ -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

View File

@@ -19,7 +19,7 @@ func Test_init(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if version != "3.49.0" {
if version != "3.49.1" {
t.Error(version)
}
}

Binary file not shown.

View File

@@ -36,4 +36,13 @@ you can load into your database connections.
- [`github.com/ncruces/go-sqlite3/ext/uuid`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/uuid)
generates [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier).
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
maps multidimensional data to one dimension.
maps multidimensional data to one dimension.
### Pakages
These packages may also be useful to work with SQLite:
- [`github.com/ncruces/decimal`](https://pkg.go.dev/github.com/ncruces/decimal)
decimal arithmetic.
- [`github.com/ncruces/julianday`](https://pkg.go.dev/github.com/ncruces/julianday)
Julian day math.

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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++

View File

@@ -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{}
})
}

View File

@@ -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)
}
})
}

View File

@@ -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()

View File

@@ -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

View File

@@ -8,17 +8,21 @@ import (
"github.com/ncruces/go-sqlite3/vfs"
)
const vfsName = "github.com/ncruces/go-sqlite3/ext/serdes.sliceVFS"
func init() {
vfs.Register(vfsName, sliceVFS{})
}
var fileToOpen = make(chan *sliceFile, 1)
// Serialize backs up a database into a byte slice.
//
// https://sqlite.org/c3ref/serialize.html
func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) {
var file sliceFile
fileToOpen <- &file
err := db.Backup(schema, "file:db?vfs="+vfsName)
err := db.Backup(schema, "file:serdes.db?vfs="+vfsName)
return file.data, err
}
@@ -38,21 +42,21 @@ func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) {
// ["reader"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs
func Deserialize(db *sqlite3.Conn, schema string, data []byte) error {
fileToOpen <- &sliceFile{data}
return db.Restore(schema, "file:db?vfs="+vfsName)
return db.Restore(schema, "file:serdes.db?vfs="+vfsName)
}
var fileToOpen = make(chan *sliceFile, 1)
const vfsName = "github.com/ncruces/go-sqlite3/ext/deserialize.sliceVFS"
type sliceVFS struct{}
func (sliceVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
if flags&vfs.OPEN_MAIN_DB == 0 {
// notest // OPEN_MEMORY
if flags&vfs.OPEN_MAIN_DB == 0 || name != "serdes.db" {
return nil, flags, sqlite3.CANTOPEN
}
return <-fileToOpen, flags | vfs.OPEN_MEMORY, nil
select {
case file := <-fileToOpen:
return file, flags | vfs.OPEN_MEMORY, nil
default:
return nil, flags, sqlite3.MISUSE
}
}
func (sliceVFS) Delete(name string, dirSync bool) error {
@@ -61,7 +65,7 @@ func (sliceVFS) Delete(name string, dirSync bool) error {
}
func (sliceVFS) Access(name string, flag vfs.AccessFlag) (bool, error) {
return name == "db", nil
return name == "serdes.db", nil
}
func (sliceVFS) FullPathname(name string) (string, error) {

View File

@@ -1,6 +1,7 @@
package serdes_test
import (
"errors"
"io"
"net/http"
"testing"
@@ -66,3 +67,21 @@ func httpGet() ([]byte, error) {
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func TestOpen_errors(t *testing.T) {
_, err := sqlite3.Open("file:test.db?vfs=github.com/ncruces/go-sqlite3/ext/serdes.sliceVFS")
if err == nil {
t.Error("want error")
}
if !errors.Is(err, sqlite3.CANTOPEN) {
t.Errorf("got %v, want sqlite3.CANTOPEN", err)
}
_, err = sqlite3.Open("file:serdes.db?vfs=github.com/ncruces/go-sqlite3/ext/serdes.sliceVFS")
if err == nil {
t.Error("want error")
}
if !errors.Is(err, sqlite3.MISUSE) {
t.Errorf("got %v, want sqlite3.MISUSE", err)
}
}

View File

@@ -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()
}

View File

@@ -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} }
}

View File

@@ -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} }
}

View File

@@ -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} }
}

View File

@@ -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
View File

@@ -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
View 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
}

19
go.mod
View File

@@ -1,24 +1,27 @@
module github.com/ncruces/go-sqlite3
go 1.22
go 1.23.0
toolchain go1.23.0
toolchain go1.24.0
require (
github.com/ncruces/julianday v1.0.0
github.com/ncruces/sort v0.1.5
github.com/tetratelabs/wazero v1.8.2
golang.org/x/crypto v0.33.0
golang.org/x/sys v0.30.0
github.com/tetratelabs/wazero v1.9.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
)

20
go.sum
View File

@@ -8,15 +8,15 @@ 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/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
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=
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=
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.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=

View File

@@ -1,11 +1,11 @@
module github.com/ncruces/go-sqlite3/gormlite
go 1.22
go 1.23.0
toolchain go1.23.0
toolchain go1.24.0
require (
github.com/ncruces/go-sqlite3 v0.22.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
)

View File

@@ -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.22.0 h1:FkGSBhd0TY6e66k1LVhyEpA+RnG/8QkQNed5pjIk4cs=
github.com/ncruces/go-sqlite3 v0.22.0/go.mod h1:ueXOZXYZS2OFQirCU3mHneDwJm5fGKHrtccYBeGEV7M=
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=

View File

@@ -7,18 +7,12 @@ import (
"github.com/tetratelabs/wazero"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
// notest
func init() {
if util.CompilerSupported() {
sqlite3.RuntimeConfig = wazero.NewRuntimeConfigCompiler()
} else {
sqlite3.RuntimeConfig = wazero.NewRuntimeConfigInterpreter()
}
sqlite3.RuntimeConfig = sqlite3.RuntimeConfig.WithMemoryLimitPages(512)
sqlite3.RuntimeConfig = wazero.NewRuntimeConfig().WithMemoryLimitPages(512)
if os.Getenv("CI") != "" {
path := filepath.Join(os.TempDir(), "wazero")
if err := os.MkdirAll(path, 0777); err == nil {

View File

@@ -1,27 +0,0 @@
package util
import (
"runtime"
"golang.org/x/sys/cpu"
)
func CompilerSupported() bool {
switch runtime.GOOS {
case "linux", "android",
"windows", "darwin",
"freebsd", "netbsd", "dragonfly",
"solaris", "illumos":
break
default:
return false
}
switch runtime.GOARCH {
case "amd64":
return cpu.X86.HasSSE41
case "arm64":
return true
default:
return false
}
}

View File

@@ -26,9 +26,6 @@ func View(mod api.Module, ptr Ptr_t, size int64) []byte {
if ptr == 0 {
panic(NilErr)
}
if size == 0 {
return nil
}
if uint64(size) > math.MaxUint32 {
panic(RangeErr)
}
@@ -110,6 +107,18 @@ func WriteFloat64(mod api.Module, ptr Ptr_t, v float64) {
Write64(mod, ptr, math.Float64bits(v))
}
func ReadBool(mod api.Module, ptr Ptr_t) bool {
return Read32[int32](mod, ptr) != 0
}
func WriteBool(mod api.Module, ptr Ptr_t, v bool) {
var i int32
if v {
i = 1
}
Write32(mod, ptr, i)
}
func ReadString(mod api.Module, ptr Ptr_t, maxlen int64) string {
if ptr == 0 {
panic(NilErr)

View File

@@ -47,11 +47,7 @@ func compileSQLite() {
ctx := context.Background()
cfg := RuntimeConfig
if cfg == nil {
if util.CompilerSupported() {
cfg = wazero.NewRuntimeConfigCompiler()
} else {
cfg = wazero.NewRuntimeConfigInterpreter()
}
cfg = wazero.NewRuntimeConfig()
if bits.UintSize < 64 {
cfg = cfg.WithMemoryLimitPages(512) // 32MB
} else {
@@ -321,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)

View File

@@ -2,7 +2,7 @@
# handle, and interrupt, sqlite3_busy_timeout.
--- sqlite3.c.orig
+++ sqlite3.c
@@ -183054,7 +183054,7 @@
@@ -183355,7 +183355,7 @@
if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;
#endif
if( ms>0 ){

View File

@@ -3,7 +3,7 @@ set -euo pipefail
cd -P -- "$(dirname -- "$0")"
curl -#OL "https://sqlite.org/2025/sqlite-amalgamation-3490000.zip"
curl -#OL "https://sqlite.org/2025/sqlite-amalgamation-3490100.zip"
unzip -d . sqlite-amalgamation-*.zip
mv sqlite-amalgamation-*/sqlite3.c .
mv sqlite-amalgamation-*/sqlite3.h .
@@ -19,30 +19,30 @@ rm -rf sqlite-amalgamation-*
mkdir -p ext/
cd ext/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/ext/misc/anycollseq.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/ext/misc/base64.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/ext/misc/decimal.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/ext/misc/ieee754.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/ext/misc/regexp.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/ext/misc/series.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/ext/misc/spellfix.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/ext/misc/uint.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/ext/misc/anycollseq.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/ext/misc/base64.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/ext/misc/decimal.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/ext/misc/ieee754.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/ext/misc/regexp.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/ext/misc/series.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/ext/misc/spellfix.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/ext/misc/uint.c"
cd ~-
cd ../vfs/tests/mptest/testdata/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/mptest/config01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/mptest/config02.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/mptest/crash01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/mptest/crash02.subtest"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/mptest/multiwrite01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/mptest/config01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/mptest/config02.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/mptest/crash01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/mptest/crash02.subtest"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/mptest/multiwrite01.test"
cd ~-
cd ../vfs/tests/mptest/wasm/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/mptest/mptest.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/mptest/mptest.c"
cd ~-
cd ../vfs/tests/speedtest1/wasm/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.0/test/speedtest1.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.49.1/test/speedtest1.c"
cd ~-
cat *.patch | patch -p0 --no-backup-if-mismatch

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -1,7 +1,7 @@
# Remove VFS registration. Go handles it.
--- sqlite3.c.orig
+++ sqlite3.c
@@ -26623,7 +26623,7 @@
@@ -26725,7 +26725,7 @@
sqlite3_free(p);
return sqlite3_os_init();
}
@@ -10,7 +10,7 @@
/*
** The list of all registered VFS implementations.
*/
@@ -26720,7 +26720,7 @@
@@ -26822,7 +26822,7 @@
sqlite3_mutex_leave(mutex);
return SQLITE_OK;
}

View File

@@ -130,10 +130,6 @@ func Test_sqlite_newBytes(t *testing.T) {
if ptr == 0 {
t.Fatal("got nullptr, want a pointer")
}
if got := util.View(sqlite.mod, ptr, 0); got != nil {
t.Errorf("got %q, want nil", got)
}
}
func Test_sqlite_newString(t *testing.T) {

View File

@@ -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()

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
)

View File

@@ -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 {

View File

@@ -47,14 +47,14 @@ func Test_vfsLock(t *testing.T) {
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got != LOCK_NONE {
if got := util.ReadBool(mod, pOutput); got {
t.Error("file was locked")
}
rc = vfsCheckReservedLock(ctx, mod, pFile2, pOutput)
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got != LOCK_NONE {
if got := util.ReadBool(mod, pOutput); got {
t.Error("file was locked")
}
rc = vfsFileControl(ctx, mod, pFile2, _FCNTL_LOCKSTATE, pOutput)
@@ -74,14 +74,14 @@ func Test_vfsLock(t *testing.T) {
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got != LOCK_NONE {
if got := util.ReadBool(mod, pOutput); got {
t.Error("file was locked")
}
rc = vfsCheckReservedLock(ctx, mod, pFile2, pOutput)
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got != LOCK_NONE {
if got := util.ReadBool(mod, pOutput); got {
t.Error("file was locked")
}
rc = vfsFileControl(ctx, mod, pFile2, _FCNTL_LOCKSTATE, pOutput)
@@ -105,14 +105,14 @@ func Test_vfsLock(t *testing.T) {
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got == LOCK_NONE {
if got := util.ReadBool(mod, pOutput); !got {
t.Log("file wasn't locked, locking is incompatible with SQLite")
}
rc = vfsCheckReservedLock(ctx, mod, pFile2, pOutput)
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got == LOCK_NONE {
if got := util.ReadBool(mod, pOutput); !got {
t.Error("file wasn't locked")
}
rc = vfsFileControl(ctx, mod, pFile2, _FCNTL_LOCKSTATE, pOutput)
@@ -132,14 +132,14 @@ func Test_vfsLock(t *testing.T) {
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got == LOCK_NONE {
if got := util.ReadBool(mod, pOutput); !got {
t.Log("file wasn't locked, locking is incompatible with SQLite")
}
rc = vfsCheckReservedLock(ctx, mod, pFile2, pOutput)
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got == LOCK_NONE {
if got := util.ReadBool(mod, pOutput); !got {
t.Error("file wasn't locked")
}
rc = vfsFileControl(ctx, mod, pFile2, _FCNTL_LOCKSTATE, pOutput)
@@ -159,14 +159,14 @@ func Test_vfsLock(t *testing.T) {
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got == LOCK_NONE {
if got := util.ReadBool(mod, pOutput); !got {
t.Log("file wasn't locked, locking is incompatible with SQLite")
}
rc = vfsCheckReservedLock(ctx, mod, pFile2, pOutput)
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got == LOCK_NONE {
if got := util.ReadBool(mod, pOutput); !got {
t.Error("file wasn't locked")
}
rc = vfsFileControl(ctx, mod, pFile1, _FCNTL_LOCKSTATE, pOutput)
@@ -186,14 +186,14 @@ func Test_vfsLock(t *testing.T) {
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got != LOCK_NONE {
if got := util.ReadBool(mod, pOutput); got {
t.Error("file was locked")
}
rc = vfsCheckReservedLock(ctx, mod, pFile2, pOutput)
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[LockLevel](mod, pOutput); got != LOCK_NONE {
if got := util.ReadBool(mod, pOutput); got {
t.Error("file was locked")
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
View 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)
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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

View File

@@ -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"))

Binary file not shown.

View File

@@ -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"))

View File

@@ -58,13 +58,8 @@ func vfsFind(ctx context.Context, mod api.Module, zVfsName ptr_t) uint32 {
}
func vfsLocaltime(ctx context.Context, mod api.Module, pTm ptr_t, t int64) _ErrorCode {
tm := time.Unix(t, 0)
var isdst int32
if tm.IsDST() {
isdst = 1
}
const size = 32 / 8
tm := time.Unix(t, 0)
// https://pubs.opengroup.org/onlinepubs/7908799/xsh/time.h.html
util.Write32(mod, pTm+0*size, int32(tm.Second()))
util.Write32(mod, pTm+1*size, int32(tm.Minute()))
@@ -74,7 +69,7 @@ func vfsLocaltime(ctx context.Context, mod api.Module, pTm ptr_t, t int64) _Erro
util.Write32(mod, pTm+5*size, int32(tm.Year()-1900))
util.Write32(mod, pTm+6*size, int32(tm.Weekday()-time.Sunday))
util.Write32(mod, pTm+7*size, int32(tm.YearDay()-1))
util.Write32(mod, pTm+8*size, isdst)
util.WriteBool(mod, pTm+8*size, tm.IsDST())
return _OK
}
@@ -123,11 +118,7 @@ func vfsAccess(ctx context.Context, mod api.Module, pVfs, zPath ptr_t, flags Acc
path := util.ReadString(mod, zPath, _MAX_PATHNAME)
ok, err := vfs.Access(path, flags)
var res int32
if ok {
res = 1
}
util.Write32(mod, pResOut, res)
util.WriteBool(mod, pResOut, ok)
return vfsErrorCode(err, _IOERR_ACCESS)
}
@@ -151,9 +142,8 @@ func vfsOpen(ctx context.Context, mod api.Module, pVfs, zPath, pFile ptr_t, flag
file.SetPowersafeOverwrite(b)
}
}
if file, ok := file.(FileSharedMemory); ok &&
pOutVFS != 0 && file.SharedMemory() != nil {
util.Write32(mod, pOutVFS, int32(1))
if file, ok := file.(FileSharedMemory); ok && pOutVFS != 0 {
util.WriteBool(mod, pOutVFS, file.SharedMemory() != nil)
}
if pOutFlags != 0 {
util.Write32(mod, pOutFlags, flags)
@@ -225,12 +215,7 @@ func vfsUnlock(ctx context.Context, mod api.Module, pFile ptr_t, eLock LockLevel
func vfsCheckReservedLock(ctx context.Context, mod api.Module, pFile, pResOut ptr_t) _ErrorCode {
file := vfsFileGet(ctx, mod, pFile).(File)
locked, err := file.CheckReservedLock()
var res int32
if locked {
res = 1
}
util.Write32(mod, pResOut, res)
util.WriteBool(mod, pResOut, locked)
return vfsErrorCode(err, _IOERR_CHECKRESERVEDLOCK)
}
@@ -254,24 +239,20 @@ func vfsFileControlImpl(ctx context.Context, mod api.Module, file File, op _Fcnt
case _FCNTL_PERSIST_WAL:
if file, ok := file.(FilePersistWAL); ok {
if i := util.Read32[int32](mod, pArg); i >= 0 {
file.SetPersistWAL(i != 0)
} else if file.PersistWAL() {
util.Write32(mod, pArg, int32(1))
if i := util.Read32[int32](mod, pArg); i < 0 {
util.WriteBool(mod, pArg, file.PersistWAL())
} else {
util.Write32(mod, pArg, int32(0))
file.SetPersistWAL(i != 0)
}
return _OK
}
case _FCNTL_POWERSAFE_OVERWRITE:
if file, ok := file.(FilePowersafeOverwrite); ok {
if i := util.Read32[int32](mod, pArg); i >= 0 {
file.SetPowersafeOverwrite(i != 0)
} else if file.PowersafeOverwrite() {
util.Write32(mod, pArg, int32(1))
if i := util.Read32[int32](mod, pArg); i < 0 {
util.WriteBool(mod, pArg, file.PowersafeOverwrite())
} else {
util.Write32(mod, pArg, int32(0))
file.SetPowersafeOverwrite(i != 0)
}
return _OK
}
@@ -293,11 +274,7 @@ func vfsFileControlImpl(ctx context.Context, mod api.Module, file File, op _Fcnt
case _FCNTL_HAS_MOVED:
if file, ok := file.(FileHasMoved); ok {
moved, err := file.HasMoved()
var val uint32
if moved {
val = 1
}
util.Write32(mod, pArg, val)
util.WriteBool(mod, pArg, moved)
return vfsErrorCode(err, _IOERR_FSTAT)
}
@@ -394,7 +371,7 @@ func vfsFileControlImpl(ctx context.Context, mod api.Module, file File, op _Fcnt
case _FCNTL_LOCK_TIMEOUT:
if file, ok := file.(FileSharedMemory); ok {
if shm, ok := file.SharedMemory().(blockingSharedMemory); ok {
shm.shmEnableBlocking(util.Read32[uint32](mod, pArg) != 0)
shm.shmEnableBlocking(util.ReadBool(mod, pArg))
return _OK
}
}

View File

@@ -172,7 +172,7 @@ func Test_vfsAccess(t *testing.T) {
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[int32](mod, 4); got != 1 {
if got := util.ReadBool(mod, 4); !got {
t.Error("directory did not exist")
}
@@ -180,7 +180,7 @@ func Test_vfsAccess(t *testing.T) {
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[int32](mod, 4); got != 1 {
if got := util.ReadBool(mod, 4); !got {
t.Error("can't access directory")
}
@@ -189,7 +189,7 @@ func Test_vfsAccess(t *testing.T) {
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[int32](mod, 4); got != 1 {
if got := util.ReadBool(mod, 4); !got {
t.Error("can't access file")
}
@@ -207,7 +207,7 @@ func Test_vfsAccess(t *testing.T) {
if rc != _OK {
t.Fatal("returned", rc)
}
if got := util.Read32[int32](mod, 4); got != 0 {
if got := util.ReadBool(mod, 4); got {
t.Error("can access file")
}
}

26
vtab.go
View File

@@ -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 {
@@ -399,10 +401,10 @@ func (idx *IndexInfo) save() {
util.Write32(mod, ptr+20, int32(idx.IdxNum))
if idx.IdxStr != "" {
util.Write32(mod, ptr+24, idx.c.newString(idx.IdxStr))
util.Write32(mod, ptr+28, int32(1)) // needToFreeIdxStr
util.WriteBool(mod, ptr+28, true) // needToFreeIdxStr
}
if idx.OrderByConsumed {
util.Write32(mod, ptr+32, int32(1))
util.WriteBool(mod, ptr+32, true)
}
util.WriteFloat64(mod, ptr+40, idx.EstimatedCost)
util.Write64(mod, ptr+48, idx.EstimatedRows)
@@ -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)
}