mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-12 22:19:14 +00:00
Compare commits
28 Commits
v0.10.3
...
gormlite/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
831a34a4c4 | ||
|
|
7c820ede3c | ||
|
|
089a0c0670 | ||
|
|
8b45cac16b | ||
|
|
06d2ff6752 | ||
|
|
987f0f13a2 | ||
|
|
cd40213898 | ||
|
|
8a0baedc10 | ||
|
|
c667a1f469 | ||
|
|
9c562f5d8b | ||
|
|
d862f47d95 | ||
|
|
a9e32fd3f0 | ||
|
|
b262f5cd01 | ||
|
|
4160b9a4bb | ||
|
|
dbaf2d99cd | ||
|
|
3f05115cd7 | ||
|
|
9bf14becaf | ||
|
|
997e197f54 | ||
|
|
b81fe284b6 | ||
|
|
269306c5c8 | ||
|
|
8a18243830 | ||
|
|
fcd6cc91d8 | ||
|
|
c1838fc0bc | ||
|
|
dc0d8236bf | ||
|
|
ba18facb0d | ||
|
|
5653efa70e | ||
|
|
1acb95917a | ||
|
|
e31a42fb22 |
40
.github/workflows/cpu.yml
vendored
Normal file
40
.github/workflows/cpu.yml
vendored
Normal 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 ./...
|
||||
2
.github/workflows/cross.sh
vendored
2
.github/workflows/cross.sh
vendored
@@ -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 .
|
||||
4
.github/workflows/cross.yml
vendored
4
.github/workflows/cross.yml
vendored
@@ -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
23
.github/workflows/repro.sh
vendored
Executable 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
24
.github/workflows/repro.yml
vendored
Normal 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
|
||||
@@ -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
|
||||
|
||||
14
backup.go
14
backup.go
@@ -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
36
blob.go
@@ -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
50
conn.go
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
const.go
25
const.go
@@ -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
|
||||
|
||||
46
context.go
46
context.go
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
149
driver/driver.go
149
driver/driver.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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.
5
error.go
5
error.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package csv
|
||||
import "testing"
|
||||
|
||||
func Test_uintParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
arg string
|
||||
key string
|
||||
|
||||
@@ -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(')')
|
||||
|
||||
@@ -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
128
ext/lines/lines.go
Normal 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
186
ext/lines/lines_test.go
Normal 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
267
ext/pivot/pivot.go
Normal 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
219
ext/pivot/pivot_test.go
Normal 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
210
ext/statement/stmt.go
Normal 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
145
ext/statement/stmt_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
49
func.go
@@ -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))
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
249
sqlite.go
@@ -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)
|
||||
|
||||
@@ -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 ~-
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
168
stmt.go
@@ -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
|
||||
}
|
||||
|
||||
58
stmt_test.go
58
stmt_test.go
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -58,6 +58,7 @@ func TestJSON(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
want := []string{
|
||||
"null", "1", "3.141592653589793",
|
||||
|
||||
@@ -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
BIN
tests/testdata/utf16be.db
vendored
Normal file
Binary file not shown.
2
time.go
2
time.go
@@ -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
|
||||
|
||||
|
||||
47
value.go
47
value.go
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
28
vfs/memdb/memdb_test.go
Normal 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
BIN
vfs/memdb/testdata/wal.db
vendored
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
2
vfs/tests/mptest/testdata/build.sh
vendored
2
vfs/tests/mptest/testdata/build.sh
vendored
@@ -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 \
|
||||
|
||||
4
vfs/tests/mptest/testdata/mptest.wasm.bz2
vendored
4
vfs/tests/mptest/testdata/mptest.wasm.bz2
vendored
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:74294bf19d213056ef5ffb7a980c3a7de5d029d0621ded53394d3055dfc4f604
|
||||
size 513604
|
||||
oid sha256:8d26f7fa2d68ba43e7d36433cfb875586f2bf7b4244ec5f97d031d042688b28d
|
||||
size 512278
|
||||
|
||||
2
vfs/tests/speedtest1/testdata/build.sh
vendored
2
vfs/tests/speedtest1/testdata/build.sh
vendored
@@ -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 \
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:966754393264cc43eb931ece22941d0d607e7e776e26c26b548209d2264d01a1
|
||||
size 527530
|
||||
oid sha256:3c581db6af92832b16476c0b0d8bbdd18211f11712dcf5b7b728f24397fadfea
|
||||
size 526471
|
||||
|
||||
15
vfs/vfs.go
15
vfs/vfs.go
@@ -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
|
||||
|
||||
@@ -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
50
vtab.go
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user