Compare commits

...

28 Commits

Author SHA1 Message Date
Nuno Cruces
831a34a4c4 Updated dependencies. 2023-12-07 14:00:08 +00:00
Nuno Cruces
7c820ede3c Driver time formatting. 2023-12-07 13:49:33 +00:00
Nuno Cruces
089a0c0670 Pivot virtual table. 2023-12-06 17:49:48 +00:00
Nuno Cruces
8b45cac16b Improved error handling. 2023-12-05 18:17:33 +00:00
Nuno Cruces
06d2ff6752 Optimize VFS find. 2023-12-05 14:11:20 +00:00
Nuno Cruces
987f0f13a2 Test CPUs. 2023-12-04 14:01:25 +00:00
Nuno Cruces
cd40213898 Reuse statement, API. 2023-12-04 13:46:48 +00:00
Nuno Cruces
8a0baedc10 Tests, fixes. 2023-12-02 12:17:18 +00:00
Nuno Cruces
c667a1f469 Declared type. 2023-12-02 12:17:18 +00:00
Nuno Cruces
9c562f5d8b Cache functions. 2023-12-02 12:17:18 +00:00
Nuno Cruces
d862f47d95 Deoptimize. 2023-12-02 12:17:18 +00:00
Nuno Cruces
a9e32fd3f0 Fix compiler crash. 2023-12-02 12:17:18 +00:00
Nuno Cruces
b262f5cd01 Statement virtual table. 2023-12-02 12:17:18 +00:00
Nuno Cruces
4160b9a4bb Simplify tails. 2023-11-30 18:18:27 +00:00
Nuno Cruces
dbaf2d99cd Unprotected values. 2023-11-30 00:29:41 +00:00
Nuno Cruces
3f05115cd7 Virtual table API. 2023-11-29 10:46:11 +00:00
Nuno Cruces
9bf14becaf Reentrant arenas. 2023-11-29 10:46:02 +00:00
Nuno Cruces
997e197f54 VFS tweaks. 2023-11-28 16:38:02 +00:00
Nuno Cruces
b81fe284b6 memdb WAL. 2023-11-28 11:40:04 +00:00
dependabot[bot]
269306c5c8 Bump golang.org/x/sys from 0.14.0 to 0.15.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.14.0 to 0.15.0.
- [Commits](https://github.com/golang/sys/compare/v0.14.0...v0.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-28 02:00:06 +00:00
Nuno Cruces
8a18243830 Reproducible builds. 2023-11-28 01:55:26 +00:00
Nuno Cruces
fcd6cc91d8 Skip BOM. 2023-11-27 23:35:43 +00:00
Nuno Cruces
c1838fc0bc Fix encoding issues. 2023-11-27 15:37:53 +00:00
Nuno Cruces
dc0d8236bf Updated dependencies. 2023-11-25 00:39:20 +00:00
Nuno Cruces
ba18facb0d SQLite 3.44.2. 2023-11-25 00:10:38 +00:00
Nuno Cruces
5653efa70e Limits. 2023-11-25 00:10:32 +00:00
Nuno Cruces
1acb95917a Simplify aggregate context. 2023-11-24 15:33:17 +00:00
Nuno Cruces
e31a42fb22 Lines virtual table. 2023-11-23 15:59:24 +00:00
78 changed files with 2190 additions and 754 deletions

40
.github/workflows/cpu.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: CPUs
on:
workflow_dispatch:
jobs:
test-386:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up
uses: actions/setup-go@v4
with:
go-version: stable
- name: Test
run: GOARCH=386 go test -v ./...
test-arm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up
uses: actions/setup-go@v4
with:
go-version: stable
- name: Install QEMU
uses: docker/setup-qemu-action@v3
- name: Test
run: GOARCH=arm64 go test -v ./...

View File

@@ -19,4 +19,4 @@ echo darwin-flock ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_flock .
echo darwin-nosys ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_nosys .
echo linux-nosys ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_nosys .
echo windows-nosys ; GOOS=windows GOARCH=amd64 go build -tags sqlite3_nosys .
echo freebsd-nosys ; GOOS=freebsd GOARCH=amd64 go build -tags sqlite3_nosys .
echo freebsd-nosys ; GOOS=freebsd GOARCH=amd64 go build -tags sqlite3_nosys .

View File

@@ -4,13 +4,11 @@ on:
workflow_dispatch:
jobs:
test:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up
uses: actions/setup-go@v4

23
.github/workflows/repro.sh vendored Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "$OSTYPE" == "linux"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/wasi-sdk-20.0-linux.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-linux.tar.gz"
elif [[ "$OSTYPE" == "darwin"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/wasi-sdk-20.0-macos.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-macos.tar.gz"
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/wasi-sdk-20.0.m-mingw.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-windows.tar.gz"
fi
# Download tools
mkdir -p tools
[ -d "tools/wasi-sdk"* ] || curl -#L "$WASI_SDK" | tar xzC tools &
[ -d "tools/binaryen-version"* ] || curl -#L "$BINARYEN" | tar xzC tools &
wait
sqlite3/download.sh # Download SQLite
embed/build.sh # Build WASM
git diff --exit-code # Check diffs

24
.github/workflows/repro.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Reproducible build
on:
workflow_dispatch:
jobs:
build:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up
uses: actions/setup-go@v4
with:
go-version: stable
- name: Build
run: .github/workflows/repro.sh

View File

@@ -28,6 +28,12 @@ and uses [wazero](https://wazero.io/) to provide `cgo`-free SQLite bindings.
simplifies [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html).
- [`github.com/ncruces/go-sqlite3/ext/csv`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/csv)
reads [comma-separated values](https://sqlite.org/csv.html).
- [`github.com/ncruces/go-sqlite3/ext/lines`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/lines)
reads files [line-by-line](https://github.com/asg017/sqlite-lines).
- [`github.com/ncruces/go-sqlite3/ext/pivot`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/pivot)
creates [pivot tables](https://github.com/jakethaw/pivot_vtab).
- [`github.com/ncruces/go-sqlite3/ext/statement`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/statement)
creates [table-valued functions with SQL](https://github.com/0x09/sqlite-statement-vtab).
- [`github.com/ncruces/go-sqlite3/ext/stats`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
provides [statistics functions](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html).
- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
@@ -45,7 +51,7 @@ and uses [wazero](https://wazero.io/) to provide `cgo`-free SQLite bindings.
- [x] [virtual tables](https://sqlite.org/vtab.html)
- [x] [custom VFSes](https://sqlite.org/vfs.html)
- [x] [online backup](https://sqlite.org/backup.html)
- [x] [JSON support](https://www.sqlite.org/json1.html)
- [x] [JSON support](https://sqlite.org/json1.html)
- [x] [Unicode support](https://sqlite.org/src/dir/ext/icu)
### Caveats

View File

@@ -62,7 +62,7 @@ func (src *Conn) BackupInit(srcDB, dstURI string) (*Backup, error) {
}
func (c *Conn) backupInit(dst uint32, dstName string, src uint32, srcName string) (*Backup, error) {
defer c.arena.reset()
defer c.arena.mark()()
dstPtr := c.arena.string(dstName)
srcPtr := c.arena.string(srcName)
@@ -71,12 +71,12 @@ func (c *Conn) backupInit(dst uint32, dstName string, src uint32, srcName string
other = src
}
r := c.call(c.api.backupInit,
r := c.call("sqlite3_backup_init",
uint64(dst), uint64(dstPtr),
uint64(src), uint64(srcPtr))
if r == 0 {
defer c.closeDB(other)
r = c.call(c.api.errcode, uint64(dst))
r = c.call("sqlite3_errcode", uint64(dst))
return nil, c.sqlite.error(r, dst)
}
@@ -97,7 +97,7 @@ func (b *Backup) Close() error {
return nil
}
r := b.c.call(b.c.api.backupFinish, uint64(b.handle))
r := b.c.call("sqlite3_backup_finish", uint64(b.handle))
b.c.closeDB(b.otherc)
b.handle = 0
return b.c.error(r)
@@ -108,7 +108,7 @@ func (b *Backup) Close() error {
//
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
func (b *Backup) Step(nPage int) (done bool, err error) {
r := b.c.call(b.c.api.backupStep, uint64(b.handle), uint64(nPage))
r := b.c.call("sqlite3_backup_step", uint64(b.handle), uint64(nPage))
if r == _DONE {
return true, nil
}
@@ -120,7 +120,7 @@ func (b *Backup) Step(nPage int) (done bool, err error) {
//
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backupremaining
func (b *Backup) Remaining() int {
r := b.c.call(b.c.api.backupRemaining, uint64(b.handle))
r := b.c.call("sqlite3_backup_remaining", uint64(b.handle))
return int(r)
}
@@ -129,6 +129,6 @@ func (b *Backup) Remaining() int {
//
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backuppagecount
func (b *Backup) PageCount() int {
r := b.c.call(b.c.api.backupPageCount, uint64(b.handle))
r := b.c.call("sqlite3_backup_pagecount", uint64(b.handle))
return int(r)
}

36
blob.go
View File

@@ -30,7 +30,7 @@ var _ io.ReadWriteSeeker = &Blob{}
// https://sqlite.org/c3ref/blob_open.html
func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob, error) {
c.checkInterrupt()
defer c.arena.reset()
defer c.arena.mark()()
blobPtr := c.arena.new(ptrlen)
dbPtr := c.arena.string(db)
tablePtr := c.arena.string(table)
@@ -41,7 +41,7 @@ func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob,
flags = 1
}
r := c.call(c.api.blobOpen, uint64(c.handle),
r := c.call("sqlite3_blob_open", uint64(c.handle),
uint64(dbPtr), uint64(tablePtr), uint64(columnPtr),
uint64(row), flags, uint64(blobPtr))
@@ -51,7 +51,7 @@ func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob,
blob := Blob{c: c}
blob.handle = util.ReadUint32(c.mod, blobPtr)
blob.bytes = int64(c.call(c.api.blobBytes, uint64(blob.handle)))
blob.bytes = int64(c.call("sqlite3_blob_bytes", uint64(blob.handle)))
return &blob, nil
}
@@ -65,7 +65,7 @@ func (b *Blob) Close() error {
return nil
}
r := b.c.call(b.c.api.blobClose, uint64(b.handle))
r := b.c.call("sqlite3_blob_close", uint64(b.handle))
b.handle = 0
return b.c.error(r)
@@ -92,10 +92,10 @@ func (b *Blob) Read(p []byte) (n int, err error) {
want = avail
}
ptr := b.c.new(uint64(want))
defer b.c.free(ptr)
defer b.c.arena.mark()()
ptr := b.c.arena.new(uint64(want))
r := b.c.call(b.c.api.blobRead, uint64(b.handle),
r := b.c.call("sqlite3_blob_read", uint64(b.handle),
uint64(ptr), uint64(want), uint64(b.offset))
err = b.c.error(r)
if err != nil {
@@ -124,11 +124,11 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
want = avail
}
ptr := b.c.new(uint64(want))
defer b.c.free(ptr)
defer b.c.arena.mark()()
ptr := b.c.arena.new(uint64(want))
for want > 0 {
r := b.c.call(b.c.api.blobRead, uint64(b.handle),
r := b.c.call("sqlite3_blob_read", uint64(b.handle),
uint64(ptr), uint64(want), uint64(b.offset))
err = b.c.error(r)
if err != nil {
@@ -158,10 +158,10 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
//
// https://sqlite.org/c3ref/blob_write.html
func (b *Blob) Write(p []byte) (n int, err error) {
ptr := b.c.newBytes(p)
defer b.c.free(ptr)
defer b.c.arena.mark()()
ptr := b.c.arena.bytes(p)
r := b.c.call(b.c.api.blobWrite, uint64(b.handle),
r := b.c.call("sqlite3_blob_write", uint64(b.handle),
uint64(ptr), uint64(len(p)), uint64(b.offset))
err = b.c.error(r)
if err != nil {
@@ -187,14 +187,14 @@ func (b *Blob) ReadFrom(r io.Reader) (n int64, err error) {
want = 1
}
ptr := b.c.new(uint64(want))
defer b.c.free(ptr)
defer b.c.arena.mark()()
ptr := b.c.arena.new(uint64(want))
for {
mem := util.View(b.c.mod, ptr, uint64(want))
m, err := r.Read(mem[:want])
if m > 0 {
r := b.c.call(b.c.api.blobWrite, uint64(b.handle),
r := b.c.call("sqlite3_blob_write", uint64(b.handle),
uint64(ptr), uint64(m), uint64(b.offset))
err := b.c.error(r)
if err != nil {
@@ -243,8 +243,8 @@ func (b *Blob) Seek(offset int64, whence int) (int64, error) {
//
// https://sqlite.org/c3ref/blob_reopen.html
func (b *Blob) Reopen(row int64) error {
err := b.c.error(b.c.call(b.c.api.blobReopen, uint64(b.handle), uint64(row)))
b.bytes = int64(b.c.call(b.c.api.blobBytes, uint64(b.handle)))
err := b.c.error(b.c.call("sqlite3_blob_reopen", uint64(b.handle), uint64(row)))
b.bytes = int64(b.c.call("sqlite3_blob_bytes", uint64(b.handle)))
b.offset = 0
return err
}

50
conn.go
View File

@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/url"
"runtime"
"strings"
"github.com/ncruces/go-sqlite3/internal/util"
@@ -56,8 +55,6 @@ func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
defer func() {
if conn == nil {
sqlite.close()
} else {
runtime.SetFinalizer(conn, util.Finalizer[Conn](3))
}
}()
@@ -72,12 +69,12 @@ func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
}
func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
defer c.arena.reset()
defer c.arena.mark()()
connPtr := c.arena.new(ptrlen)
namePtr := c.arena.string(filename)
flags |= OPEN_EXRESCODE
r := c.call(c.api.open, uint64(namePtr), uint64(connPtr), uint64(flags), 0)
r := c.call("sqlite3_open_v2", uint64(namePtr), uint64(connPtr), uint64(flags), 0)
handle := util.ReadUint32(c.mod, connPtr)
if err := c.sqlite.error(r, handle); err != nil {
@@ -92,13 +89,12 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
for _, p := range query["_pragma"] {
pragmas.WriteString(`PRAGMA `)
pragmas.WriteString(p)
pragmas.WriteByte(';')
pragmas.WriteString(`;`)
}
}
c.arena.reset()
pragmaPtr := c.arena.string(pragmas.String())
r := c.call(c.api.exec, uint64(handle), uint64(pragmaPtr), 0, 0, 0)
r := c.call("sqlite3_exec", uint64(handle), uint64(pragmaPtr), 0, 0, 0)
if err := c.sqlite.error(r, handle, pragmas.String()); err != nil {
if errors.Is(err, ERROR) {
err = fmt.Errorf("sqlite3: invalid _pragma: %w", err)
@@ -112,7 +108,7 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
}
func (c *Conn) closeDB(handle uint32) {
r := c.call(c.api.closeZombie, uint64(handle))
r := c.call("sqlite3_close_v2", uint64(handle))
if err := c.sqlite.error(r, handle); err != nil {
panic(err)
}
@@ -135,13 +131,12 @@ func (c *Conn) Close() error {
c.pending.Close()
c.pending = nil
r := c.call(c.api.close, uint64(c.handle))
r := c.call("sqlite3_close", uint64(c.handle))
if err := c.error(r); err != nil {
return err
}
c.handle = 0
runtime.SetFinalizer(c, nil)
return c.close()
}
@@ -151,11 +146,11 @@ func (c *Conn) Close() error {
// https://sqlite.org/c3ref/exec.html
func (c *Conn) Exec(sql string) error {
c.checkInterrupt()
defer c.arena.reset()
defer c.arena.mark()()
sqlPtr := c.arena.string(sql)
r := c.call(c.api.exec, uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
return c.error(r)
r := c.call("sqlite3_exec", uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
return c.error(r, sql)
}
// Prepare calls [Conn.PrepareFlags] with no flags.
@@ -170,23 +165,24 @@ func (c *Conn) Prepare(sql string) (stmt *Stmt, tail string, err error) {
//
// https://sqlite.org/c3ref/prepare.html
func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail string, err error) {
if emptyStatement(sql) {
return nil, "", nil
if len(sql) > _MAX_LENGTH {
return nil, "", TOOBIG
}
defer c.arena.reset()
defer c.arena.mark()()
stmtPtr := c.arena.new(ptrlen)
tailPtr := c.arena.new(ptrlen)
sqlPtr := c.arena.string(sql)
r := c.call(c.api.prepare, uint64(c.handle),
r := c.call("sqlite3_prepare_v3", uint64(c.handle),
uint64(sqlPtr), uint64(len(sql)+1), uint64(flags),
uint64(stmtPtr), uint64(tailPtr))
stmt = &Stmt{c: c}
stmt.handle = util.ReadUint32(c.mod, stmtPtr)
i := util.ReadUint32(c.mod, tailPtr)
tail = sql[i-sqlPtr:]
if sql := sql[util.ReadUint32(c.mod, tailPtr)-sqlPtr:]; sql != "" {
tail = sql
}
if err := c.error(r, sql); err != nil {
return nil, "", err
@@ -194,14 +190,14 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
if stmt.handle == 0 {
return nil, "", nil
}
return
return stmt, tail, nil
}
// GetAutocommit tests the connection for auto-commit mode.
//
// https://sqlite.org/c3ref/get_autocommit.html
func (c *Conn) GetAutocommit() bool {
r := c.call(c.api.autocommit, uint64(c.handle))
r := c.call("sqlite3_get_autocommit", uint64(c.handle))
return r != 0
}
@@ -210,7 +206,7 @@ func (c *Conn) GetAutocommit() bool {
//
// https://sqlite.org/c3ref/last_insert_rowid.html
func (c *Conn) LastInsertRowID() int64 {
r := c.call(c.api.lastRowid, uint64(c.handle))
r := c.call("sqlite3_last_insert_rowid", uint64(c.handle))
return int64(r)
}
@@ -220,7 +216,7 @@ func (c *Conn) LastInsertRowID() int64 {
//
// https://sqlite.org/c3ref/changes.html
func (c *Conn) Changes() int64 {
r := c.call(c.api.changes, uint64(c.handle))
r := c.call("sqlite3_changes64", uint64(c.handle))
return int64(r)
}
@@ -256,12 +252,12 @@ func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
c.interrupt = ctx
// Remove the handler if the context can't be canceled.
if ctx == nil || ctx.Done() == nil {
c.call(c.api.progressHandler, uint64(c.handle), 0)
c.call("sqlite3_progress_handler_go", uint64(c.handle), 0)
return old
}
c.pending.Step()
c.call(c.api.progressHandler, uint64(c.handle), 100)
c.call("sqlite3_progress_handler_go", uint64(c.handle), 100)
return old
}
@@ -276,7 +272,7 @@ func progressCallback(ctx context.Context, mod api.Module, _ uint32) uint32 {
func (c *Conn) checkInterrupt() {
if c.interrupt != nil && c.interrupt.Err() != nil {
c.call(c.api.interrupt, uint64(c.handle))
c.call("sqlite3_interrupt", uint64(c.handle))
}
}

View File

@@ -9,8 +9,9 @@ const (
_UTF8 = 1
_MAX_STRING = 512 // Used for short strings: names, error messages…
_MAX_NAME = 512 // Used for short strings: names, error messages…
_MAX_LENGTH = 1e9
_MAX_SQL_LENGTH = 1e9
_MAX_ALLOCATION_SIZE = 0x7ffffeff
ptrlen = 4
@@ -168,7 +169,8 @@ const (
PREPARE_NO_VTAB PrepareFlag = 0x04
)
// FunctionFlag is a flag that can be passed to [Conn.PrepareFlags].
// FunctionFlag is a flag that can be passed to
// [Conn.CreateFunction] and [Conn.CreateWindowFunction].
//
// https://sqlite.org/c3ref/c_deterministic.html
type FunctionFlag uint32
@@ -180,6 +182,23 @@ const (
INNOCUOUS FunctionFlag = 0x000200000
)
// StmtStatus name counter values associated with the [Stmt.Status] method.
//
// https://sqlite.org/c3ref/c_stmtstatus_counter.html
type StmtStatus uint32
const (
STMTSTATUS_FULLSCAN_STEP StmtStatus = 1
STMTSTATUS_SORT StmtStatus = 2
STMTSTATUS_AUTOINDEX StmtStatus = 3
STMTSTATUS_VM_STEP StmtStatus = 4
STMTSTATUS_REPREPARE StmtStatus = 5
STMTSTATUS_RUN StmtStatus = 6
STMTSTATUS_FILTER_MISS StmtStatus = 7
STMTSTATUS_FILTER_HIT StmtStatus = 8
STMTSTATUS_MEMUSED StmtStatus = 99
)
// Datatype is a fundamental datatype of SQLite.
//
// https://sqlite.org/c3ref/c_blob.html

View File

@@ -32,14 +32,14 @@ func (ctx Context) Conn() *Conn {
// https://sqlite.org/c3ref/get_auxdata.html
func (ctx Context) SetAuxData(n int, data any) {
ptr := util.AddHandle(ctx.c.ctx, data)
ctx.c.call(ctx.c.api.setAuxData, uint64(ctx.handle), uint64(n), uint64(ptr))
ctx.c.call("sqlite3_set_auxdata_go", uint64(ctx.handle), uint64(n), uint64(ptr))
}
// GetAuxData returns metadata for argument n of the function.
//
// https://sqlite.org/c3ref/get_auxdata.html
func (ctx Context) GetAuxData(n int) any {
ptr := uint32(ctx.c.call(ctx.c.api.getAuxData, uint64(ctx.handle), uint64(n)))
ptr := uint32(ctx.c.call("sqlite3_get_auxdata", uint64(ctx.handle), uint64(n)))
return util.GetHandle(ctx.c.ctx, ptr)
}
@@ -67,7 +67,7 @@ func (ctx Context) ResultInt(value int) {
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultInt64(value int64) {
ctx.c.call(ctx.c.api.resultInteger,
ctx.c.call("sqlite3_result_int64",
uint64(ctx.handle), uint64(value))
}
@@ -75,7 +75,7 @@ func (ctx Context) ResultInt64(value int64) {
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultFloat(value float64) {
ctx.c.call(ctx.c.api.resultFloat,
ctx.c.call("sqlite3_result_double",
uint64(ctx.handle), math.Float64bits(value))
}
@@ -84,9 +84,9 @@ func (ctx Context) ResultFloat(value float64) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultText(value string) {
ptr := ctx.c.newString(value)
ctx.c.call(ctx.c.api.resultText,
ctx.c.call("sqlite3_result_text64",
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
uint64(ctx.c.api.destructor), _UTF8)
uint64(ctx.c.freer), _UTF8)
}
// ResultRawText sets the text result of the function to a []byte.
@@ -94,9 +94,9 @@ func (ctx Context) ResultText(value string) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultRawText(value []byte) {
ptr := ctx.c.newBytes(value)
ctx.c.call(ctx.c.api.resultText,
ctx.c.call("sqlite3_result_text64",
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
uint64(ctx.c.api.destructor), _UTF8)
uint64(ctx.c.freer), _UTF8)
}
// ResultBlob sets the result of the function to a []byte.
@@ -105,16 +105,16 @@ func (ctx Context) ResultRawText(value []byte) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultBlob(value []byte) {
ptr := ctx.c.newBytes(value)
ctx.c.call(ctx.c.api.resultBlob,
ctx.c.call("sqlite3_result_blob64",
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
uint64(ctx.c.api.destructor))
uint64(ctx.c.freer))
}
// ResultZeroBlob sets the result of the function to a zero-filled, length n BLOB.
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultZeroBlob(n int64) {
ctx.c.call(ctx.c.api.resultZeroBlob,
ctx.c.call("sqlite3_result_zeroblob64",
uint64(ctx.handle), uint64(n))
}
@@ -122,7 +122,7 @@ func (ctx Context) ResultZeroBlob(n int64) {
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultNull() {
ctx.c.call(ctx.c.api.resultNull,
ctx.c.call("sqlite3_result_null",
uint64(ctx.handle))
}
@@ -153,9 +153,9 @@ func (ctx Context) resultRFC3339Nano(value time.Time) {
buf := util.View(ctx.c.mod, ptr, maxlen)
buf = value.AppendFormat(buf[:0], time.RFC3339Nano)
ctx.c.call(ctx.c.api.resultText,
ctx.c.call("sqlite3_result_text64",
uint64(ctx.handle), uint64(ptr), uint64(len(buf)),
uint64(ctx.c.api.destructor), _UTF8)
uint64(ctx.c.freer), _UTF8)
}
// ResultPointer sets the result of the function to NULL, just like [Context.ResultNull],
@@ -165,7 +165,7 @@ func (ctx Context) resultRFC3339Nano(value time.Time) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultPointer(ptr any) {
valPtr := util.AddHandle(ctx.c.ctx, ptr)
ctx.c.call(ctx.c.api.resultPointer, uint64(valPtr))
ctx.c.call("sqlite3_result_pointer_go", uint64(valPtr))
}
// ResultJSON sets the result of the function to the JSON encoding of value.
@@ -175,6 +175,7 @@ func (ctx Context) ResultJSON(value any) {
data, err := json.Marshal(value)
if err != nil {
ctx.ResultError(err)
return
}
ctx.ResultRawText(data)
}
@@ -185,8 +186,9 @@ func (ctx Context) ResultJSON(value any) {
func (ctx Context) ResultValue(value Value) {
if value.sqlite != ctx.c.sqlite {
ctx.ResultError(MISUSE)
return
}
ctx.c.call(ctx.c.api.resultValue,
ctx.c.call("sqlite3_result_value",
uint64(ctx.handle), uint64(value.handle))
}
@@ -195,24 +197,24 @@ func (ctx Context) ResultValue(value Value) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultError(err error) {
if errors.Is(err, NOMEM) {
ctx.c.call(ctx.c.api.resultErrorMem, uint64(ctx.handle))
ctx.c.call("sqlite3_result_error_nomem", uint64(ctx.handle))
return
}
if errors.Is(err, TOOBIG) {
ctx.c.call(ctx.c.api.resultErrorBig, uint64(ctx.handle))
ctx.c.call("sqlite3_result_error_toobig", uint64(ctx.handle))
return
}
msg, code := errorCode(err, _OK)
if msg != "" {
ptr := ctx.c.newString(msg)
ctx.c.call(ctx.c.api.resultError,
defer ctx.c.arena.mark()()
ptr := ctx.c.arena.string(msg)
ctx.c.call("sqlite3_result_error",
uint64(ctx.handle), uint64(ptr), uint64(len(msg)))
ctx.c.free(ptr)
}
if code != _OK {
ctx.c.call(ctx.c.api.resultErrorCode,
ctx.c.call("sqlite3_result_error_code",
uint64(ctx.handle), uint64(code))
}
}

View File

@@ -12,6 +12,18 @@
//
// sql.Open("sqlite3", "file:demo.db?_txlock=immediate")
//
// Possible values are: "deferred", "immediate", "exclusive".
// A [read-only] transaction is always "deferred", regardless of "_txlock".
//
// The time encoding/decoding format can be specified using "_timefmt":
//
// sql.Open("sqlite3", "file:demo.db?_timefmt=sqlite")
//
// Possible values are: "auto" (the default), "sqlite", "rfc3339";
// "auto" encodes as RFC 3339 and decodes any [format] supported by SQLite;
// "sqlite" encodes as SQLite and decodes any [format] supported by SQLite;
// "rfc3339" encodes and decodes RFC 3339 only.
//
// [PRAGMA] statements can be specified using "_pragma":
//
// sql.Open("sqlite3", "file:demo.db?_pragma=busy_timeout(10000)")
@@ -23,7 +35,9 @@
//
// [URI]: https://sqlite.org/uri.html
// [PRAGMA]: https://sqlite.org/pragma.html
// [format]: https://sqlite.org/lang_datefunc.html#time_values
// [TRANSACTION]: https://sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions
// [read-only]: https://pkg.go.dev/database/sql#TxOptions
package driver
import (
@@ -43,7 +57,7 @@ import (
// This variable can be replaced with -ldflags:
//
// go build -ldflags="-X github.com/ncruces/go-sqlite3.driverName=sqlite"
// go build -ldflags="-X github.com/ncruces/go-sqlite3/driver.driverName=sqlite"
var driverName = "sqlite3"
func init() {
@@ -81,23 +95,52 @@ func (sqlite) OpenConnector(name string) (driver.Connector, error) {
func newConnector(name string, init func(*sqlite3.Conn) error) (*connector, error) {
c := connector{name: name, init: init}
var txlock, timefmt string
if strings.HasPrefix(name, "file:") {
if _, after, ok := strings.Cut(name, "?"); ok {
query, err := url.ParseQuery(after)
if err != nil {
return nil, err
}
c.txlock = query.Get("_txlock")
c.pragmas = len(query["_pragma"]) > 0
txlock = query.Get("_txlock")
timefmt = query.Get("_timefmt")
c.pragmas = query.Has("_pragma")
}
}
switch txlock {
case "":
c.txBegin = "BEGIN"
case "deferred", "immediate", "exclusive":
c.txBegin = "BEGIN " + txlock
default:
return nil, fmt.Errorf("sqlite3: invalid _txlock: %s", txlock)
}
switch timefmt {
case "":
c.tmRead = sqlite3.TimeFormatAuto
c.tmWrite = sqlite3.TimeFormatDefault
case "sqlite":
c.tmRead = sqlite3.TimeFormatAuto
c.tmWrite = sqlite3.TimeFormat3
case "rfc3339":
c.tmRead = sqlite3.TimeFormatDefault
c.tmWrite = sqlite3.TimeFormatDefault
default:
c.tmRead = sqlite3.TimeFormat(timefmt)
c.tmWrite = sqlite3.TimeFormat(timefmt)
}
return &c, nil
}
type connector struct {
init func(*sqlite3.Conn) error
name string
txlock string
txBegin string
tmRead sqlite3.TimeFormat
tmWrite sqlite3.TimeFormat
pragmas bool
}
@@ -106,7 +149,12 @@ func (n *connector) Driver() driver.Driver {
}
func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
var c conn
c := &conn{
txBegin: n.txBegin,
tmRead: n.tmRead,
tmWrite: n.tmWrite,
}
c.Conn, err = sqlite3.Open(n.name)
if err != nil {
return nil, err
@@ -120,14 +168,6 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
old := c.Conn.SetInterrupt(ctx)
defer c.Conn.SetInterrupt(old)
switch n.txlock {
case "":
c.txBegin = "BEGIN"
case "deferred", "immediate", "exclusive":
c.txBegin = "BEGIN " + n.txlock
default:
return nil, fmt.Errorf("sqlite3: invalid _txlock: %s", n.txlock)
}
if !n.pragmas {
err = c.Conn.Exec(`PRAGMA busy_timeout=60000`)
if err != nil {
@@ -155,7 +195,7 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
return nil, err
}
}
return &c, nil
return c, nil
}
type conn struct {
@@ -163,6 +203,8 @@ type conn struct {
txBegin string
txCommit string
txRollback string
tmRead sqlite3.TimeFormat
tmWrite sqlite3.TimeFormat
readOnly byte
}
@@ -247,19 +289,10 @@ func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, e
return nil, err
}
if tail != "" {
// Check if the tail contains any SQL.
st, _, err := c.Conn.Prepare(tail)
if err != nil {
s.Close()
return nil, err
}
if st != nil {
s.Close()
st.Close()
return nil, util.TailErr
}
s.Close()
return nil, util.TailErr
}
return &stmt{s, c.Conn}, nil
return &stmt{Stmt: s, tmRead: c.tmRead, tmWrite: c.tmWrite}, nil
}
func (c *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
@@ -270,7 +303,7 @@ func (c *conn) ExecContext(ctx context.Context, query string, args []driver.Name
if savept, ok := ctx.(*saveptCtx); ok {
// Called from driver.Savepoint.
savept.Savepoint = c.Savepoint()
savept.Savepoint = c.Conn.Savepoint()
return resultRowsAffected(0), nil
}
@@ -290,8 +323,9 @@ func (*conn) CheckNamedValue(arg *driver.NamedValue) error {
}
type stmt struct {
Stmt *sqlite3.Stmt
Conn *sqlite3.Conn
*sqlite3.Stmt
tmWrite sqlite3.TimeFormat
tmRead sqlite3.TimeFormat
}
var (
@@ -301,10 +335,6 @@ var (
_ driver.NamedValueChecker = &stmt{}
)
func (s *stmt) Close() error {
return s.Stmt.Close()
}
func (s *stmt) NumInput() int {
n := s.Stmt.BindCount()
for i := 1; i <= n; i++ {
@@ -331,15 +361,15 @@ func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (drive
return nil, err
}
old := s.Conn.SetInterrupt(ctx)
defer s.Conn.SetInterrupt(old)
old := s.Stmt.Conn().SetInterrupt(ctx)
defer s.Stmt.Conn().SetInterrupt(old)
err = s.Stmt.Exec()
if err != nil {
return nil, err
}
return newResult(s.Conn), nil
return newResult(s.Stmt.Conn()), nil
}
func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
@@ -347,7 +377,7 @@ func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driv
if err != nil {
return nil, err
}
return &rows{ctx, s.Stmt, s.Conn}, nil
return &rows{s, ctx}, nil
}
func (s *stmt) setupBindings(args []driver.NamedValue) error {
@@ -386,7 +416,7 @@ func (s *stmt) setupBindings(args []driver.NamedValue) error {
case sqlite3.ZeroBlob:
err = s.Stmt.BindZeroBlob(id, int64(a))
case time.Time:
err = s.Stmt.BindTime(id, a, sqlite3.TimeFormatDefault)
err = s.Stmt.BindTime(id, a, s.tmWrite)
case interface{ Pointer() any }:
err = s.Stmt.BindPointer(id, a.Pointer())
case interface{ JSON() any }:
@@ -449,12 +479,12 @@ func (r resultRowsAffected) RowsAffected() (int64, error) {
}
type rows struct {
ctx context.Context
Stmt *sqlite3.Stmt
Conn *sqlite3.Conn
*stmt
ctx context.Context
}
func (r *rows) Close() error {
r.Stmt.ClearBindings()
return r.Stmt.Reset()
}
@@ -467,9 +497,19 @@ func (r *rows) Columns() []string {
return columns
}
func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
decltype := r.Stmt.ColumnDeclType(index)
if len := len(decltype); len > 0 && decltype[len-1] == ')' {
if i := strings.LastIndexByte(decltype, '('); i >= 0 {
decltype = decltype[:i]
}
}
return strings.ToUpper(strings.TrimSpace(decltype))
}
func (r *rows) Next(dest []driver.Value) error {
old := r.Conn.SetInterrupt(r.ctx)
defer r.Conn.SetInterrupt(old)
old := r.Stmt.Conn().SetInterrupt(r.ctx)
defer r.Stmt.Conn().SetInterrupt(old)
if !r.Stmt.Step() {
if err := r.Stmt.Err(); err != nil {
@@ -479,6 +519,10 @@ func (r *rows) Next(dest []driver.Value) error {
}
for i := range dest {
if t, ok := r.decodeTime(i); ok {
dest[i] = t
continue
}
switch r.Stmt.ColumnType(i) {
case sqlite3.INTEGER:
dest[i] = r.Stmt.ColumnInt64(i)
@@ -497,3 +541,22 @@ func (r *rows) Next(dest []driver.Value) error {
return r.Stmt.Err()
}
func (s *stmt) decodeTime(i int) (_ time.Time, _ bool) {
if s.tmRead == "" {
return
}
switch s.Stmt.ColumnType(i) {
case sqlite3.INTEGER, sqlite3.FLOAT, sqlite3.TEXT:
// maybe
default:
return
}
switch strings.ToUpper(s.Stmt.ColumnDeclType(i)) {
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
// maybe
default:
return
}
return s.Stmt.ColumnTime(i, s.tmRead), s.Stmt.Err() == nil
}

View File

@@ -114,13 +114,7 @@ func Test_Open_txLock(t *testing.T) {
func Test_Open_txLock_invalid(t *testing.T) {
t.Parallel()
db, err := sql.Open("sqlite3", "file::memory:?_txlock=xclusive")
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.Conn(context.TODO())
_, err := sql.Open("sqlite3", "file::memory:?_txlock=xclusive")
if err == nil {
t.Fatal("want error")
}
@@ -186,12 +180,6 @@ func Test_Prepare(t *testing.T) {
}
defer db.Close()
stmt, err := db.Prepare(`SELECT 1; -- HERE`)
if err != nil {
t.Error(err)
}
defer stmt.Close()
var serr *sqlite3.Error
_, err = db.Prepare(`SELECT`)
if err == nil {
@@ -207,18 +195,14 @@ func Test_Prepare(t *testing.T) {
t.Error("got message:", got)
}
_, err = db.Prepare(`SELECT 1; `)
if err.Error() != string(util.TailErr) {
t.Error("want tailErr")
}
_, err = db.Prepare(`SELECT 1; SELECT`)
if err == nil {
t.Error("want error")
}
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.ERROR {
t.Errorf("got %d, want sqlite3.ERROR", rc)
}
if got := err.Error(); got != `sqlite3: SQL logic error: incomplete input` {
t.Error("got message:", got)
if err.Error() != string(util.TailErr) {
t.Error("want tailErr")
}
_, err = db.Prepare(`SELECT 1; SELECT 2`)
@@ -297,6 +281,7 @@ func Test_QueryRow_blob_null(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer rows.Close()
want := [][]byte{nil, {0xca, 0xfe}, {0xba, 0xbe}, nil}
for i := 0; rows.Next(); i++ {

View File

@@ -49,14 +49,14 @@ func Fuzz_stringOrTime_1(f *testing.F) {
// This checks that any [time.Time] can be recovered as a [time.Time],
// with nanosecond accuracy, and preserving any timezone offset.
func Fuzz_stringOrTime_2(f *testing.F) {
f.Add(0, 0)
f.Add(0, 1)
f.Add(0, -1)
f.Add(0, 999_999_999)
f.Add(0, 1_000_000_000)
f.Add(7956915742, 222_222_222) // twosday
f.Add(639095955742, 222_222_222) // twosday, year 22222AD
f.Add(-763421161058, 222_222_222) // twosday, year 22222BC
f.Add(int64(0), int64(0))
f.Add(int64(0), int64(1))
f.Add(int64(0), int64(-1))
f.Add(int64(0), int64(999_999_999))
f.Add(int64(0), int64(1_000_000_000))
f.Add(int64(7956915742), int64(222_222_222)) // twosday
f.Add(int64(639095955742), int64(222_222_222)) // twosday, year 22222AD
f.Add(int64(-763421161058), int64(222_222_222)) // twosday, year 22222BC
checkTime := func(t testing.TB, date time.Time) {
value := stringOrTime([]byte(date.Format(time.RFC3339Nano)))
@@ -80,7 +80,7 @@ func Fuzz_stringOrTime_2(f *testing.F) {
}
}
f.Fuzz(func(t *testing.T, sec, nsec int) {
f.Fuzz(func(t *testing.T, sec, nsec int64) {
// Reduce the search space.
if 1e12 < sec || sec < -1e12 {
// Dates before 29000BC and after 33000AD; I think we're safe.
@@ -91,7 +91,7 @@ func Fuzz_stringOrTime_2(f *testing.F) {
return
}
unix := time.Unix(int64(sec), int64(nsec))
unix := time.Unix(sec, nsec)
checkTime(t, unix)
checkTime(t, unix.UTC())
checkTime(t, unix.In(time.FixedZone("", -8*3600)))

View File

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

View File

@@ -23,7 +23,7 @@ WASI_SDK="$ROOT/tools/wasi-sdk-20.0/bin"
trap 'rm -f sqlite3.tmp' EXIT
"$BINARYEN/wasm-ctor-eval" -g -c _initialize sqlite3.wasm -o sqlite3.tmp
"$BINARYEN/wasm-opt" -g --strip -c -O3 \
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
sqlite3.tmp -o sqlite3.wasm \
--enable-simd --enable-mutable-globals --enable-multivalue \
--enable-bulk-memory --enable-reference-types \

View File

@@ -1,91 +1,101 @@
free
malloc
malloc_destructor
sqlite3_errcode
sqlite3_errstr
sqlite3_errmsg
sqlite3_error_offset
sqlite3_open_v2
sqlite3_close
sqlite3_close_v2
sqlite3_prepare_v3
sqlite3_finalize
sqlite3_reset
sqlite3_step
sqlite3_exec
sqlite3_interrupt
sqlite3_progress_handler_go
sqlite3_clear_bindings
sqlite3_aggregate_context
sqlite3_anycollseq_init
sqlite3_backup_finish
sqlite3_backup_init
sqlite3_backup_pagecount
sqlite3_backup_remaining
sqlite3_backup_step
sqlite3_bind_blob64
sqlite3_bind_double
sqlite3_bind_int64
sqlite3_bind_null
sqlite3_bind_parameter_count
sqlite3_bind_parameter_index
sqlite3_bind_parameter_name
sqlite3_bind_null
sqlite3_bind_int64
sqlite3_bind_double
sqlite3_bind_text64
sqlite3_bind_blob64
sqlite3_bind_zeroblob64
sqlite3_bind_pointer_go
sqlite3_column_count
sqlite3_column_name
sqlite3_column_type
sqlite3_column_int64
sqlite3_column_double
sqlite3_column_text
sqlite3_bind_text64
sqlite3_bind_value
sqlite3_bind_zeroblob64
sqlite3_blob_bytes
sqlite3_blob_close
sqlite3_blob_open
sqlite3_blob_read
sqlite3_blob_reopen
sqlite3_blob_write
sqlite3_changes64
sqlite3_clear_bindings
sqlite3_close
sqlite3_close_v2
sqlite3_column_blob
sqlite3_column_bytes
sqlite3_blob_open
sqlite3_blob_close
sqlite3_blob_reopen
sqlite3_blob_bytes
sqlite3_blob_read
sqlite3_blob_write
sqlite3_backup_init
sqlite3_backup_step
sqlite3_backup_finish
sqlite3_backup_remaining
sqlite3_backup_pagecount
sqlite3_uri_parameter
sqlite3_uri_key
sqlite3_changes64
sqlite3_last_insert_rowid
sqlite3_get_autocommit
sqlite3_anycollseq_init
sqlite3_column_count
sqlite3_column_decltype
sqlite3_column_double
sqlite3_column_int64
sqlite3_column_name
sqlite3_column_text
sqlite3_column_type
sqlite3_column_value
sqlite3_create_aggregate_function_go
sqlite3_create_collation_go
sqlite3_create_function_go
sqlite3_create_aggregate_function_go
sqlite3_create_module_go
sqlite3_create_window_function_go
sqlite3_aggregate_context
sqlite3_user_data
sqlite3_set_auxdata_go
sqlite3_declare_vtab
sqlite3_errcode
sqlite3_errmsg
sqlite3_error_offset
sqlite3_errstr
sqlite3_exec
sqlite3_finalize
sqlite3_get_autocommit
sqlite3_get_auxdata
sqlite3_value_type
sqlite3_value_int64
sqlite3_value_double
sqlite3_value_text
sqlite3_value_blob
sqlite3_value_bytes
sqlite3_value_pointer_go
sqlite3_result_null
sqlite3_result_int64
sqlite3_result_double
sqlite3_result_text64
sqlite3_interrupt
sqlite3_last_insert_rowid
sqlite3_open_v2
sqlite3_overload_function
sqlite3_prepare_v3
sqlite3_progress_handler_go
sqlite3_reset
sqlite3_result_blob64
sqlite3_result_zeroblob64
sqlite3_result_pointer_go
sqlite3_result_value
sqlite3_result_double
sqlite3_result_error
sqlite3_result_error_code
sqlite3_result_error_nomem
sqlite3_result_error_toobig
sqlite3_create_module_go
sqlite3_declare_vtab
sqlite3_vtab_config_go
sqlite3_result_int64
sqlite3_result_null
sqlite3_result_pointer_go
sqlite3_result_text64
sqlite3_result_value
sqlite3_result_zeroblob64
sqlite3_set_auxdata_go
sqlite3_step
sqlite3_stmt_busy
sqlite3_stmt_readonly
sqlite3_stmt_status
sqlite3_uri_key
sqlite3_uri_parameter
sqlite3_user_data
sqlite3_value_blob
sqlite3_value_bytes
sqlite3_value_double
sqlite3_value_dup
sqlite3_value_free
sqlite3_value_int64
sqlite3_value_nochange
sqlite3_value_pointer_go
sqlite3_value_text
sqlite3_value_type
sqlite3_vtab_collation
sqlite3_vtab_config_go
sqlite3_vtab_distinct
sqlite3_vtab_in
sqlite3_vtab_in_first
sqlite3_vtab_in_next
sqlite3_vtab_rhs_value
sqlite3_vtab_nochange
sqlite3_vtab_on_conflict
sqlite3_vtab_on_conflict
sqlite3_vtab_rhs_value

Binary file not shown.

View File

@@ -44,8 +44,7 @@ func (e *Error) Error() string {
}
if e.msg != "" {
b.WriteByte(':')
b.WriteByte(' ')
b.WriteString(": ")
b.WriteString(e.msg)
}
@@ -143,6 +142,8 @@ func errorCode(err error, def ErrorCode) (msg string, code uint32) {
return "", uint32(code)
case ExtendedErrorCode:
return "", uint32(code)
case *Error:
return code.msg, uint32(code.code)
case nil:
return "", _OK
}

View File

@@ -135,8 +135,8 @@ func Test_ErrorCode_Error(t *testing.T) {
// Test all error codes.
for i := 0; i == int(ErrorCode(i)); i++ {
want := "sqlite3: "
r := db.call(db.api.errstr, uint64(i))
want += util.ReadString(db.mod, uint32(r), _MAX_STRING)
r := db.call("sqlite3_errstr", uint64(i))
want += util.ReadString(db.mod, uint32(r), _MAX_NAME)
got := ErrorCode(i).Error()
if got != want {
@@ -157,8 +157,8 @@ func Test_ExtendedErrorCode_Error(t *testing.T) {
// Test all extended error codes.
for i := 0; i == int(ExtendedErrorCode(i)); i++ {
want := "sqlite3: "
r := db.call(db.api.errstr, uint64(i))
want += util.ReadString(db.mod, uint32(r), _MAX_STRING)
r := db.call("sqlite3_errstr", uint64(i))
want += util.ReadString(db.mod, uint32(r), _MAX_NAME)
got := ExtendedErrorCode(i).Error()
if got != want {

View File

@@ -15,7 +15,7 @@ import (
// https://sqlite.org/carray.html
func Register(db *sqlite3.Conn) {
sqlite3.CreateModule[array](db, "array", nil,
func(db *sqlite3.Conn, arg ...string) (array, error) {
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (array, error) {
err := db.DeclareVtab(`CREATE TABLE x(value, array HIDDEN)`)
return array{}, err
})
@@ -119,7 +119,7 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
return nil
}
func indexable(v reflect.Value) (_ reflect.Value, err error) {
func indexable(v reflect.Value) (reflect.Value, error) {
if v.Kind() == reflect.Slice {
return v, nil
}

View File

@@ -52,6 +52,8 @@ func Example() {
}
func Test_cursor_Column(t *testing.T) {
t.Parallel()
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
array.Register(c)
return nil

View File

@@ -24,7 +24,7 @@ func Register(db *sqlite3.Conn) {
func openBlob(ctx sqlite3.Context, arg ...sqlite3.Value) {
if len(arg) < 6 {
ctx.ResultError(util.ErrorString("wrong number of arguments to function blob_open()"))
ctx.ResultError(util.ErrorString("blob_open: wrong number of arguments"))
return
}
@@ -60,10 +60,10 @@ func openBlob(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
// This ensures the blob is closed if db, table, column or write change.
ctx.SetAuxData(0, blob)
ctx.SetAuxData(1, blob)
ctx.SetAuxData(2, blob)
ctx.SetAuxData(4, blob)
ctx.SetAuxData(0, blob) // db
ctx.SetAuxData(1, blob) // table
ctx.SetAuxData(2, blob) // column
ctx.SetAuxData(4, blob) // write
}
// OpenCallback is the type for the blob_open callback.

View File

@@ -4,10 +4,13 @@ import (
"io"
"log"
"os"
"reflect"
"testing"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/array"
"github.com/ncruces/go-sqlite3/ext/blob"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
)
@@ -59,3 +62,67 @@ func Example() {
// Output:
// Hello BLOB!
}
func TestRegister(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
blob.Register(db)
array.Register(db)
err = db.Exec(`SELECT blob_open()`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`
CREATE TABLE IF NOT EXISTS test1 (col);
CREATE TABLE IF NOT EXISTS test2 (col);
INSERT INTO test1 VALUES (x'cafe');
INSERT INTO test2 VALUES (x'babe');
`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT blob_open('main', value, 'col', 1, false, ?) FROM array(?)`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
var got []string
err = stmt.BindPointer(1, blob.OpenCallback(func(b *sqlite3.Blob, _ ...sqlite3.Value) error {
d, err := io.ReadAll(b)
if err != nil {
return err
}
got = append(got, string(d))
return nil
}))
if err != nil {
t.Fatal(err)
}
err = stmt.BindPointer(2, []string{"test1", "test2"})
if err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
t.Fatal(err)
}
want := []string{"\xca\xfe", "\xba\xbe"}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}

View File

@@ -28,7 +28,7 @@ func Register(db *sqlite3.Conn) {
// RegisterOpen registers the CSV virtual table.
// If a filename is specified, open is used to open the file.
func RegisterOpen(db *sqlite3.Conn, open func(name string) (io.ReaderAt, error)) {
declare := func(db *sqlite3.Conn, arg ...string) (_ *table, err error) {
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
var (
filename string
data string
@@ -40,7 +40,7 @@ func RegisterOpen(db *sqlite3.Conn, open func(name string) (io.ReaderAt, error))
done = map[string]struct{}{}
)
for _, arg := range arg[3:] {
for _, arg := range arg {
key, val := getParam(arg)
if _, ok := done[key]; ok {
return nil, fmt.Errorf("csv: more than one %q parameter", key)
@@ -85,6 +85,7 @@ func RegisterOpen(db *sqlite3.Conn, open func(name string) (io.ReaderAt, error))
r: r,
comma: comma,
header: header,
bom: -1,
}
defer func() {
if err != nil {
@@ -92,11 +93,13 @@ func RegisterOpen(db *sqlite3.Conn, open func(name string) (io.ReaderAt, error))
}
}()
if schema == "" && (header || columns < 0) {
csv := table.newReader()
row, err := csv.Read()
if err != nil {
return nil, err
if schema == "" {
var row []string
if header || columns < 0 {
row, err = table.newReader().Read()
if err != nil {
return nil, err
}
}
schema = getSchema(header, columns, row)
}
@@ -119,6 +122,7 @@ type table struct {
r io.ReaderAt
comma rune
header bool
bom int8
}
func (t *table) Close() error {
@@ -151,7 +155,16 @@ func (t *table) Integrity(schema, table string, flags int) (err error) {
}
func (t *table) newReader() *csv.Reader {
csv := csv.NewReader(io.NewSectionReader(t.r, 0, math.MaxInt64))
if t.bom < 0 {
var bom [3]byte
t.r.ReadAt(bom[:], 0)
if string(bom[:]) == "\xEF\xBB\xBF" {
t.bom = 3
} else {
t.bom = 0
}
}
csv := csv.NewReader(io.NewSectionReader(t.r, int64(t.bom), math.MaxInt64))
csv.ReuseRecord = true
csv.Comma = t.comma
return csv
@@ -159,9 +172,9 @@ func (t *table) newReader() *csv.Reader {
type cursor struct {
table *table
rowID int64
row []string
csv *csv.Reader
row []string
rowID int64
}
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {

View File

@@ -51,6 +51,8 @@ func Example() {
}
func TestRegister(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
@@ -59,7 +61,7 @@ func TestRegister(t *testing.T) {
csv.Register(db)
data := `
const data = "\xEF\xBB\xBF" + `
"Rob" "Pike" rob
"Ken" Thompson ken
Robert "Griesemer" "gri"`
@@ -106,6 +108,8 @@ Robert "Griesemer" "gri"`
}
func TestRegister_errors(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
@@ -116,35 +120,35 @@ func TestRegister_errors(t *testing.T) {
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv()`)
if err == nil {
t.Fatal(err)
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv(data='abc', data='abc')`)
if err == nil {
t.Fatal(err)
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv(data='abc', xpto='abc')`)
if err == nil {
t.Fatal(err)
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv(data='abc', comma='"')`)
if err == nil {
t.Fatal(err)
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv(data='abc', header=tru)`)
if err == nil {
t.Fatal(err)
t.Fatal("want error")
} else {
t.Log(err)
}

View File

@@ -3,6 +3,8 @@ package csv
import "testing"
func Test_uintParam(t *testing.T) {
t.Parallel()
tests := []struct {
arg string
key string

View File

@@ -8,9 +8,9 @@ import (
)
func getSchema(header bool, columns int, row []string) string {
var sep = ""
var sep string
var str strings.Builder
str.WriteString(`CREATE TABLE x(`)
str.WriteString("CREATE TABLE x(")
if 0 <= columns && columns < len(row) {
row = row[:columns]
@@ -20,15 +20,17 @@ func getSchema(header bool, columns int, row []string) string {
if header && f != "" {
str.WriteString(sqlite3.QuoteIdentifier(f))
} else {
str.WriteByte('c')
str.WriteString("c")
str.WriteString(strconv.Itoa(i + 1))
}
str.WriteString(" TEXT")
sep = ","
}
for i := len(row); i < columns; i++ {
str.WriteString(sep)
str.WriteByte('c')
str.WriteString("c")
str.WriteString(strconv.Itoa(i + 1))
str.WriteString(" TEXT")
sep = ","
}
str.WriteByte(')')

View File

@@ -3,16 +3,20 @@ package csv
import "testing"
func Test_getSchema(t *testing.T) {
t.Parallel()
tests := []struct {
header bool
columns int
row []string
want string
}{
{true, 2, nil, `CREATE TABLE x(c1,c2)`},
{false, 2, nil, `CREATE TABLE x(c1,c2)`},
{true, 3, []string{"abc", ""}, `CREATE TABLE x("abc",c2,c3)`},
{true, 1, []string{"abc", "def"}, `CREATE TABLE x("abc")`},
{true, 2, nil, `CREATE TABLE x(c1 TEXT,c2 TEXT)`},
{false, 2, nil, `CREATE TABLE x(c1 TEXT,c2 TEXT)`},
{false, -1, []string{"abc", ""}, `CREATE TABLE x(c1 TEXT,c2 TEXT)`},
{true, 3, []string{"abc", ""}, `CREATE TABLE x("abc" TEXT,c2 TEXT,c3 TEXT)`},
{true, -1, []string{"abc", "def"}, `CREATE TABLE x("abc" TEXT,"def" TEXT)`},
{true, 1, []string{"abc", "def"}, `CREATE TABLE x("abc" TEXT)`},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {

128
ext/lines/lines.go Normal file
View File

@@ -0,0 +1,128 @@
// Package lines provides a virtual table to read large files line-by-line.
package lines
import (
"bufio"
"bytes"
"fmt"
"io"
"math"
"os"
"github.com/ncruces/go-sqlite3"
)
// Register registers the lines and lines_read virtual tables.
// The lines virtual table reads from a database blob or text.
// The lines_read virtual table reads from a file or an [io.ReaderAt].
func Register(db *sqlite3.Conn) {
sqlite3.CreateModule[lines](db, "lines", nil,
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
err := db.DeclareVtab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
db.VtabConfig(sqlite3.VTAB_INNOCUOUS)
return false, err
})
sqlite3.CreateModule[lines](db, "lines_read", nil,
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
err := db.DeclareVtab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
db.VtabConfig(sqlite3.VTAB_DIRECTONLY)
return true, err
})
}
type lines bool
func (l lines) BestIndex(idx *sqlite3.IndexInfo) error {
for i, cst := range idx.Constraint {
if cst.Column == 1 && cst.Op == sqlite3.INDEX_CONSTRAINT_EQ && cst.Usable {
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
Omit: true,
ArgvIndex: 1,
}
idx.EstimatedCost = 1e6
idx.EstimatedRows = 100
return nil
}
}
return sqlite3.CONSTRAINT
}
func (l lines) Open() (sqlite3.VTabCursor, error) {
return &cursor{reader: bool(l)}, nil
}
type cursor struct {
scanner *bufio.Scanner
closer io.Closer
rowID int64
eof bool
reader bool
}
func (c *cursor) Close() (err error) {
if c.closer != nil {
err = c.closer.Close()
c.closer = nil
}
return err
}
func (c *cursor) EOF() bool {
return c.eof
}
func (c *cursor) Next() error {
c.rowID++
c.eof = !c.scanner.Scan()
return c.scanner.Err()
}
func (c *cursor) RowID() (int64, error) {
return c.rowID, nil
}
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
if n == 0 {
ctx.ResultRawText(c.scanner.Bytes())
}
return nil
}
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
if err := c.Close(); err != nil {
return err
}
var r io.Reader
data := arg[0]
typ := data.Type()
if c.reader {
switch typ {
case sqlite3.NULL:
if p, ok := data.Pointer().(io.ReaderAt); ok {
r = io.NewSectionReader(p, 0, math.MaxInt64)
}
case sqlite3.TEXT:
f, err := os.Open(data.Text())
if err != nil {
return err
}
c.closer = f
r = f
}
} else {
switch typ {
case sqlite3.TEXT:
r = bytes.NewReader(data.RawText())
case sqlite3.BLOB:
r = bytes.NewReader(data.RawBlob())
}
}
if r == nil {
return fmt.Errorf("lines: unsupported argument:%.0w %v", sqlite3.MISMATCH, typ)
}
c.scanner = bufio.NewScanner(r)
c.rowID = 0
return c.Next()
}

186
ext/lines/lines_test.go Normal file
View File

@@ -0,0 +1,186 @@
package lines_test
import (
"database/sql"
"errors"
"fmt"
"log"
"os"
"strings"
"testing"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/lines"
)
func Example() {
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
lines.Register(c)
return nil
})
if err != nil {
log.Fatal(err)
}
defer db.Close()
// https://storage.googleapis.com/quickdraw_dataset/full/simplified/calendar.ndjson
f, err := os.Open("calendar.ndjson")
if err != nil {
log.Fatal(err)
}
defer f.Close()
rows, err := db.Query(`
SELECT
line ->> '$.countrycode' as countrycode,
COUNT(*)
FROM lines_read(?)
GROUP BY 1
ORDER BY 2 DESC
LIMIT 5`,
sqlite3.Pointer(f))
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var countrycode sql.RawBytes
var count int
for rows.Next() {
err := rows.Scan(&countrycode, &count)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %d\n", countrycode, count)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
// Sample output:
// US: 141001
// GB: 22560
// CA: 11759
// RU: 9250
// DE: 8748
}
func Test_lines(t *testing.T) {
t.Parallel()
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
lines.Register(c)
return nil
})
if err != nil {
log.Fatal(err)
}
defer db.Close()
const data = "line 1\nline 2\nline 3"
rows, err := db.Query(`SELECT rowid, line FROM lines(?)`, data)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int64
var line string
err := rows.Scan(&id, &line)
if err != nil {
t.Fatal(err)
}
}
}
func Test_lines_error(t *testing.T) {
t.Parallel()
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
lines.Register(c)
return nil
})
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`SELECT rowid, line FROM lines(?)`, nil)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
_, err = db.Exec(`SELECT rowid, line FROM lines_read(?)`, "xpto")
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
}
func Test_lines_read(t *testing.T) {
t.Parallel()
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
lines.Register(c)
return nil
})
if err != nil {
log.Fatal(err)
}
defer db.Close()
const data = "line 1\nline 2\nline 3"
rows, err := db.Query(`SELECT rowid, line FROM lines_read(?)`,
sqlite3.Pointer(strings.NewReader(data)))
if err != nil {
t.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int64
var line string
err := rows.Scan(&id, &line)
if err != nil {
t.Fatal(err)
}
}
}
func Test_lines_test(t *testing.T) {
t.Parallel()
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
lines.Register(c)
return nil
})
if err != nil {
log.Fatal(err)
}
defer db.Close()
rows, err := db.Query(`SELECT rowid, line FROM lines_read(?)`, "lines_test.go")
if errors.Is(err, os.ErrNotExist) {
t.Skip(err)
}
if err != nil {
t.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int64
var line string
err := rows.Scan(&id, &line)
if err != nil {
t.Fatal(err)
}
}
}

267
ext/pivot/pivot.go Normal file
View File

@@ -0,0 +1,267 @@
// Package pivot implements a pivot virtual table.
//
// https://github.com/jakethaw/pivot_vtab
package pivot
import (
"errors"
"fmt"
"strings"
"github.com/ncruces/go-sqlite3"
)
// Register registers the pivot virtual table.
func Register(db *sqlite3.Conn) {
sqlite3.CreateModule(db, "pivot", declare, declare)
}
type table struct {
db *sqlite3.Conn
scan string
cell string
keys []string
cols []*sqlite3.Value
}
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
if len(arg) != 3 {
return nil, fmt.Errorf("pivot: wrong number of arguments")
}
table := &table{db: db}
defer func() {
if err != nil {
table.Close()
}
}()
var sep string
var create strings.Builder
create.WriteString("CREATE TABLE x(")
// Row key query.
table.scan = "SELECT * FROM\n" + arg[0]
stmt, _, err := db.Prepare(table.scan)
if err != nil {
return nil, err
}
defer stmt.Close()
table.keys = make([]string, stmt.ColumnCount())
for i := range table.keys {
name := sqlite3.QuoteIdentifier(stmt.ColumnName(i))
table.keys[i] = name
create.WriteString(sep)
create.WriteString(name)
sep = ","
}
stmt.Close()
// Column definition query.
stmt, _, err = db.Prepare("SELECT * FROM\n" + arg[1])
if err != nil {
return nil, err
}
if stmt.ColumnCount() != 2 {
return nil, fmt.Errorf("pivot: column definition query expects 2 result columns")
}
for stmt.Step() {
name := sqlite3.QuoteIdentifier(stmt.ColumnText(1))
table.cols = append(table.cols, stmt.ColumnValue(0).Dup())
create.WriteString(",")
create.WriteString(name)
}
stmt.Close()
// Pivot cell query.
table.cell = "SELECT * FROM\n" + arg[2]
stmt, _, err = db.Prepare(table.cell)
if err != nil {
return nil, err
}
if stmt.ColumnCount() != 1 {
return nil, fmt.Errorf("pivot: cell query expects 1 result columns")
}
if stmt.BindCount() != len(table.keys)+1 {
return nil, fmt.Errorf("pivot: cell query expects %d bound parameters", len(table.keys)+1)
}
create.WriteByte(')')
err = db.DeclareVtab(create.String())
if err != nil {
return nil, err
}
return table, nil
}
func (t *table) Close() error {
for i := range t.cols {
t.cols[i].Close()
}
return nil
}
func (t *table) BestIndex(idx *sqlite3.IndexInfo) error {
var idxStr strings.Builder
idxStr.WriteString(t.scan)
argvIndex := 1
sep := " WHERE "
for i, cst := range idx.Constraint {
if !cst.Usable || !(0 <= cst.Column && cst.Column < len(t.keys)) {
continue
}
var op string
switch cst.Op {
case sqlite3.INDEX_CONSTRAINT_EQ:
op = "="
case sqlite3.INDEX_CONSTRAINT_LT:
op = "<"
case sqlite3.INDEX_CONSTRAINT_GT:
op = ">"
case sqlite3.INDEX_CONSTRAINT_LE:
op = "<="
case sqlite3.INDEX_CONSTRAINT_GE:
op = ">="
case sqlite3.INDEX_CONSTRAINT_NE:
op = "<>"
case sqlite3.INDEX_CONSTRAINT_MATCH:
op = "MATCH"
case sqlite3.INDEX_CONSTRAINT_LIKE:
op = "LIKE"
case sqlite3.INDEX_CONSTRAINT_GLOB:
op = "GLOB"
case sqlite3.INDEX_CONSTRAINT_REGEXP:
op = "REGEXP"
case sqlite3.INDEX_CONSTRAINT_IS, sqlite3.INDEX_CONSTRAINT_ISNULL:
op = "IS"
case sqlite3.INDEX_CONSTRAINT_ISNOT, sqlite3.INDEX_CONSTRAINT_ISNOTNULL:
op = "IS NOT"
default:
continue
}
idxStr.WriteString(sep)
idxStr.WriteString(t.keys[cst.Column])
idxStr.WriteString(" ")
idxStr.WriteString(op)
idxStr.WriteString(" ?")
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
ArgvIndex: argvIndex,
Omit: true,
}
sep = " AND "
argvIndex++
}
sep = " ORDER BY "
idx.OrderByConsumed = true
for _, ord := range idx.OrderBy {
if !(0 <= ord.Column && ord.Column < len(t.keys)) {
idx.OrderByConsumed = false
continue
}
idxStr.WriteString(sep)
idxStr.WriteString(t.keys[ord.Column])
if ord.Desc {
idxStr.WriteString(" DESC")
}
sep = ","
}
idx.EstimatedCost = 1e9 / float64(argvIndex)
idx.IdxStr = idxStr.String()
return nil
}
func (t *table) Open() (sqlite3.VTabCursor, error) {
return &cursor{table: t}, nil
}
func (t *table) Rename(new string) error {
return nil
}
type cursor struct {
table *table
scan *sqlite3.Stmt
cell *sqlite3.Stmt
rowID int64
}
func (c *cursor) Close() error {
return errors.Join(c.scan.Close(), c.cell.Close())
}
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
err := c.scan.Close()
if err != nil {
return err
}
c.scan, _, err = c.table.db.Prepare(idxStr)
if err != nil {
return err
}
for i, arg := range arg {
err := c.scan.BindValue(i+1, arg)
if err != nil {
return err
}
}
if c.cell == nil {
c.cell, _, err = c.table.db.Prepare(c.table.cell)
if err != nil {
return err
}
}
c.rowID = 0
return c.Next()
}
func (c *cursor) Next() error {
if c.scan.Step() {
count := c.scan.ColumnCount()
for i := 0; i < count; i++ {
err := c.cell.BindValue(i+1, c.scan.ColumnValue(i))
if err != nil {
return err
}
}
c.rowID++
}
return c.scan.Err()
}
func (c *cursor) EOF() bool {
return !c.scan.Busy()
}
func (c *cursor) RowID() (int64, error) {
return c.rowID, nil
}
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
count := c.scan.ColumnCount()
if col < count {
ctx.ResultValue(c.scan.ColumnValue(col))
return nil
}
err := c.cell.BindValue(count+1, *c.table.cols[col-count])
if err != nil {
return err
}
if c.cell.Step() {
ctx.ResultValue(c.cell.ColumnValue(0))
}
return c.cell.Reset()
}

219
ext/pivot/pivot_test.go Normal file
View File

@@ -0,0 +1,219 @@
package pivot_test
import (
"fmt"
"log"
"strings"
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/pivot"
)
// https://antonz.org/sqlite-pivot-table/
func Example() {
db, err := sqlite3.Open(":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
pivot.Register(db)
err = db.Exec(`
CREATE TABLE sales(product TEXT, year INT, income DECIMAL);
INSERT INTO sales(product, year, income) VALUES
('alpha', 2020, 100),
('alpha', 2021, 120),
('alpha', 2022, 130),
('alpha', 2023, 140),
('beta', 2020, 10),
('beta', 2021, 20),
('beta', 2022, 40),
('beta', 2023, 80),
('gamma', 2020, 80),
('gamma', 2021, 75),
('gamma', 2022, 78),
('gamma', 2023, 80);
`)
if err != nil {
log.Fatal(err)
}
err = db.Exec(`
CREATE VIRTUAL TABLE v_sales USING pivot(
-- rows
(SELECT DISTINCT product FROM sales),
-- columns
(SELECT DISTINCT year, year FROM sales),
-- cells
(SELECT sum(income) FROM sales WHERE product = ? AND year = ?)
)`)
if err != nil {
log.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT * FROM v_sales`)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
cols := make([]string, stmt.ColumnCount())
for i := range cols {
cols[i] = stmt.ColumnName(i)
}
fmt.Println(pretty(cols))
for stmt.Step() {
for i := range cols {
cols[i] = stmt.ColumnText(i)
}
fmt.Println(pretty(cols))
}
if err := stmt.Reset(); err != nil {
log.Fatal(err)
}
// Output:
// product 2020 2021 2022 2023
// alpha 100 120 130 140
// beta 10 20 40 80
// gamma 80 75 78 80
}
func TestRegister(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
pivot.Register(db)
err = db.Exec(`
CREATE TABLE r AS
SELECT 1 id UNION SELECT 2 UNION SELECT 3;
CREATE TABLE c(
id INTEGER PRIMARY KEY,
name TEXT
);
INSERT INTO c (name) VALUES
('a'),('b'),('c'),('d');
CREATE TABLE x(
r_id INT,
c_id INT,
val TEXT
);
INSERT INTO x (r_id, c_id, val)
SELECT r.id, c.id, c.name || r.id
FROM c, r;
`)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`
CREATE VIRTUAL TABLE v_x USING pivot(
-- rows
(SELECT id r_id FROM r),
-- columns
(SELECT id c_id, name FROM c),
-- cells
(SELECT val FROM x WHERE r_id = ?1 AND c_id = ?2)
)`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT * FROM v_x WHERE rowid <> 0 AND r_id <> 1 ORDER BY rowid, r_id DESC LIMIT 1`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if stmt.Step() {
if got := stmt.ColumnInt(0); got != 3 {
t.Errorf("got %d, want 3", got)
}
}
}
func TestRegister_errors(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
pivot.Register(db)
err = db.Exec(`CREATE VIRTUAL TABLE pivot USING pivot()`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot(SELECT 1, SELECT 2, SELECT 3)`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot((SELECT 1), SELECT 2, SELECT 3)`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot((SELECT 1), (SELECT 2), SELECT 3)`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot((SELECT 1), (SELECT 1, 2), SELECT 3)`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot((SELECT 1), (SELECT 1, 2), (SELECT 3, 4))`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot((SELECT 1), (SELECT 1, 2), (SELECT 3))`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
}
func pretty(cols []string) string {
var buf strings.Builder
for i, s := range cols {
if i != 0 {
buf.WriteByte(' ')
}
for buf.Len()%8 != 0 {
buf.WriteByte(' ')
}
buf.WriteString(s)
}
return buf.String()
}

210
ext/statement/stmt.go Normal file
View File

@@ -0,0 +1,210 @@
// Package statement defines table-valued functions natively using SQL.
//
// https://github.com/0x09/sqlite-statement-vtab
package statement
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"unsafe"
"github.com/ncruces/go-sqlite3"
)
// Register registers the statement virtual table.
func Register(db *sqlite3.Conn) {
sqlite3.CreateModule(db, "statement", declare, declare)
}
type table struct {
stmt *sqlite3.Stmt
sql string
inuse bool
}
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (*table, error) {
if len(arg) != 1 {
return nil, fmt.Errorf("statement: wrong number of arguments")
}
sql := "SELECT * FROM\n" + arg[0]
stmt, _, err := db.Prepare(sql)
if err != nil {
return nil, err
}
var sep string
var str strings.Builder
str.WriteString("CREATE TABLE x(")
outputs := stmt.ColumnCount()
for i := 0; i < outputs; i++ {
name := sqlite3.QuoteIdentifier(stmt.ColumnName(i))
str.WriteString(sep)
str.WriteString(name)
str.WriteString(" ")
str.WriteString(stmt.ColumnDeclType(i))
sep = ","
}
inputs := stmt.BindCount()
for i := 1; i <= inputs; i++ {
str.WriteString(sep)
name := stmt.BindName(i)
if name == "" {
str.WriteString("[")
str.WriteString(strconv.Itoa(i))
str.WriteString("] HIDDEN")
} else {
str.WriteString(sqlite3.QuoteIdentifier(name[1:]))
str.WriteString(" HIDDEN")
}
sep = ","
}
str.WriteByte(')')
err = db.DeclareVtab(str.String())
if err != nil {
stmt.Close()
return nil, err
}
return &table{sql: sql, stmt: stmt}, nil
}
func (t *table) Close() error {
return t.stmt.Close()
}
func (t *table) BestIndex(idx *sqlite3.IndexInfo) error {
idx.EstimatedCost = 1000
var argvIndex = 1
var needIndex bool
var listIndex []int
outputs := t.stmt.ColumnCount()
for i, cst := range idx.Constraint {
// Skip if this is a constraint on one of our output columns.
if cst.Column < outputs {
continue
}
// A given query plan is only usable if all provided input columns
// are usable and have equal constraints only.
if !cst.Usable || cst.Op != sqlite3.INDEX_CONSTRAINT_EQ {
return sqlite3.CONSTRAINT
}
// The non-zero argvIdx values must be contiguous.
// If they're not, build a list and serialize it through IdxStr.
nextIndex := cst.Column - outputs + 1
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
ArgvIndex: argvIndex,
Omit: true,
}
if nextIndex != argvIndex {
needIndex = true
}
listIndex = append(listIndex, nextIndex)
argvIndex++
}
if needIndex {
buf, err := json.Marshal(listIndex)
if err != nil {
return err
}
idx.IdxStr = unsafe.String(&buf[0], len(buf))
}
return nil
}
func (t *table) Open() (sqlite3.VTabCursor, error) {
stmt := t.stmt
if !t.inuse {
t.inuse = true
} else {
var err error
stmt, _, err = t.stmt.Conn().Prepare(t.sql)
if err != nil {
return nil, err
}
}
return &cursor{table: t, stmt: stmt}, nil
}
func (t *table) Rename(new string) error {
return nil
}
type cursor struct {
table *table
stmt *sqlite3.Stmt
arg []sqlite3.Value
rowID int64
}
func (c *cursor) Close() error {
if c.stmt == c.table.stmt {
c.table.inuse = false
c.stmt.ClearBindings()
return c.stmt.Reset()
}
return c.stmt.Close()
}
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
c.arg = arg
c.rowID = 0
c.stmt.ClearBindings()
if err := c.stmt.Reset(); err != nil {
return err
}
var list []int
if idxStr != "" {
buf := unsafe.Slice(unsafe.StringData(idxStr), len(idxStr))
err := json.Unmarshal(buf, &list)
if err != nil {
return err
}
}
for i, arg := range arg {
param := i + 1
if list != nil {
param = list[i]
}
err := c.stmt.BindValue(param, arg)
if err != nil {
return err
}
}
return c.Next()
}
func (c *cursor) Next() error {
if c.stmt.Step() {
c.rowID++
}
return c.stmt.Err()
}
func (c *cursor) EOF() bool {
return !c.stmt.Busy()
}
func (c *cursor) RowID() (int64, error) {
return c.rowID, nil
}
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
switch outputs := c.stmt.ColumnCount(); {
case col < outputs:
ctx.ResultValue(c.stmt.ColumnValue(col))
case col-outputs < len(c.arg):
ctx.ResultValue(c.arg[col-outputs])
}
return nil
}

145
ext/statement/stmt_test.go Normal file
View File

@@ -0,0 +1,145 @@
package statement_test
import (
"fmt"
"log"
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/statement"
)
func Example() {
db, err := sqlite3.Open(":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
statement.Register(db)
err = db.Exec(`
CREATE VIRTUAL TABLE split_date USING statement((
SELECT
strftime('%Y', :date) AS year,
strftime('%m', :date) AS month,
strftime('%d', :date) AS day
))`)
if err != nil {
log.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT * FROM split_date('2022-02-22')`)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
if stmt.Step() {
fmt.Printf("Twosday was %d-%d-%d", stmt.ColumnInt(0), stmt.ColumnInt(1), stmt.ColumnInt(2))
}
if err := stmt.Reset(); err != nil {
log.Fatal(err)
}
// Output:
// Twosday was 2022-2-22
}
func TestRegister(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
statement.Register(db)
err = db.Exec(`
CREATE VIRTUAL TABLE arguments USING statement((SELECT ? AS a, ? AS b, ? AS c))
`)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`
SELECT * from arguments WHERE [2] = 'y' AND [3] = 'z'
`)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`
CREATE VIRTUAL TABLE hypot USING statement((SELECT sqrt(:x * :x + :y * :y) AS hypotenuse))
`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`
SELECT x, y, * FROM hypot WHERE x = 3 AND y = 4
`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if stmt.Step() {
x := stmt.ColumnInt(0)
y := stmt.ColumnInt(1)
hypot := stmt.ColumnInt(2)
if x != 3 || y != 4 || hypot != 5 {
t.Errorf("hypot(%d, %d) = %d", x, y, hypot)
}
}
}
func TestRegister_errors(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
statement.Register(db)
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement()`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement(SELECT 1, SELECT 2)`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement((SELECT 1, SELECT 2))`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement((SELECT 1; SELECT 2))`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement((CREATE TABLE x(val)))`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
}

View File

@@ -6,6 +6,8 @@ import (
)
func Test_welford(t *testing.T) {
t.Parallel()
var s1, s2 welford
s1.enqueue(4)
@@ -38,6 +40,8 @@ func Test_welford(t *testing.T) {
}
func Test_covar(t *testing.T) {
t.Parallel()
var c1, c2 welford2
c1.enqueue(3, 70)
@@ -64,6 +68,8 @@ func Test_covar(t *testing.T) {
}
func Test_correlation(t *testing.T) {
t.Parallel()
var c welford2
c.enqueue(1, 3)
c.enqueue(2, 2)

View File

@@ -113,14 +113,14 @@ func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
re = r
ctx.SetAuxData(0, re)
}
ctx.ResultBool(re.Match(arg[1].RawBlob()))
ctx.ResultBool(re.Match(arg[1].RawText()))
}
func like(ctx sqlite3.Context, arg ...sqlite3.Value) {
escape := rune(-1)
if len(arg) == 3 {
var size int
b := arg[2].RawBlob()
b := arg[2].RawText()
escape, size = utf8.DecodeRune(b)
if size != len(b) {
ctx.ResultError(util.ErrorString("ESCAPE expression must be a single character"))
@@ -141,7 +141,7 @@ func like(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
ctx.SetAuxData(0, re)
}
ctx.ResultBool(re.Match(arg[1].RawBlob()))
ctx.ResultBool(re.Match(arg[1].RawText()))
}
func like2regex(pattern string, escape rune) string {

View File

@@ -189,6 +189,8 @@ func TestRegister_error(t *testing.T) {
}
func Test_like2regex(t *testing.T) {
t.Parallel()
const prefix = `(?is)\A`
const sufix = `\z`
tests := []struct {

49
func.go
View File

@@ -14,17 +14,17 @@ import (
// This can be used to load schemas that contain
// one or more unknown collating sequences.
func (c *Conn) AnyCollationNeeded() {
c.call(c.api.anyCollation, uint64(c.handle), 0, 0)
c.call("sqlite3_anycollseq_init", uint64(c.handle), 0, 0)
}
// 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 {
defer c.arena.reset()
defer c.arena.mark()()
namePtr := c.arena.string(name)
funcPtr := util.AddHandle(c.ctx, fn)
r := c.call(c.api.createCollation,
r := c.call("sqlite3_create_collation_go",
uint64(c.handle), uint64(namePtr), uint64(funcPtr))
return c.error(r)
}
@@ -32,28 +32,31 @@ func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
// CreateFunction defines a new scalar SQL function.
//
// https://sqlite.org/c3ref/create_function.html
func (c *Conn) CreateFunction(name string, nArg int, flag FunctionFlag, fn func(ctx Context, arg ...Value)) error {
defer c.arena.reset()
func (c *Conn) CreateFunction(name string, nArg int, flag FunctionFlag, fn ScalarFunction) error {
defer c.arena.mark()()
namePtr := c.arena.string(name)
funcPtr := util.AddHandle(c.ctx, fn)
r := c.call(c.api.createFunction,
r := c.call("sqlite3_create_function_go",
uint64(c.handle), uint64(namePtr), uint64(nArg),
uint64(flag), uint64(funcPtr))
return c.error(r)
}
// ScalarFunction is the type of a scalar SQL function.
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.
//
// https://sqlite.org/c3ref/create_function.html
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn func() AggregateFunction) error {
defer c.arena.reset()
call := c.api.createAggregate
defer c.arena.mark()()
call := "sqlite3_create_aggregate_function_go"
namePtr := c.arena.string(name)
funcPtr := util.AddHandle(c.ctx, fn)
if _, ok := fn().(WindowFunction); ok {
call = c.api.createWindow
call = "sqlite3_create_window_function_go"
}
r := c.call(call,
uint64(c.handle), uint64(namePtr), uint64(nArg),
@@ -95,20 +98,20 @@ func compareCallback(ctx context.Context, mod api.Module, pApp, nKey1, pKey1, nK
func funcCallback(ctx context.Context, mod api.Module, pCtx, nArg, pArg uint32) {
db := ctx.Value(connKey{}).(*Conn)
fn := userDataHandle(db, pCtx).(func(ctx Context, arg ...Value))
fn := userDataHandle(db, pCtx).(ScalarFunction)
fn(Context{db, pCtx}, callbackArgs(db, nArg, pArg)...)
}
func stepCallback(ctx context.Context, mod api.Module, pCtx, nArg, pArg uint32) {
db := ctx.Value(connKey{}).(*Conn)
fn := aggregateCtxHandle(db, pCtx, nil).(AggregateFunction)
fn := aggregateCtxHandle(db, pCtx, nil)
fn.Step(Context{db, pCtx}, callbackArgs(db, nArg, pArg)...)
}
func finalCallback(ctx context.Context, mod api.Module, pCtx uint32) {
var handle uint32
db := ctx.Value(connKey{}).(*Conn)
fn := aggregateCtxHandle(db, pCtx, &handle).(AggregateFunction)
fn := aggregateCtxHandle(db, pCtx, &handle)
fn.Value(Context{db, pCtx})
if err := util.DelHandle(ctx, handle); err != nil {
Context{db, pCtx}.ResultError(err)
@@ -117,7 +120,7 @@ func finalCallback(ctx context.Context, mod api.Module, pCtx uint32) {
func valueCallback(ctx context.Context, mod api.Module, pCtx uint32) {
db := ctx.Value(connKey{}).(*Conn)
fn := aggregateCtxHandle(db, pCtx, nil).(AggregateFunction)
fn := aggregateCtxHandle(db, pCtx, nil)
fn.Value(Context{db, pCtx})
}
@@ -128,33 +131,31 @@ func inverseCallback(ctx context.Context, mod api.Module, pCtx, nArg, pArg uint3
}
func userDataHandle(db *Conn, pCtx uint32) any {
pApp := uint32(db.call(db.api.userData, uint64(pCtx)))
pApp := uint32(db.call("sqlite3_user_data", uint64(pCtx)))
return util.GetHandle(db.ctx, pApp)
}
func aggregateCtxHandle(db *Conn, pCtx uint32, close *uint32) any {
// On close, we're getting rid of the handle.
func aggregateCtxHandle(db *Conn, pCtx uint32, close *uint32) AggregateFunction {
// On close, we're getting rid of the aggregate.
// Don't allocate space to store it.
var size uint64
if close == nil {
size = ptrlen
}
ptr := uint32(db.call(db.api.aggregateCtx, uint64(pCtx), size))
ptr := uint32(db.call("sqlite3_aggregate_context", uint64(pCtx), size))
// Try loading the handle, if we already have one, or want a new one.
if ptr != 0 || size != 0 {
// If we already have an aggregate, return it.
if ptr != 0 {
if handle := util.ReadUint32(db.mod, ptr); handle != 0 {
fn := util.GetHandle(db.ctx, handle)
fn := util.GetHandle(db.ctx, handle).(AggregateFunction)
if close != nil {
*close = handle
}
if fn != nil {
return fn
}
return fn
}
}
// Create a new aggregate and store the handle.
// Create a new aggregate, and store it if needed.
fn := userDataHandle(db, pCtx).(func() AggregateFunction)()
if ptr != 0 {
util.WriteUint32(db.mod, ptr, util.AddHandle(db.ctx, fn))

View File

@@ -129,7 +129,7 @@ func ExampleContext_SetAuxData() {
ctx.SetAuxData(0, r)
re = r
}
ctx.ResultBool(re.Match(arg[1].RawBlob()))
ctx.ResultBool(re.Match(arg[1].RawText()))
})
if err != nil {
log.Fatal(err)

2
go.mod
View File

@@ -7,7 +7,7 @@ require (
github.com/psanford/httpreadat v0.1.0
github.com/tetratelabs/wazero v1.5.0
golang.org/x/sync v0.5.0
golang.org/x/sys v0.14.0
golang.org/x/sys v0.15.0
golang.org/x/text v0.14.0
)

4
go.sum
View File

@@ -6,7 +6,7 @@ github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08q
github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

View File

@@ -1,4 +1,3 @@
github.com/ncruces/go-sqlite3 v0.10.2/go.mod h1:8aGu9/G8lLZbvO6TXA0FXTP2liIefFmbpeXuhG4nJLw=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=

View File

@@ -13,7 +13,7 @@ import (
var (
sqliteSeparator = "`|\"|'|\t"
indexRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)CREATE(?: UNIQUE)? INDEX [%v]?[\w\d-]+[%v]? ON (.*)$`, sqliteSeparator, sqliteSeparator))
indexRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)CREATE(?: UNIQUE)? INDEX [%v]?[\w\d-]+[%v]?(?s:.*?)ON (.*)$`, sqliteSeparator, sqliteSeparator))
tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?`, sqliteSeparator, sqliteSeparator))
separatorRegexp = regexp.MustCompile(fmt.Sprintf("[%v]", sqliteSeparator))
columnsRegexp = regexp.MustCompile(fmt.Sprintf(`[(,][%v]?(\w+)[%v]?`, sqliteSeparator, sqliteSeparator))

View File

@@ -97,6 +97,24 @@ func TestParseDDL(t *testing.T) {
},
},
},
{
"index with \n from .schema sqlite",
[]string{
"CREATE TABLE `test-d` (`field` integer NOT NULL)",
"CREATE INDEX `idx_uq`\n ON `test-b`(`field`) WHERE field = 0",
},
1,
[]migrator.ColumnType{
{
NameValue: sql.NullString{String: "field", Valid: true},
DataTypeValue: sql.NullString{String: "integer", Valid: true},
ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
UniqueValue: sql.NullBool{Bool: false, Valid: true},
NullableValue: sql.NullBool{Bool: false, Valid: true},
},
},
},
}
for _, p := range params {

View File

@@ -3,7 +3,7 @@ module github.com/ncruces/go-sqlite3/gormlite
go 1.21
require (
github.com/ncruces/go-sqlite3 v0.10.2
github.com/ncruces/go-sqlite3 v0.11.0
gorm.io/gorm v1.25.5
)
@@ -12,5 +12,5 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.5.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/sys v0.15.0 // indirect
)

View File

@@ -2,14 +2,14 @@ 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.10.2 h1:TBAhgQFU4rRyqeH1xKrvgAPmflZt0QEjDEJtETIo4vs=
github.com/ncruces/go-sqlite3 v0.10.2/go.mod h1:8aGu9/G8lLZbvO6TXA0FXTP2liIefFmbpeXuhG4nJLw=
github.com/ncruces/go-sqlite3 v0.11.0 h1:PDjs8Ve2Z0GWmHyKQHGUyG78grCXKhiHCUZQI8CqXO8=
github.com/ncruces/go-sqlite3 v0.11.0/go.mod h1:zaYJ6xP+EQiWJCa3nd3h28cD8DuSIcIqh+LrJMrBN9k=
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.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0=
github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=

View File

@@ -13,7 +13,6 @@ import (
"gorm.io/gorm/migrator"
"gorm.io/gorm/schema"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/driver"
)
@@ -40,9 +39,7 @@ func (dialector _Dialector) Initialize(db *gorm.DB) (err error) {
if dialector.Conn != nil {
db.ConnPool = dialector.Conn
} else {
conn, err := driver.Open(dialector.DSN, func(c *sqlite3.Conn) error {
return c.Exec("PRAGMA foreign_keys = ON")
})
conn, err := driver.Open(dialector.DSN, nil)
if err != nil {
return err
}

View File

@@ -3,6 +3,8 @@ set -euo pipefail
cd -P -- "$(dirname -- "$0")"
go test
rm -rf gorm/ tests/
git clone --filter=blob:none https://github.com/go-gorm/gorm.git
mv gorm/tests tests

View File

@@ -1,7 +1,6 @@
package util
import (
"fmt"
"runtime"
"strconv"
)
@@ -15,9 +14,8 @@ const (
OOMErr = ErrorString("sqlite3: out of memory")
RangeErr = ErrorString("sqlite3: index out of range")
NoNulErr = ErrorString("sqlite3: missing NUL terminator")
NoGlobalErr = ErrorString("sqlite3: could not find global: ")
NoFuncErr = ErrorString("sqlite3: could not find function: ")
BinaryErr = ErrorString("sqlite3: no SQLite binary embed/set/loaded")
NoBinaryErr = ErrorString("sqlite3: no SQLite binary embed/set/loaded")
BadBinaryErr = ErrorString("sqlite3: invalid SQLite binary embed/set/loaded")
TimeErr = ErrorString("sqlite3: invalid time value")
WhenceErr = ErrorString("sqlite3: invalid whence")
OffsetErr = ErrorString("sqlite3: invalid offset")
@@ -35,14 +33,6 @@ func AssertErr() ErrorString {
return ErrorString(msg)
}
func Finalizer[T any](skip int) func(*T) {
msg := fmt.Sprintf("sqlite3: %T not closed", new(T))
if _, file, line, ok := runtime.Caller(skip + 1); ok && skip >= 0 {
msg += " (" + file + ":" + strconv.Itoa(line) + ")"
}
return func(*T) { panic(ErrorString(msg)) }
}
func ErrorCodeString(rc uint32) string {
switch rc {
case ABORT_ROLLBACK:

249
sqlite.go
View File

@@ -57,7 +57,7 @@ func compileSQLite() {
}
}
if bin == nil {
instance.err = util.BinaryErr
instance.err = util.NoBinaryErr
return
}
@@ -67,8 +67,9 @@ func compileSQLite() {
type sqlite struct {
ctx context.Context
mod api.Module
api sqliteAPI
funcs [8]api.Function
stack [8]uint64
freer uint32
}
func instantiateSQLite() (sqlt *sqlite, err error) {
@@ -86,108 +87,12 @@ func instantiateSQLite() (sqlt *sqlite, err error) {
return nil, err
}
getFun := func(name string) api.Function {
f := sqlt.mod.ExportedFunction(name)
if f == nil {
err = util.NoFuncErr + util.ErrorString(name)
return nil
}
return f
global := sqlt.mod.ExportedGlobal("malloc_destructor")
if global == nil {
return nil, util.BadBinaryErr
}
getVal := func(name string) uint32 {
g := sqlt.mod.ExportedGlobal(name)
if g == nil {
err = util.NoGlobalErr + util.ErrorString(name)
return 0
}
return util.ReadUint32(sqlt.mod, uint32(g.Get()))
}
sqlt.api = sqliteAPI{
free: getFun("free"),
malloc: getFun("malloc"),
destructor: getVal("malloc_destructor"),
errcode: getFun("sqlite3_errcode"),
errstr: getFun("sqlite3_errstr"),
errmsg: getFun("sqlite3_errmsg"),
erroff: getFun("sqlite3_error_offset"),
open: getFun("sqlite3_open_v2"),
close: getFun("sqlite3_close"),
closeZombie: getFun("sqlite3_close_v2"),
prepare: getFun("sqlite3_prepare_v3"),
finalize: getFun("sqlite3_finalize"),
reset: getFun("sqlite3_reset"),
step: getFun("sqlite3_step"),
exec: getFun("sqlite3_exec"),
interrupt: getFun("sqlite3_interrupt"),
progressHandler: getFun("sqlite3_progress_handler_go"),
clearBindings: getFun("sqlite3_clear_bindings"),
bindCount: getFun("sqlite3_bind_parameter_count"),
bindIndex: getFun("sqlite3_bind_parameter_index"),
bindName: getFun("sqlite3_bind_parameter_name"),
bindNull: getFun("sqlite3_bind_null"),
bindInteger: getFun("sqlite3_bind_int64"),
bindFloat: getFun("sqlite3_bind_double"),
bindText: getFun("sqlite3_bind_text64"),
bindBlob: getFun("sqlite3_bind_blob64"),
bindZeroBlob: getFun("sqlite3_bind_zeroblob64"),
bindPointer: getFun("sqlite3_bind_pointer_go"),
columnCount: getFun("sqlite3_column_count"),
columnName: getFun("sqlite3_column_name"),
columnType: getFun("sqlite3_column_type"),
columnInteger: getFun("sqlite3_column_int64"),
columnFloat: getFun("sqlite3_column_double"),
columnText: getFun("sqlite3_column_text"),
columnBlob: getFun("sqlite3_column_blob"),
columnBytes: getFun("sqlite3_column_bytes"),
blobOpen: getFun("sqlite3_blob_open"),
blobClose: getFun("sqlite3_blob_close"),
blobReopen: getFun("sqlite3_blob_reopen"),
blobBytes: getFun("sqlite3_blob_bytes"),
blobRead: getFun("sqlite3_blob_read"),
blobWrite: getFun("sqlite3_blob_write"),
backupInit: getFun("sqlite3_backup_init"),
backupStep: getFun("sqlite3_backup_step"),
backupFinish: getFun("sqlite3_backup_finish"),
backupRemaining: getFun("sqlite3_backup_remaining"),
backupPageCount: getFun("sqlite3_backup_pagecount"),
changes: getFun("sqlite3_changes64"),
lastRowid: getFun("sqlite3_last_insert_rowid"),
autocommit: getFun("sqlite3_get_autocommit"),
anyCollation: getFun("sqlite3_anycollseq_init"),
createCollation: getFun("sqlite3_create_collation_go"),
createFunction: getFun("sqlite3_create_function_go"),
createAggregate: getFun("sqlite3_create_aggregate_function_go"),
createWindow: getFun("sqlite3_create_window_function_go"),
aggregateCtx: getFun("sqlite3_aggregate_context"),
userData: getFun("sqlite3_user_data"),
setAuxData: getFun("sqlite3_set_auxdata_go"),
getAuxData: getFun("sqlite3_get_auxdata"),
valueType: getFun("sqlite3_value_type"),
valueInteger: getFun("sqlite3_value_int64"),
valueFloat: getFun("sqlite3_value_double"),
valueText: getFun("sqlite3_value_text"),
valueBlob: getFun("sqlite3_value_blob"),
valueBytes: getFun("sqlite3_value_bytes"),
valuePointer: getFun("sqlite3_value_pointer_go"),
resultNull: getFun("sqlite3_result_null"),
resultInteger: getFun("sqlite3_result_int64"),
resultFloat: getFun("sqlite3_result_double"),
resultText: getFun("sqlite3_result_text64"),
resultBlob: getFun("sqlite3_result_blob64"),
resultZeroBlob: getFun("sqlite3_result_zeroblob64"),
resultPointer: getFun("sqlite3_result_pointer_go"),
resultValue: getFun("sqlite3_result_value"),
resultError: getFun("sqlite3_result_error"),
resultErrorCode: getFun("sqlite3_result_error_code"),
resultErrorMem: getFun("sqlite3_result_error_nomem"),
resultErrorBig: getFun("sqlite3_result_error_toobig"),
createModule: getFun("sqlite3_create_module_go"),
declareVTab: getFun("sqlite3_declare_vtab"),
vtabConfig: getFun("sqlite3_vtab_config_go"),
vtabRHSValue: getFun("sqlite3_vtab_rhs_value"),
}
sqlt.freer = util.ReadUint32(sqlt.mod, uint32(global.Get()))
if err != nil {
return nil, err
}
@@ -209,17 +114,17 @@ func (sqlt *sqlite) error(rc uint64, handle uint32, sql ...string) error {
panic(util.OOMErr)
}
if r := sqlt.call(sqlt.api.errstr, rc); r != 0 {
err.str = util.ReadString(sqlt.mod, uint32(r), _MAX_STRING)
if r := sqlt.call("sqlite3_errstr", rc); r != 0 {
err.str = util.ReadString(sqlt.mod, uint32(r), _MAX_NAME)
}
if handle != 0 {
if r := sqlt.call(sqlt.api.errmsg, uint64(handle)); r != 0 {
err.msg = util.ReadString(sqlt.mod, uint32(r), _MAX_STRING)
if r := sqlt.call("sqlite3_errmsg", uint64(handle)); r != 0 {
err.msg = util.ReadString(sqlt.mod, uint32(r), _MAX_NAME)
}
if sql != nil {
if r := sqlt.call(sqlt.api.erroff, uint64(handle)); r != math.MaxUint32 {
if r := sqlt.call("sqlite3_error_offset", uint64(handle)); r != math.MaxUint32 {
err.sql = sql[0][r:]
}
}
@@ -232,12 +137,33 @@ func (sqlt *sqlite) error(rc uint64, handle uint32, sql ...string) error {
return &err
}
func (sqlt *sqlite) call(fn api.Function, params ...uint64) uint64 {
func (sqlt *sqlite) getfn(name string) (api.Function, uint32) {
// https://cr.yp.to/cdb/cdb.txt
hash := func(s string) uint32 {
var hash uint32 = 5381
for _, b := range []byte(s) {
hash = (hash<<5 + hash) ^ uint32(b)
}
return hash
}(name) % uint32(len(sqlt.funcs))
fn := sqlt.funcs[hash]
if fn == nil || name != fn.Definition().Name() {
fn = sqlt.mod.ExportedFunction(name)
} else {
sqlt.funcs[hash] = nil
}
return fn, hash
}
func (sqlt *sqlite) call(name string, params ...uint64) uint64 {
copy(sqlt.stack[:], params)
fn, hash := sqlt.getfn(name)
err := fn.CallWithStack(sqlt.ctx, sqlt.stack[:])
if err != nil {
panic(err)
}
sqlt.funcs[hash] = fn
return sqlt.stack[0]
}
@@ -245,14 +171,14 @@ func (sqlt *sqlite) free(ptr uint32) {
if ptr == 0 {
return
}
sqlt.call(sqlt.api.free, uint64(ptr))
sqlt.call("free", uint64(ptr))
}
func (sqlt *sqlite) new(size uint64) uint32 {
if size > _MAX_ALLOCATION_SIZE {
panic(util.OOMErr)
}
ptr := uint32(sqlt.call(sqlt.api.malloc, size))
ptr := uint32(sqlt.call("malloc", size))
if ptr == 0 && size != 0 {
panic(util.OOMErr)
}
@@ -294,17 +220,23 @@ func (a *arena) free() {
if a.sqlt == nil {
return
}
a.reset()
for _, ptr := range a.ptrs {
a.sqlt.free(ptr)
}
a.sqlt.free(a.base)
a.sqlt = nil
}
func (a *arena) reset() {
for _, ptr := range a.ptrs {
a.sqlt.free(ptr)
func (a *arena) mark() (reset func()) {
ptrs := len(a.ptrs)
next := a.next
return func() {
for _, ptr := range a.ptrs[ptrs:] {
a.sqlt.free(ptr)
}
a.ptrs = a.ptrs[:ptrs]
a.next = next
}
a.ptrs = nil
a.next = 0
}
func (a *arena) new(size uint64) uint32 {
@@ -333,91 +265,6 @@ func (a *arena) string(s string) uint32 {
return ptr
}
type sqliteAPI struct {
free api.Function
malloc api.Function
errcode api.Function
errstr api.Function
errmsg api.Function
erroff api.Function
open api.Function
close api.Function
closeZombie api.Function
prepare api.Function
finalize api.Function
reset api.Function
step api.Function
exec api.Function
interrupt api.Function
progressHandler api.Function
clearBindings api.Function
bindCount api.Function
bindIndex api.Function
bindName api.Function
bindNull api.Function
bindInteger api.Function
bindFloat api.Function
bindText api.Function
bindBlob api.Function
bindZeroBlob api.Function
bindPointer api.Function
columnCount api.Function
columnName api.Function
columnType api.Function
columnInteger api.Function
columnFloat api.Function
columnText api.Function
columnBlob api.Function
columnBytes api.Function
blobOpen api.Function
blobClose api.Function
blobReopen api.Function
blobBytes api.Function
blobRead api.Function
blobWrite api.Function
backupInit api.Function
backupStep api.Function
backupFinish api.Function
backupRemaining api.Function
backupPageCount api.Function
changes api.Function
lastRowid api.Function
autocommit api.Function
anyCollation api.Function
createCollation api.Function
createFunction api.Function
createAggregate api.Function
createWindow api.Function
aggregateCtx api.Function
userData api.Function
setAuxData api.Function
getAuxData api.Function
valueType api.Function
valueInteger api.Function
valueFloat api.Function
valueText api.Function
valueBlob api.Function
valueBytes api.Function
valuePointer api.Function
resultNull api.Function
resultInteger api.Function
resultFloat api.Function
resultText api.Function
resultBlob api.Function
resultZeroBlob api.Function
resultPointer api.Function
resultValue api.Function
resultError api.Function
resultErrorCode api.Function
resultErrorMem api.Function
resultErrorBig api.Function
createModule api.Function
declareVTab api.Function
vtabConfig api.Function
vtabRHSValue api.Function
destructor uint32
}
func exportCallbacks(env wazero.HostModuleBuilder) wazero.HostModuleBuilder {
util.ExportFuncII(env, "go_progress", progressCallback)
util.ExportFuncVI(env, "go_destroy", destroyCallback)

View File

@@ -3,33 +3,33 @@ set -euo pipefail
cd -P -- "$(dirname -- "$0")"
curl -#OL "https://sqlite.org/2023/sqlite-amalgamation-3440000.zip"
curl -#OL "https://sqlite.org/2023/sqlite-amalgamation-3440200.zip"
unzip -d . sqlite-amalgamation-*.zip
mv sqlite-amalgamation-*/sqlite3* .
rm -rf sqlite-amalgamation-*
cat *.patch | patch --posix
cat *.patch | patch --no-backup-if-mismatch
mkdir -p ext/
cd ext/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/ext/misc/decimal.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/ext/misc/uint.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/ext/misc/uuid.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/ext/misc/base64.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/ext/misc/regexp.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/ext/misc/series.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/ext/misc/anycollseq.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/ext/misc/decimal.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/ext/misc/uint.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/ext/misc/uuid.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/ext/misc/base64.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/ext/misc/regexp.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/ext/misc/series.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/ext/misc/anycollseq.c"
cd ~-
cd ../vfs/tests/mptest/testdata/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/mptest/mptest.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/mptest/config01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/mptest/config02.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/mptest/crash01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/mptest/crash02.subtest"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/mptest/multiwrite01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/mptest/mptest.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/mptest/config01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/mptest/config02.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/mptest/crash01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/mptest/crash02.subtest"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/mptest/multiwrite01.test"
cd ~-
cd ../vfs/tests/speedtest1/testdata/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.0/test/speedtest1.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.44.2/test/speedtest1.c"
cd ~-

View File

@@ -37,18 +37,20 @@
#define SQLITE_DEFAULT_WAL_SYNCHRONOUS 1
#define SQLITE_LIKE_DOESNT_MATCH_BLOBS
#define SQLITE_MAX_EXPR_DEPTH 0
#define SQLITE_OMIT_DECLTYPE
#define SQLITE_USE_ALLOCA
#define SQLITE_OMIT_DEPRECATED
#define SQLITE_OMIT_SHARED_CACHE
#define SQLITE_OMIT_AUTOINIT
#define SQLITE_USE_ALLOCA
// #define SQLITE_OMIT_DECLTYPE
// #define SQLITE_OMIT_PROGRESS_CALLBACK
// Other Options
#define SQLITE_ALLOW_URI_AUTHORITY
#define SQLITE_ENABLE_BATCH_ATOMIC_WRITE
#define SQLITE_TRUSTED_SCHEMA 0
#define SQLITE_DEFAULT_FOREIGN_KEYS 1
#define SQLITE_ENABLE_ATOMIC_WRITE
#define SQLITE_OMIT_DESERIALIZE
#define SQLITE_ENABLE_BATCH_ATOMIC_WRITE
// Because WASM does not support shared memory,
// SQLite disables WAL for WASM builds.
@@ -56,6 +58,12 @@
// https://sqlite.org/wal.html#noshm
#undef SQLITE_OMIT_WAL
// We have our own memdb VFS.
// To avoid interactions between the two,
// omit sqlite3_serialize/sqlite3_deserialize,
// which we also don't wrap.
#define SQLITE_OMIT_DESERIALIZE
// Amalgamated Extensions
#define SQLITE_ENABLE_MATH_FUNCTIONS 1

View File

@@ -10,7 +10,6 @@ int go_vfs_find(const char *zVfsName);
int go_randomness(sqlite3_vfs *, int nByte, char *zOut);
int go_sleep(sqlite3_vfs *, int microseconds);
int go_current_time(sqlite3_vfs *, double *);
int go_current_time_64(sqlite3_vfs *, sqlite3_int64 *);
int go_open(sqlite3_vfs *, sqlite3_filename zName, sqlite3_file *, int flags,
@@ -78,7 +77,6 @@ int sqlite3_os_init() {
.xRandomness = go_randomness,
.xSleep = go_sleep,
.xCurrentTime = go_current_time,
.xCurrentTimeInt64 = go_current_time_64,
};
return sqlite3_vfs_register(&os_vfs, /*default=*/true);
@@ -89,11 +87,11 @@ int localtime_s(struct tm *const pTm, time_t const *const pTime) {
}
sqlite3_vfs *sqlite3_vfs_find(const char *zVfsName) {
if (zVfsName) {
if (zVfsName && go_vfs_find(zVfsName)) {
static sqlite3_vfs *go_vfs_list;
for (sqlite3_vfs *it = go_vfs_list; it; it = it->pNext) {
if (!strcmp(zVfsName, it->zName) && go_vfs_find(it->zName)) {
if (!strcmp(zVfsName, it->zName)) {
return it;
}
}
@@ -108,30 +106,27 @@ sqlite3_vfs *sqlite3_vfs_find(const char *zVfsName) {
}
}
if (go_vfs_find(zVfsName)) {
sqlite3_vfs *head = go_vfs_list;
go_vfs_list = malloc(sizeof(sqlite3_vfs) + strlen(zVfsName) + 1);
char *name = (char *)(go_vfs_list + 1);
strcpy(name, zVfsName);
*go_vfs_list = (sqlite3_vfs){
.iVersion = 2,
.szOsFile = sizeof(struct go_file),
.mxPathname = 512,
.zName = name,
.pNext = head,
sqlite3_vfs *head = go_vfs_list;
go_vfs_list = malloc(sizeof(sqlite3_vfs) + strlen(zVfsName) + 1);
char *name = (char *)(go_vfs_list + 1);
strcpy(name, zVfsName);
*go_vfs_list = (sqlite3_vfs){
.iVersion = 2,
.szOsFile = sizeof(struct go_file),
.mxPathname = 512,
.zName = name,
.pNext = head,
.xOpen = go_open_wrapper,
.xDelete = go_delete,
.xAccess = go_access,
.xFullPathname = go_full_pathname,
.xOpen = go_open_wrapper,
.xDelete = go_delete,
.xAccess = go_access,
.xFullPathname = go_full_pathname,
.xRandomness = go_randomness,
.xSleep = go_sleep,
.xCurrentTime = go_current_time,
.xCurrentTimeInt64 = go_current_time_64,
};
return go_vfs_list;
}
.xRandomness = go_randomness,
.xSleep = go_sleep,
.xCurrentTimeInt64 = go_current_time_64,
};
return go_vfs_list;
}
return sqlite3_vfs_find_orig(zVfsName);
}

View File

@@ -37,7 +37,7 @@ func Test_sqlite_call_closed(t *testing.T) {
sqlite.close()
defer func() { _ = recover() }()
sqlite.call(sqlite.api.free)
sqlite.call("free")
t.Error("want panic")
}

168
stmt.go
View File

@@ -28,27 +28,43 @@ func (s *Stmt) Close() error {
return nil
}
r := s.c.call(s.c.api.finalize, uint64(s.handle))
r := s.c.call("sqlite3_finalize", uint64(s.handle))
s.handle = 0
return s.c.error(r)
}
// Conn returns the database connection to which the prepared statement belongs.
//
// https://sqlite.org/c3ref/db_handle.html
func (s *Stmt) Conn() *Conn {
return s.c
}
// ReadOnly returns true if and only if the statement
// makes no direct changes to the content of the database file.
//
// https://sqlite.org/c3ref/stmt_readonly.html
func (s *Stmt) ReadOnly() bool {
r := s.c.call("sqlite3_stmt_readonly", uint64(s.handle))
return r != 0
}
// Reset resets the prepared statement object.
//
// https://sqlite.org/c3ref/reset.html
func (s *Stmt) Reset() error {
r := s.c.call(s.c.api.reset, uint64(s.handle))
r := s.c.call("sqlite3_reset", uint64(s.handle))
s.err = nil
return s.c.error(r)
}
// ClearBindings resets all bindings on the prepared statement.
// Busy determines if a prepared statement has been reset.
//
// https://sqlite.org/c3ref/clear_bindings.html
func (s *Stmt) ClearBindings() error {
r := s.c.call(s.c.api.clearBindings, uint64(s.handle))
return s.c.error(r)
// https://sqlite.org/c3ref/stmt_busy.html
func (s *Stmt) Busy() bool {
r := s.c.call("sqlite3_stmt_busy", uint64(s.handle))
return r != 0
}
// Step evaluates the SQL statement.
@@ -62,9 +78,10 @@ func (s *Stmt) ClearBindings() error {
// https://sqlite.org/c3ref/step.html
func (s *Stmt) Step() bool {
s.c.checkInterrupt()
r := s.c.call(s.c.api.step, uint64(s.handle))
r := s.c.call("sqlite3_step", uint64(s.handle))
switch r {
case _ROW:
s.err = nil
return true
case _DONE:
s.err = nil
@@ -90,11 +107,32 @@ func (s *Stmt) Exec() error {
return s.Reset()
}
// Status monitors the performance characteristics of prepared statements.
//
// https://sqlite.org/c3ref/stmt_status.html
func (s *Stmt) Status(op StmtStatus, reset bool) int {
var i uint64
if reset {
i = 1
}
r := s.c.call("sqlite3_stmt_status", uint64(s.handle),
uint64(op), i)
return int(r)
}
// ClearBindings resets all bindings on the prepared statement.
//
// https://sqlite.org/c3ref/clear_bindings.html
func (s *Stmt) ClearBindings() error {
r := s.c.call("sqlite3_clear_bindings", uint64(s.handle))
return s.c.error(r)
}
// BindCount returns the number of SQL parameters in the prepared statement.
//
// https://sqlite.org/c3ref/bind_parameter_count.html
func (s *Stmt) BindCount() int {
r := s.c.call(s.c.api.bindCount,
r := s.c.call("sqlite3_bind_parameter_count",
uint64(s.handle))
return int(r)
}
@@ -104,9 +142,9 @@ func (s *Stmt) BindCount() int {
//
// https://sqlite.org/c3ref/bind_parameter_index.html
func (s *Stmt) BindIndex(name string) int {
defer s.c.arena.reset()
defer s.c.arena.mark()()
namePtr := s.c.arena.string(name)
r := s.c.call(s.c.api.bindIndex,
r := s.c.call("sqlite3_bind_parameter_index",
uint64(s.handle), uint64(namePtr))
return int(r)
}
@@ -116,14 +154,14 @@ func (s *Stmt) BindIndex(name string) int {
//
// https://sqlite.org/c3ref/bind_parameter_name.html
func (s *Stmt) BindName(param int) string {
r := s.c.call(s.c.api.bindName,
r := s.c.call("sqlite3_bind_parameter_name",
uint64(s.handle), uint64(param))
ptr := uint32(r)
if ptr == 0 {
return ""
}
return util.ReadString(s.c.mod, ptr, _MAX_STRING)
return util.ReadString(s.c.mod, ptr, _MAX_NAME)
}
// BindBool binds a bool to the prepared statement.
@@ -153,7 +191,7 @@ func (s *Stmt) BindInt(param int, value int) error {
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindInt64(param int, value int64) error {
r := s.c.call(s.c.api.bindInteger,
r := s.c.call("sqlite3_bind_int64",
uint64(s.handle), uint64(param), uint64(value))
return s.c.error(r)
}
@@ -163,7 +201,7 @@ func (s *Stmt) BindInt64(param int, value int64) error {
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindFloat(param int, value float64) error {
r := s.c.call(s.c.api.bindFloat,
r := s.c.call("sqlite3_bind_double",
uint64(s.handle), uint64(param), math.Float64bits(value))
return s.c.error(r)
}
@@ -173,11 +211,14 @@ func (s *Stmt) BindFloat(param int, value float64) error {
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindText(param int, value string) error {
if len(value) > _MAX_LENGTH {
return TOOBIG
}
ptr := s.c.newString(value)
r := s.c.call(s.c.api.bindText,
r := s.c.call("sqlite3_bind_text64",
uint64(s.handle), uint64(param),
uint64(ptr), uint64(len(value)),
uint64(s.c.api.destructor), _UTF8)
uint64(s.c.freer), _UTF8)
return s.c.error(r)
}
@@ -186,11 +227,14 @@ func (s *Stmt) BindText(param int, value string) error {
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindRawText(param int, value []byte) error {
if len(value) > _MAX_LENGTH {
return TOOBIG
}
ptr := s.c.newBytes(value)
r := s.c.call(s.c.api.bindText,
r := s.c.call("sqlite3_bind_text64",
uint64(s.handle), uint64(param),
uint64(ptr), uint64(len(value)),
uint64(s.c.api.destructor), _UTF8)
uint64(s.c.freer), _UTF8)
return s.c.error(r)
}
@@ -200,11 +244,14 @@ func (s *Stmt) BindRawText(param int, value []byte) error {
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindBlob(param int, value []byte) error {
if len(value) > _MAX_LENGTH {
return TOOBIG
}
ptr := s.c.newBytes(value)
r := s.c.call(s.c.api.bindBlob,
r := s.c.call("sqlite3_bind_blob64",
uint64(s.handle), uint64(param),
uint64(ptr), uint64(len(value)),
uint64(s.c.api.destructor))
uint64(s.c.freer))
return s.c.error(r)
}
@@ -213,7 +260,7 @@ func (s *Stmt) BindBlob(param int, value []byte) error {
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindZeroBlob(param int, n int64) error {
r := s.c.call(s.c.api.bindZeroBlob,
r := s.c.call("sqlite3_bind_zeroblob64",
uint64(s.handle), uint64(param), uint64(n))
return s.c.error(r)
}
@@ -223,7 +270,7 @@ func (s *Stmt) BindZeroBlob(param int, n int64) error {
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindNull(param int) error {
r := s.c.call(s.c.api.bindNull,
r := s.c.call("sqlite3_bind_null",
uint64(s.handle), uint64(param))
return s.c.error(r)
}
@@ -256,10 +303,10 @@ func (s *Stmt) bindRFC3339Nano(param int, value time.Time) error {
buf := util.View(s.c.mod, ptr, maxlen)
buf = value.AppendFormat(buf[:0], time.RFC3339Nano)
r := s.c.call(s.c.api.bindText,
r := s.c.call("sqlite3_bind_text64",
uint64(s.handle), uint64(param),
uint64(ptr), uint64(len(buf)),
uint64(s.c.api.destructor), _UTF8)
uint64(s.c.freer), _UTF8)
return s.c.error(r)
}
@@ -271,7 +318,7 @@ func (s *Stmt) bindRFC3339Nano(param int, value time.Time) error {
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindPointer(param int, ptr any) error {
valPtr := util.AddHandle(s.c.ctx, ptr)
r := s.c.call(s.c.api.bindPointer,
r := s.c.call("sqlite3_bind_pointer_go",
uint64(s.handle), uint64(param), uint64(valPtr))
return s.c.error(r)
}
@@ -288,11 +335,24 @@ func (s *Stmt) BindJSON(param int, value any) error {
return s.BindRawText(param, data)
}
// BindValue binds a copy of value to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindValue(param int, value Value) error {
if value.sqlite != s.c.sqlite {
return MISUSE
}
r := s.c.call("sqlite3_bind_value",
uint64(s.handle), uint64(param), uint64(value.handle))
return s.c.error(r)
}
// ColumnCount returns the number of columns in a result set.
//
// https://sqlite.org/c3ref/column_count.html
func (s *Stmt) ColumnCount() int {
r := s.c.call(s.c.api.columnCount,
r := s.c.call("sqlite3_column_count",
uint64(s.handle))
return int(r)
}
@@ -302,14 +362,14 @@ func (s *Stmt) ColumnCount() int {
//
// https://sqlite.org/c3ref/column_name.html
func (s *Stmt) ColumnName(col int) string {
r := s.c.call(s.c.api.columnName,
r := s.c.call("sqlite3_column_name",
uint64(s.handle), uint64(col))
ptr := uint32(r)
if ptr == 0 {
panic(util.OOMErr)
}
return util.ReadString(s.c.mod, ptr, _MAX_STRING)
return util.ReadString(s.c.mod, ptr, _MAX_NAME)
}
// ColumnType returns the initial [Datatype] of the result column.
@@ -317,11 +377,24 @@ func (s *Stmt) ColumnName(col int) string {
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnType(col int) Datatype {
r := s.c.call(s.c.api.columnType,
r := s.c.call("sqlite3_column_type",
uint64(s.handle), uint64(col))
return Datatype(r)
}
// ColumnDeclType returns the declared datatype of the result column.
// The leftmost column of the result set has the index 0.
//
// https://sqlite.org/c3ref/column_decltype.html
func (s *Stmt) ColumnDeclType(col int) string {
r := s.c.call("sqlite3_column_decltype",
uint64(s.handle), uint64(col))
if r == 0 {
return ""
}
return util.ReadString(s.c.mod, uint32(r), _MAX_NAME)
}
// ColumnBool returns the value of the result column as a bool.
// The leftmost column of the result set has the index 0.
// SQLite does not have a separate boolean storage class.
@@ -349,7 +422,7 @@ func (s *Stmt) ColumnInt(col int) int {
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnInt64(col int) int64 {
r := s.c.call(s.c.api.columnInteger,
r := s.c.call("sqlite3_column_int64",
uint64(s.handle), uint64(col))
return int64(r)
}
@@ -359,7 +432,7 @@ func (s *Stmt) ColumnInt64(col int) int64 {
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnFloat(col int) float64 {
r := s.c.call(s.c.api.columnFloat,
r := s.c.call("sqlite3_column_double",
uint64(s.handle), uint64(col))
return math.Float64frombits(r)
}
@@ -413,7 +486,7 @@ func (s *Stmt) ColumnBlob(col int, buf []byte) []byte {
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnRawText(col int) []byte {
r := s.c.call(s.c.api.columnText,
r := s.c.call("sqlite3_column_text",
uint64(s.handle), uint64(col))
return s.columnRawBytes(col, uint32(r))
}
@@ -425,19 +498,19 @@ func (s *Stmt) ColumnRawText(col int) []byte {
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnRawBlob(col int) []byte {
r := s.c.call(s.c.api.columnBlob,
r := s.c.call("sqlite3_column_blob",
uint64(s.handle), uint64(col))
return s.columnRawBytes(col, uint32(r))
}
func (s *Stmt) columnRawBytes(col int, ptr uint32) []byte {
if ptr == 0 {
r := s.c.call(s.c.api.errcode, uint64(s.c.handle))
r := s.c.call("sqlite3_errcode", uint64(s.c.handle))
s.err = s.c.error(r)
return nil
}
r := s.c.call(s.c.api.columnBytes,
r := s.c.call("sqlite3_column_bytes",
uint64(s.handle), uint64(col))
return util.View(s.c.mod, ptr, r)
}
@@ -466,17 +539,16 @@ func (s *Stmt) ColumnJSON(col int, ptr any) error {
return json.Unmarshal(data, ptr)
}
// Return true if stmt is an empty SQL statement.
// This is used as an optimization.
// It's OK to always return false here.
func emptyStatement(stmt string) bool {
for _, b := range []byte(stmt) {
switch b {
case ' ', '\n', '\r', '\t', '\v', '\f':
case ';':
default:
return false
}
// ColumnValue returns the unprotected value of the result column.
// The leftmost column of the result set has the index 0.
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnValue(col int) Value {
r := s.c.call("sqlite3_column_value",
uint64(s.handle), uint64(col))
return Value{
unprot: true,
sqlite: s.c.sqlite,
handle: uint32(r),
}
return true
}

View File

@@ -1,58 +0,0 @@
package sqlite3
import "testing"
func Test_emptyStatement(t *testing.T) {
t.Parallel()
tests := []struct {
name string
stmt string
want bool
}{
{"empty", "", true},
{"space", " ", true},
{"separator", ";\n ", true},
{"begin", "BEGIN", false},
{"select", "SELECT 1;", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := emptyStatement(tt.stmt); got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}
func Fuzz_emptyStatement(f *testing.F) {
f.Add("")
f.Add(" ")
f.Add(";\n ")
f.Add("; ;\v")
f.Add("BEGIN")
f.Add("SELECT 1;")
db, err := Open(":memory:")
if err != nil {
f.Fatal(err)
}
defer db.Close()
f.Fuzz(func(t *testing.T, sql string) {
// If empty, SQLite parses it as empty.
if emptyStatement(sql) {
stmt, tail, err := db.Prepare(sql)
if err != nil {
t.Errorf("%q, %v", sql, err)
}
if stmt != nil {
t.Errorf("%q, %v", sql, stmt)
}
if tail != "" {
t.Errorf("%q", sql)
}
stmt.Close()
}
})
}

View File

@@ -15,6 +15,9 @@ import (
//go:embed testdata/wal.db
var waldb []byte
//go:embed testdata/utf16be.db
var utf16db []byte
func TestDB_memory(t *testing.T) {
t.Parallel()
testDB(t, ":memory:")
@@ -34,12 +37,22 @@ func TestDB_nolock(t *testing.T) {
func TestDB_wal(t *testing.T) {
t.Parallel()
wal := filepath.Join(t.TempDir(), "test.db")
err := os.WriteFile(wal, waldb, 0666)
tmp := filepath.Join(t.TempDir(), "test.db")
err := os.WriteFile(tmp, waldb, 0666)
if err != nil {
t.Fatal(err)
}
testDB(t, wal)
testDB(t, tmp)
}
func TestDB_utf16(t *testing.T) {
t.Parallel()
tmp := filepath.Join(t.TempDir(), "test.db")
err := os.WriteFile(tmp, utf16db, 0666)
if err != nil {
t.Fatal(err)
}
testDB(t, tmp)
}
func TestDB_vfs(t *testing.T) {
@@ -80,6 +93,9 @@ func testDB(t testing.TB, name string) {
id := stmt.ColumnInt(0)
name := stmt.ColumnText(1)
if row >= 3 {
continue
}
if id != ids[row] {
t.Errorf("got %d, want %d", id, ids[row])
}

View File

@@ -72,6 +72,17 @@ func TestDriver(t *testing.T) {
}
defer rows.Close()
typs, err := rows.ColumnTypes()
if err != nil {
t.Fatal(err)
}
if got := typs[0].DatabaseTypeName(); got != "INT" {
t.Errorf("got %s, want INT", got)
}
if got := typs[1].DatabaseTypeName(); got != "VARCHAR" {
t.Errorf("got %s, want INT", got)
}
row := 0
ids := []int{0, 1, 2}
names := []string{"go", "zig", "whatever"}

View File

@@ -58,6 +58,7 @@ func TestJSON(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer rows.Close()
want := []string{
"null", "1", "3.141592653589793",

View File

@@ -30,6 +30,10 @@ func TestStmt(t *testing.T) {
}
defer stmt.Close()
if got := stmt.ReadOnly(); got != false {
t.Error("got true, want false")
}
if got := stmt.BindCount(); got != 1 {
t.Errorf("got %d, want 1", got)
}
@@ -137,6 +141,10 @@ func TestStmt(t *testing.T) {
}
defer stmt.Close()
if got := stmt.ReadOnly(); got != true {
t.Error("got false, want true")
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != sqlite3.INTEGER {
t.Errorf("got %v, want INTEGER", got)
@@ -586,4 +594,61 @@ func TestStmt_ColumnTime(t *testing.T) {
t.Errorf("want error")
}
}
if got := stmt.Status(sqlite3.STMTSTATUS_RUN, true); got != 1 {
t.Errorf("got %d, want 1", got)
}
}
func TestStmt_Error(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
var blob [1e9 + 1]byte
_, _, err = db.Prepare(string(blob[:]))
if err == nil {
t.Errorf("want error")
} else {
t.Log(err)
}
stmt, _, err := db.Prepare(`SELECT ?`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
err = stmt.BindText(1, string(blob[:]))
if err == nil {
t.Errorf("want error")
} else {
t.Log(err)
}
err = stmt.BindBlob(1, blob[:])
if err == nil {
t.Errorf("want error")
} else {
t.Log(err)
}
err = stmt.BindRawText(1, blob[:])
if err == nil {
t.Errorf("want error")
} else {
t.Log(err)
}
err = stmt.BindZeroBlob(1, 1e9+1)
if err == nil {
t.Errorf("want error")
} else {
t.Log(err)
}
}

BIN
tests/testdata/utf16be.db vendored Normal file

Binary file not shown.

View File

@@ -20,7 +20,7 @@ type TimeFormat string
// TimeFormats recognized by SQLite to encode/decode time values.
//
// https://sqlite.org/lang_datefunc.html
// https://sqlite.org/lang_datefunc.html#time_values
const (
TimeFormatDefault TimeFormat = "" // time.RFC3339Nano

View File

@@ -15,13 +15,46 @@ import (
type Value struct {
*sqlite
handle uint32
unprot bool
copied bool
}
func (v Value) protected() uint64 {
if v.unprot {
panic(util.ValueErr)
}
return uint64(v.handle)
}
// Dup makes a copy of the SQL value and returns a pointer to that copy.
//
// https://sqlite.org/c3ref/value_dup.html
func (v Value) Dup() *Value {
r := v.call("sqlite3_value_dup", uint64(v.handle))
return &Value{
copied: true,
sqlite: v.sqlite,
handle: uint32(r),
}
}
// Close frees an SQL value previously obtained by [Value.Dup].
//
// https://sqlite.org/c3ref/value_dup.html
func (dup *Value) Close() error {
if !dup.copied {
panic(util.ValueErr)
}
dup.call("sqlite3_value_free", uint64(dup.handle))
dup.handle = 0
return nil
}
// Type returns the initial [Datatype] of the value.
//
// https://sqlite.org/c3ref/value_blob.html
func (v Value) Type() Datatype {
r := v.call(v.api.valueType, uint64(v.handle))
r := v.call("sqlite3_value_type", v.protected())
return Datatype(r)
}
@@ -49,7 +82,7 @@ func (v Value) Int() int {
//
// https://sqlite.org/c3ref/value_blob.html
func (v Value) Int64() int64 {
r := v.call(v.api.valueInteger, uint64(v.handle))
r := v.call("sqlite3_value_int64", v.protected())
return int64(r)
}
@@ -57,7 +90,7 @@ func (v Value) Int64() int64 {
//
// https://sqlite.org/c3ref/value_blob.html
func (v Value) Float() float64 {
r := v.call(v.api.valueFloat, uint64(v.handle))
r := v.call("sqlite3_value_double", v.protected())
return math.Float64frombits(r)
}
@@ -103,7 +136,7 @@ func (v Value) Blob(buf []byte) []byte {
//
// https://sqlite.org/c3ref/value_blob.html
func (v Value) RawText() []byte {
r := v.call(v.api.valueText, uint64(v.handle))
r := v.call("sqlite3_value_text", v.protected())
return v.rawBytes(uint32(r))
}
@@ -113,7 +146,7 @@ func (v Value) RawText() []byte {
//
// https://sqlite.org/c3ref/value_blob.html
func (v Value) RawBlob() []byte {
r := v.call(v.api.valueBlob, uint64(v.handle))
r := v.call("sqlite3_value_blob", v.protected())
return v.rawBytes(uint32(r))
}
@@ -122,14 +155,14 @@ func (v Value) rawBytes(ptr uint32) []byte {
return nil
}
r := v.call(v.api.valueBytes, uint64(v.handle))
r := v.call("sqlite3_value_bytes", v.protected())
return util.View(v.mod, ptr, r)
}
// Pointer gets the pointer associated with this value,
// or nil if it has no associated pointer.
func (v Value) Pointer() any {
r := v.call(v.api.valuePointer, uint64(v.handle))
r := v.call("sqlite3_value_pointer_go", v.protected())
return util.GetHandle(v.ctx, uint32(r))
}

View File

@@ -3,7 +3,7 @@ package vfs
import "github.com/ncruces/go-sqlite3/internal/util"
const (
_MAX_STRING = 512 // Used for short strings: names, error messages…
_MAX_NAME = 512 // Used for short strings: names, error messages…
_MAX_PATHNAME = 512
_DEFAULT_SECTOR_SIZE = 4096
)

View File

@@ -36,6 +36,12 @@ func Create(name string, data []byte) {
db := new(memDB)
db.size = int64(len(data))
// Convert data from WAL to rollback journal.
if len(data) >= 20 && data[18] == 2 && data[19] == 2 {
data[18] = 1
data[19] = 1
}
sectors := divRoundUp(db.size, sectorSize)
db.data = make([]*[sectorSize]byte, sectors)
for i := range db.data {

28
vfs/memdb/memdb_test.go Normal file
View File

@@ -0,0 +1,28 @@
package memdb
import (
"testing"
_ "embed"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
)
//go:embed testdata/wal.db
var walDB []byte
func Test_wal(t *testing.T) {
Create("test.db", walDB)
db, err := sqlite3.Open("file:/test.db?vfs=memdb")
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)
}
}

BIN
vfs/memdb/testdata/wal.db vendored Normal file

Binary file not shown.

View File

@@ -7,12 +7,6 @@ import (
"os"
)
const (
_S_IREAD = 0400
_S_IWRITE = 0200
_S_IEXEC = 0100
)
func osAccess(path string, flags AccessFlag) error {
fi, err := os.Stat(path)
if err != nil {
@@ -22,12 +16,18 @@ func osAccess(path string, flags AccessFlag) error {
return nil
}
var want fs.FileMode = _S_IREAD
const (
S_IREAD = 0400
S_IWRITE = 0200
S_IEXEC = 0100
)
var want fs.FileMode = S_IREAD
if flags == ACCESS_READWRITE {
want |= _S_IWRITE
want |= S_IWRITE
}
if fi.IsDir() {
want |= _S_IEXEC
want |= S_IEXEC
}
if fi.Mode()&want != want {
return fs.ErrPermission

View File

@@ -7,7 +7,6 @@ import (
type readerVFS struct{}
// Open implements the [vfs.VFS] interface.
func (readerVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
if flags&vfs.OPEN_MAIN_DB == 0 {
return nil, flags, sqlite3.CANTOPEN
@@ -20,17 +19,14 @@ func (readerVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag,
return nil, flags, sqlite3.CANTOPEN
}
// Delete implements the [vfs.VFS] interface.
func (readerVFS) Delete(name string, dirSync bool) error {
return sqlite3.IOERR_DELETE
}
// Access implements the [vfs.VFS] interface.
func (readerVFS) Access(name string, flag vfs.AccessFlag) (bool, error) {
return false, nil
}
// FullPathname implements the [vfs.VFS] interface.
func (readerVFS) FullPathname(name string) (string, error) {
return name, nil
}

View File

@@ -23,7 +23,7 @@ WASI_SDK="$ROOT/tools/wasi-sdk-20.0/bin"
-DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION \
-D_WASI_EMULATED_GETPID -lwasi-emulated-getpid
"$BINARYEN/wasm-opt" -g --strip -c -O3 \
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
mptest.wasm -o mptest.tmp \
--enable-simd --enable-mutable-globals --enable-multivalue \
--enable-bulk-memory --enable-reference-types \

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:74294bf19d213056ef5ffb7a980c3a7de5d029d0621ded53394d3055dfc4f604
size 513604
oid sha256:8d26f7fa2d68ba43e7d36433cfb875586f2bf7b4244ec5f97d031d042688b28d
size 512278

View File

@@ -18,7 +18,7 @@ WASI_SDK="$ROOT/tools/wasi-sdk-20.0/bin"
-Wl,--import-undefined \
-D_HAVE_SQLITE_CONFIG_H
"$BINARYEN/wasm-opt" -g --strip -c -O3 \
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
speedtest1.wasm -o speedtest1.tmp \
--enable-simd --enable-mutable-globals --enable-multivalue \
--enable-bulk-memory --enable-reference-types \

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:966754393264cc43eb931ece22941d0d607e7e776e26c26b548209d2264d01a1
size 527530
oid sha256:3c581db6af92832b16476c0b0d8bbdd18211f11712dcf5b7b728f24397fadfea
size 526471

View File

@@ -23,7 +23,6 @@ func ExportHostFunctions(env wazero.HostModuleBuilder) wazero.HostModuleBuilder
util.ExportFuncIIJ(env, "go_localtime", vfsLocaltime)
util.ExportFuncIIII(env, "go_randomness", vfsRandomness)
util.ExportFuncIII(env, "go_sleep", vfsSleep)
util.ExportFuncIII(env, "go_current_time", vfsCurrentTime)
util.ExportFuncIII(env, "go_current_time_64", vfsCurrentTime64)
util.ExportFuncIIIII(env, "go_full_pathname", vfsFullPathname)
util.ExportFuncIIII(env, "go_delete", vfsDelete)
@@ -45,7 +44,7 @@ func ExportHostFunctions(env wazero.HostModuleBuilder) wazero.HostModuleBuilder
}
func vfsFind(ctx context.Context, mod api.Module, zVfsName uint32) uint32 {
name := util.ReadString(mod, zVfsName, _MAX_STRING)
name := util.ReadString(mod, zVfsName, _MAX_NAME)
if vfs := Find(name); vfs != nil && vfs != (vfsOS{}) {
return 1
}
@@ -84,12 +83,6 @@ func vfsSleep(ctx context.Context, mod api.Module, pVfs, nMicro uint32) _ErrorCo
return _OK
}
func vfsCurrentTime(ctx context.Context, mod api.Module, pVfs, prNow uint32) _ErrorCode {
day := julianday.Float(time.Now())
util.WriteFloat64(mod, prNow, day)
return _OK
}
func vfsCurrentTime64(ctx context.Context, mod api.Module, pVfs, piNow uint32) _ErrorCode {
day, nsec := julianday.Date(time.Now())
msec := day*86_400_000 + nsec/1_000_000
@@ -371,7 +364,7 @@ func vfsURIParameters(ctx context.Context, mod api.Module, zPath uint32, flags O
if stack[0] == 0 {
return params
}
key := util.ReadString(mod, uint32(stack[0]), _MAX_STRING)
key := util.ReadString(mod, uint32(stack[0]), _MAX_NAME)
if params.Has(key) {
continue
}
@@ -384,7 +377,7 @@ func vfsURIParameters(ctx context.Context, mod api.Module, zPath uint32, flags O
if params == nil {
params = url.Values{}
}
params.Set(key, util.ReadString(mod, uint32(stack[0]), _MAX_STRING))
params.Set(key, util.ReadString(mod, uint32(stack[0]), _MAX_NAME))
}
}
@@ -392,7 +385,7 @@ func vfsGet(mod api.Module, pVfs uint32) VFS {
var name string
if pVfs != 0 {
const zNameOffset = 16
name = util.ReadString(mod, util.ReadUint32(mod, pVfs+zNameOffset), _MAX_STRING)
name = util.ReadString(mod, util.ReadUint32(mod, pVfs+zNameOffset), _MAX_NAME)
}
if vfs := Find(name); vfs != nil {
return vfs

View File

@@ -85,22 +85,6 @@ func Test_vfsSleep(t *testing.T) {
}
}
func Test_vfsCurrentTime(t *testing.T) {
mod := wazerotest.NewModule(wazerotest.NewMemory(wazerotest.PageSize))
ctx := context.TODO()
now := time.Now()
rc := vfsCurrentTime(ctx, mod, 0, 4)
if rc != 0 {
t.Fatal("returned", rc)
}
want := julianday.Float(now)
if got := util.ReadFloat64(mod, 4); float32(got) != float32(want) {
t.Errorf("got %v, want %v", got, want)
}
}
func Test_vfsCurrentTime64(t *testing.T) {
mod := wazerotest.NewModule(wazerotest.NewMemory(wazerotest.PageSize))
ctx := context.TODO()

50
vtab.go
View File

@@ -53,10 +53,10 @@ func CreateModule[T VTab](db *Conn, name string, create, connect VTabConstructor
flags |= VTAB_SAVEPOINTER
}
defer db.arena.reset()
defer db.arena.mark()()
namePtr := db.arena.string(name)
modulePtr := util.AddHandle(db.ctx, module[T]{create, connect})
r := db.call(db.api.createModule, uint64(db.handle),
r := db.call("sqlite3_create_module_go", uint64(db.handle),
uint64(namePtr), uint64(flags), uint64(modulePtr))
return db.error(r)
}
@@ -70,9 +70,9 @@ func implements[T any](typ reflect.Type) bool {
//
// https://sqlite.org/c3ref/declare_vtab.html
func (c *Conn) DeclareVtab(sql string) error {
// defer c.arena.reset()
defer c.arena.mark()()
sqlPtr := c.arena.string(sql)
r := c.call(c.api.declareVTab, uint64(c.handle), uint64(sqlPtr))
r := c.call("sqlite3_declare_vtab", uint64(c.handle), uint64(sqlPtr))
return c.error(r)
}
@@ -98,12 +98,12 @@ func (c *Conn) VtabConfig(op VtabConfigOption, args ...any) error {
i = 1
}
}
r := c.call(c.api.vtabConfig, uint64(c.handle), uint64(op), i)
r := c.call("sqlite3_vtab_config_go", uint64(c.handle), uint64(op), i)
return c.error(r)
}
// VTabConstructor is a virtual table constructor function.
type VTabConstructor[T VTab] func(db *Conn, arg ...string) (T, error)
type VTabConstructor[T VTab] func(db *Conn, module, schema, table string, arg ...string) (T, error)
type module[T VTab] [2]VTabConstructor[T]
@@ -143,7 +143,7 @@ type VTabRenamer interface {
type VTabOverloader interface {
VTab
// https://sqlite.org/vtab.html#xfindfunction
FindFunction(arg int, name string) (func(ctx Context, arg ...Value), IndexConstraintOp)
FindFunction(arg int, name string) (ScalarFunction, IndexConstraintOp)
}
// A VTabChecker allows a virtual table to report errors
@@ -161,6 +161,11 @@ type VTabChecker interface {
// A VTabTx allows a virtual table to implement
// transactions with two-phase commit.
//
// Anything that is required as part of a commit that may fail
// should be performed in the Sync() callback.
// Current versions of SQLite ignore any errors
// returned by Commit() and Rollback().
type VTabTx interface {
VTab
// https://sqlite.org/vtab.html#xBegin
@@ -254,15 +259,15 @@ type IndexConstraintUsage struct {
// if the right-hand operand is known.
//
// https://sqlite.org/c3ref/vtab_rhs_value.html
func (idx *IndexInfo) RHSValue(column int) (*Value, error) {
// defer idx.c.arena.reset()
func (idx *IndexInfo) RHSValue(column int) (Value, error) {
defer idx.c.arena.mark()()
valPtr := idx.c.arena.new(ptrlen)
r := idx.c.call(idx.c.api.vtabRHSValue,
r := idx.c.call("sqlite3_vtab_rhs_value",
uint64(idx.handle), uint64(column), uint64(valPtr))
if err := idx.c.error(r); err != nil {
return nil, err
return Value{}, err
}
return &Value{
return Value{
sqlite: idx.c.sqlite,
handle: util.ReadUint32(idx.c.mod, valPtr),
}, nil
@@ -318,7 +323,7 @@ func (idx *IndexInfo) save() {
util.WriteUint32(mod, ptr+20, uint32(idx.IdxNum))
if idx.IdxStr != "" {
util.WriteUint32(mod, ptr+24, idx.c.newString(idx.IdxStr))
util.WriteUint32(mod, ptr+28, 1)
util.WriteUint32(mod, ptr+28, 1) // needToFreeIdxStr
}
if idx.OrderByConsumed {
util.WriteUint32(mod, ptr+32, 1)
@@ -369,7 +374,7 @@ func vtabModuleCallback(i int) func(_ context.Context, _ api.Module, _, _, _, _,
for i := uint32(0); i < argc; i++ {
ptr := util.ReadUint32(mod, argv+i*ptrlen)
arg[i+1] = reflect.ValueOf(util.ReadString(mod, ptr, _MAX_STRING))
arg[i+1] = reflect.ValueOf(util.ReadString(mod, ptr, _MAX_SQL_LENGTH))
}
module := vtabGetHandle(ctx, mod, pMod)
@@ -425,13 +430,13 @@ func vtabUpdateCallback(ctx context.Context, mod api.Module, pVTab, argc, argv,
func vtabRenameCallback(ctx context.Context, mod api.Module, pVTab, zNew uint32) uint32 {
vtab := vtabGetHandle(ctx, mod, pVTab).(VTabRenamer)
err := vtab.Rename(util.ReadString(mod, zNew, _MAX_STRING))
err := vtab.Rename(util.ReadString(mod, zNew, _MAX_NAME))
return vtabError(ctx, mod, pVTab, _VTAB_ERROR, err)
}
func vtabFindFuncCallback(ctx context.Context, mod api.Module, pVTab, nArg, zName, pxFunc uint32) uint32 {
vtab := vtabGetHandle(ctx, mod, pVTab).(VTabOverloader)
fn, op := vtab.FindFunction(int(nArg), util.ReadString(mod, zName, _MAX_STRING))
fn, op := vtab.FindFunction(int(nArg), util.ReadString(mod, zName, _MAX_NAME))
if fn != nil {
handle := util.AddHandle(ctx, fn)
util.WriteUint32(mod, pxFunc, handle)
@@ -444,8 +449,8 @@ func vtabFindFuncCallback(ctx context.Context, mod api.Module, pVTab, nArg, zNam
func vtabIntegrityCallback(ctx context.Context, mod api.Module, pVTab, zSchema, zTabName, mFlags, pzErr uint32) uint32 {
vtab := vtabGetHandle(ctx, mod, pVTab).(VTabChecker)
schema := util.ReadString(mod, zSchema, _MAX_STRING)
table := util.ReadString(mod, zTabName, _MAX_STRING)
schema := util.ReadString(mod, zSchema, _MAX_NAME)
table := util.ReadString(mod, zTabName, _MAX_NAME)
err := vtab.Integrity(schema, table, int(mFlags))
// xIntegrity should return OK - even if it finds problems in the content of the virtual table.
// https://sqlite.org/vtab.html#xintegrity
@@ -518,7 +523,7 @@ func cursorFilterCallback(ctx context.Context, mod api.Module, pCur, idxNum, idx
args := callbackArgs(db, argc, argv)
var idxName string
if idxStr != 0 {
idxName = util.ReadString(mod, idxStr, _MAX_STRING)
idxName = util.ReadString(mod, idxStr, _MAX_NAME)
}
err := cursor.Filter(int(idxNum), idxName, args...)
return vtabError(ctx, mod, pCur, _CURSOR_ERROR, err)
@@ -567,11 +572,14 @@ func vtabError(ctx context.Context, mod api.Module, ptr, kind uint32, err error)
if msg != "" && ptr != 0 {
switch kind {
case _VTAB_ERROR:
ptr = ptr + 8
ptr = ptr + 8 // zErrMsg
case _CURSOR_ERROR:
ptr = util.ReadUint32(mod, ptr) + 8
ptr = util.ReadUint32(mod, ptr) + 8 // pVtab->zErrMsg
}
db := ctx.Value(connKey{}).(*Conn)
if ptr := util.ReadUint32(mod, ptr); ptr != 0 {
db.free(ptr)
}
util.WriteUint32(mod, ptr, db.newString(msg))
}
return code

View File

@@ -16,7 +16,7 @@ func ExampleCreateModule() {
defer db.Close()
err = sqlite3.CreateModule[seriesTable](db, "generate_series", nil,
func(db *sqlite3.Conn, arg ...string) (seriesTable, error) {
func(db *sqlite3.Conn, module, schema, table string, arg ...string) (seriesTable, error) {
err := db.DeclareVtab(`CREATE TABLE x(value, start HIDDEN, stop HIDDEN, step HIDDEN)`)
return seriesTable{}, err
})