Compare commits

..

47 Commits

Author SHA1 Message Date
Nuno Cruces
f2545534af Rename. 2024-10-25 14:20:52 +01:00
Nuno Cruces
69e5cf706b Checksums in default VFS. (#177) 2024-10-25 13:49:06 +01:00
Nuno Cruces
75c1dbb052 Checksum VFS. (#176) 2024-10-25 00:12:29 +01:00
Nuno Cruces
64e2500ca8 SQLite 3.47.0. 2024-10-24 00:34:54 +01:00
Nuno Cruces
0cd0f48365 Rename WAL, fixes. 2024-10-24 00:23:13 +01:00
Nuno Cruces
c69ee0fe8d Rename sql3util. 2024-10-22 23:36:38 +01:00
Nuno Cruces
b9b489aae9 Fixes, internal docs. 2024-10-22 13:08:31 +01:00
Nuno Cruces
21de004779 VFS API refactor. 2024-10-21 15:07:30 +01:00
Nuno Cruces
9eec439d35 More tests. 2024-10-19 08:58:55 +01:00
Nuno Cruces
fefee692db VFS utilities. 2024-10-18 18:07:48 +01:00
Nuno Cruces
f18561ee11 Lerp. 2024-10-18 13:07:39 +01:00
Nuno Cruces
ace01b2927 Split local imports. 2024-10-18 12:20:32 +01:00
Nuno Cruces
89f750a6e9 Wrap FilePersistentWAL. 2024-10-18 12:14:31 +01:00
Nuno Cruces
d6aebe67cc AES-XTS VFS (#171)
Co-authored-by: Ben Krieger <ben.krieger@intel.com>
2024-10-17 23:53:39 +01:00
Nuno Cruces
714ea0e779 Blocking locks improvements. 2024-10-17 15:39:01 +01:00
Nuno Cruces
c900889848 Configure memory, 32-bit WAL. (#170) 2024-10-17 13:04:23 +01:00
Nuno Cruces
50c8517603 Window percentil. 2024-10-17 08:15:44 +01:00
Nuno Cruces
c78d00dca0 Better percentile compatibility. 2024-10-16 14:00:22 +01:00
Nuno Cruces
ddfaf12cd8 Reorg READMEs. 2024-10-15 14:51:11 +01:00
Nuno Cruces
368c900db8 Test latest snapshot. 2024-10-10 12:58:32 +01:00
Nuno Cruces
e524fb185d More consistent interrupts. 2024-10-08 16:38:57 +01:00
Nuno Cruces
cc7bacfb9c Remove rangefunc experiment. 2024-10-07 15:13:19 +01:00
Nuno Cruces
911e497891 Improved quoting. 2024-10-07 14:42:09 +01:00
Nuno Cruces
3469460635 Improved error handling. 2024-10-07 13:22:31 +01:00
Nuno Cruces
b5adcacec4 Don't panic on memory commit failure. (#154) 2024-10-06 00:06:43 +01:00
dependabot[bot]
62d6712f82 Bump github.com/tetratelabs/wazero from 1.8.0 to 1.8.1 (#166)
Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/tetratelabs/wazero/releases)
- [Commits](https://github.com/tetratelabs/wazero/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/tetratelabs/wazero
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-06 00:02:33 +01:00
dependabot[bot]
d34e6197a8 Bump golang.org/x/crypto from 0.27.0 to 0.28.0 (#163)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.28.0.
- [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-04 23:39:43 +01:00
Nuno Cruces
f9e867be60 Time fix. 2024-10-04 18:29:35 +01:00
Nuno Cruces
96c61a2f55 Improved interrupts. 2024-10-04 16:18:24 +01:00
Nuno Cruces
ac94a5406e Clear bindings. 2024-10-04 00:35:04 +01:00
Nuno Cruces
34617e15f0 Savepoint quoting. 2024-10-03 12:01:54 +01:00
Nuno Cruces
83b3f6ce0a Time fix. 2024-10-02 12:59:59 +01:00
Nuno Cruces
f2c8aa0ddf Scanning improvements. 2024-10-02 12:59:49 +01:00
Nuno Cruces
63ea13e41e Fix examples. 2024-10-02 12:01:22 +01:00
Nuno Cruces
b1508bface Faster stats. 2024-10-01 15:16:06 +01:00
Nuno Cruces
1c6897c8e2 Multi-value Wasm. 2024-09-30 23:45:33 +01:00
Nuno Cruces
170e1dbebd Don't override LIKE. 2024-09-30 14:04:23 +01:00
Nuno Cruces
25fc5a606a Delete functions, modules, hooks. 2024-09-30 13:11:04 +01:00
Nuno Cruces
8b6c2b28fb More BCE. 2024-09-30 11:03:50 +01:00
kim
e59e2ed2a2 use "len(buf)-1" to access final slice index to eliminate boundary-checks (#160) 2024-09-29 20:01:51 +01:00
Nuno Cruces
505c6640c2 Unaccent, title case. 2024-09-29 20:01:14 +01:00
Nuno Cruces
5b0a063bfe More BCE. 2024-09-28 10:37:47 +01:00
kim
32931032d3 add compiler hints to reduce number of slice bounds checks (#158)
* add compiler hints to reduce number of slice bounds checks

* panic on assert instead of returning error for code coverage
2024-09-28 01:26:46 +01:00
Nuno Cruces
b7055ef04b Spellfix1, fix #157. 2024-09-27 16:28:17 +01:00
Nuno Cruces
167025f47a On demand collations. 2024-09-27 16:28:17 +01:00
Nuno Cruces
b4b50fc547 Context aware busy handler. (#146) 2024-09-27 12:33:57 +01:00
Nuno Cruces
08f7764fe0 Improved OPEN_NOFOLLOW. 2024-09-27 12:32:11 +01:00
170 changed files with 2743 additions and 1101 deletions

View File

@@ -27,8 +27,8 @@ embed/build.sh
embed/bcw2/build.sh
# Download and build sqlite-createtable-parser
util/vtabutil/parse/download.sh
util/vtabutil/parse/build.sh
util/sql3util/parse/download.sh
util/sql3util/parse/build.sh
# Check diffs
git diff --exit-code

View File

@@ -31,4 +31,4 @@ jobs:
subject-path: |
embed/sqlite3.wasm
embed/bcw2/bcw2.wasm
util/vtabutil/parse/sql3parse_table.wasm
util/sql3util/parse/sql3parse_table.wasm

View File

@@ -3,6 +3,10 @@ name: Test
on:
push:
branches: [ "main" ]
paths:
- '**.go'
- '**.wasm'
- '**.wasm.bz2'
pull_request:
branches: [ "main" ]
workflow_dispatch:
@@ -36,7 +40,7 @@ jobs:
run: go mod verify
- name: Vet
run: go vet -tags vet ./...
run: go vet ./...
- name: Build
run: go build -v ./...
@@ -61,9 +65,7 @@ jobs:
run: gormlite/test.sh
- name: Collect coverage
run: |
go install github.com/dave/courtney@latest
courtney
run: go run github.com/dave/courtney@latest
if: |
github.event_name == 'push' &&
matrix.os == 'ubuntu-latest'
@@ -156,7 +158,7 @@ jobs:
- name: Test ppc64le (interpreter)
run: GOARCH=ppc64le go test -v -short ./...
- name: Test s390x (big-endian, z/OS demo)
- name: Test s390x (big-endian, z/OS like)
run: GOARCH=s390x go test -v -short -tags sqlite3_flock ./...
test-vm:

View File

@@ -1,4 +1,4 @@
# Go bindings to SQLite using Wazero
# Go bindings to SQLite using wazero
[![Go Reference](https://pkg.go.dev/badge/image)](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
[![Go Report](https://goreportcard.com/badge/github.com/ncruces/go-sqlite3)](https://goreportcard.com/report/github.com/ncruces/go-sqlite3)
@@ -41,45 +41,6 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
- [`github.com/ncruces/go-sqlite3/gormlite`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/gormlite)
provides a [GORM](https://gorm.io) driver.
### Extensions
- [`github.com/ncruces/go-sqlite3/ext/array`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/array)
provides the [`array`](https://sqlite.org/carray.html) table-valued function.
- [`github.com/ncruces/go-sqlite3/ext/blobio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blobio)
simplifies [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html).
- [`github.com/ncruces/go-sqlite3/ext/bloom`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/bloom)
provides a [Bloom filter](https://github.com/nalgeon/sqlean/issues/27#issuecomment-1002267134) virtual table.
- [`github.com/ncruces/go-sqlite3/ext/closure`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/closure)
provides a transitive closure virtual table.
- [`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/fileio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/fileio)
reads, writes and lists files.
- [`github.com/ncruces/go-sqlite3/ext/hash`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/hash)
provides cryptographic hash functions.
- [`github.com/ncruces/go-sqlite3/ext/lines`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/lines)
reads data [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/regexp`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/regexp)
provides regular expression functions.
- [`github.com/ncruces/go-sqlite3/ext/statement`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/statement)
creates [parameterized views](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](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html) functions.
- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions.
- [`github.com/ncruces/go-sqlite3/ext/uuid`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/uuid)
generates [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier).
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
maps multidimensional data to one dimension.
- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)
wraps a VFS to offer encryption at rest.
- [`github.com/ncruces/go-sqlite3/vfs/memdb`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb)
implements an in-memory VFS.
- [`github.com/ncruces/go-sqlite3/vfs/readervfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs)
implements a VFS for immutable databases.
### Advanced features
- [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html)
@@ -92,7 +53,11 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
- [math functions](https://sqlite.org/lang_mathfunc.html)
- [full-text search](https://sqlite.org/fts5.html)
- [geospatial search](https://sqlite.org/geopoly.html)
- [Unicode support](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
- [statistics functions](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
- [encryption at rest](vfs/adiantum/README.md)
- [many extensions](ext/README.md)
- [custom VFSes](vfs/README.md#custom-vfses)
- [and more…](embed/README.md)
### Caveats

View File

@@ -31,7 +31,6 @@ 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.mark()()
blobPtr := c.arena.new(ptrlen)
dbPtr := c.arena.string(db)
@@ -43,6 +42,7 @@ func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob,
flags = 1
}
c.checkInterrupt(c.handle)
r := c.call("sqlite3_blob_open", uint64(c.handle),
uint64(dbPtr), uint64(tablePtr), uint64(columnPtr),
uint64(row), flags, uint64(blobPtr))
@@ -253,6 +253,7 @@ func (b *Blob) Seek(offset int64, whence int) (int64, error) {
//
// https://sqlite.org/c3ref/blob_reopen.html
func (b *Blob) Reopen(row int64) error {
b.c.checkInterrupt(b.c.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

143
config.go
View File

@@ -2,10 +2,12 @@ package sqlite3
import (
"context"
"strconv"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/vfs"
"github.com/tetratelabs/wazero/api"
)
// Config makes configuration changes to a database connection.
@@ -15,8 +17,19 @@ import (
//
// https://sqlite.org/c3ref/db_config.html
func (c *Conn) Config(op DBConfig, arg ...bool) (bool, error) {
if op < DBCONFIG_ENABLE_FKEY || op > DBCONFIG_REVERSE_SCANORDER {
return false, MISUSE
}
// We need to call sqlite3_db_config, a variadic function.
// We only support the `int int*` variants.
// The int is a three-valued bool: -1 queries, 0/1 sets false/true.
// The int* points to where new state will be written to.
// The vararg is a pointer to an array containing these arguments:
// an int and an int* pointing to that int.
defer c.arena.mark()()
argsPtr := c.arena.new(2 * ptrlen)
argsPtr := c.arena.new(intlen + ptrlen)
var flag int
switch {
@@ -63,18 +76,23 @@ func logCallback(ctx context.Context, mod api.Module, _, iCode, zMsg uint32) {
// https://sqlite.org/c3ref/file_control.html
func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, error) {
defer c.arena.mark()()
ptr := c.arena.new(max(ptrlen, intlen))
var schemaPtr uint32
if schema != "" {
schemaPtr = c.arena.string(schema)
}
var rc uint64
var res any
switch op {
default:
return nil, MISUSE
case FCNTL_RESET_CACHE:
r := c.call("sqlite3_file_control",
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), 0)
return nil, c.error(r)
case FCNTL_PERSIST_WAL, FCNTL_POWERSAFE_OVERWRITE:
var flag int
@@ -84,70 +102,69 @@ func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, erro
case arg[0]:
flag = 1
}
ptr := c.arena.new(4)
util.WriteUint32(c.mod, ptr, uint32(flag))
r := c.call("sqlite3_file_control",
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
return util.ReadUint32(c.mod, ptr) != 0, c.error(r)
res = util.ReadUint32(c.mod, ptr) != 0
case FCNTL_CHUNK_SIZE:
ptr := c.arena.new(4)
util.WriteUint32(c.mod, ptr, uint32(arg[0].(int)))
r := c.call("sqlite3_file_control",
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
return nil, c.error(r)
case FCNTL_RESERVE_BYTES:
bytes := -1
if len(arg) > 0 {
bytes = arg[0].(int)
}
ptr := c.arena.new(4)
util.WriteUint32(c.mod, ptr, uint32(bytes))
r := c.call("sqlite3_file_control",
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
return int(util.ReadUint32(c.mod, ptr)), c.error(r)
res = int(util.ReadUint32(c.mod, ptr))
case FCNTL_DATA_VERSION:
ptr := c.arena.new(4)
r := c.call("sqlite3_file_control",
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
return util.ReadUint32(c.mod, ptr), c.error(r)
res = util.ReadUint32(c.mod, ptr)
case FCNTL_LOCKSTATE:
ptr := c.arena.new(4)
r := c.call("sqlite3_file_control",
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
return vfs.LockLevel(util.ReadUint32(c.mod, ptr)), c.error(r)
res = vfs.LockLevel(util.ReadUint32(c.mod, ptr))
case FCNTL_VFS_POINTER:
ptr := c.arena.new(4)
r := c.call("sqlite3_file_control",
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
const zNameOffset = 16
ptr = util.ReadUint32(c.mod, ptr)
ptr = util.ReadUint32(c.mod, ptr+zNameOffset)
name := util.ReadString(c.mod, ptr, _MAX_NAME)
return vfs.Find(name), c.error(r)
if rc == _OK {
const zNameOffset = 16
ptr = util.ReadUint32(c.mod, ptr)
ptr = util.ReadUint32(c.mod, ptr+zNameOffset)
name := util.ReadString(c.mod, ptr, _MAX_NAME)
res = vfs.Find(name)
}
case FCNTL_FILE_POINTER, FCNTL_JOURNAL_POINTER:
ptr := c.arena.new(4)
r := c.call("sqlite3_file_control",
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
const fileHandleOffset = 4
ptr = util.ReadUint32(c.mod, ptr)
ptr = util.ReadUint32(c.mod, ptr+fileHandleOffset)
return util.GetHandle(c.ctx, ptr), c.error(r)
if rc == _OK {
const fileHandleOffset = 4
ptr = util.ReadUint32(c.mod, ptr)
ptr = util.ReadUint32(c.mod, ptr+fileHandleOffset)
res = util.GetHandle(c.ctx, ptr)
}
}
return nil, MISUSE
if err := c.error(rc); err != nil {
return nil, err
}
return res, nil
}
// Limit allows the size of various constructs to be
@@ -234,10 +251,10 @@ func traceCallback(ctx context.Context, mod api.Module, evt TraceEvent, pDB, pAr
return rc
}
// WalCheckpoint checkpoints a WAL database.
// WALCheckpoint checkpoints a WAL database.
//
// https://sqlite.org/c3ref/wal_checkpoint_v2.html
func (c *Conn) WalCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt int, err error) {
func (c *Conn) WALCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt int, err error) {
defer c.arena.mark()()
nLogPtr := c.arena.new(ptrlen)
nCkptPtr := c.arena.new(ptrlen)
@@ -250,19 +267,19 @@ func (c *Conn) WalCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt in
return nLog, nCkpt, c.error(r)
}
// WalAutoCheckpoint configures WAL auto-checkpoints.
// WALAutoCheckpoint configures WAL auto-checkpoints.
//
// https://sqlite.org/c3ref/wal_autocheckpoint.html
func (c *Conn) WalAutoCheckpoint(pages int) error {
func (c *Conn) WALAutoCheckpoint(pages int) error {
r := c.call("sqlite3_wal_autocheckpoint", uint64(c.handle), uint64(pages))
return c.error(r)
}
// WalHook registers a callback function to be invoked
// WALHook registers a callback function to be invoked
// each time data is committed to a database in WAL mode.
//
// https://sqlite.org/c3ref/wal_hook.html
func (c *Conn) WalHook(cb func(db *Conn, schema string, pages int) error) {
func (c *Conn) WALHook(cb func(db *Conn, schema string, pages int) error) {
var enable uint64
if cb != nil {
enable = 1
@@ -284,7 +301,10 @@ func walCallback(ctx context.Context, mod api.Module, _, pDB, zSchema uint32, pa
//
// https://sqlite.org/c3ref/autovacuum_pages.html
func (c *Conn) AutoVacuumPages(cb func(schema string, dbPages, freePages, bytesPerPage uint) uint) error {
funcPtr := util.AddHandle(c.ctx, cb)
var funcPtr uint32
if cb != nil {
funcPtr = util.AddHandle(c.ctx, cb)
}
r := c.call("sqlite3_autovacuum_pages_go", uint64(c.handle), uint64(funcPtr))
return c.error(r)
}
@@ -308,3 +328,46 @@ func (c *Conn) SoftHeapLimit(n int64) int64 {
func (c *Conn) HardHeapLimit(n int64) int64 {
return int64(c.call("sqlite3_hard_heap_limit64", uint64(n)))
}
// EnableChecksums enables checksums on a database.
//
// https://sqlite.org/cksumvfs.html
func (c *Conn) EnableChecksums(schema string) error {
r, err := c.FileControl(schema, FCNTL_RESERVE_BYTES)
if err != nil {
return err
}
if r == 8 {
// Correct value, enabled.
return nil
}
if r == 0 {
// Default value, enable.
_, err = c.FileControl(schema, FCNTL_RESERVE_BYTES, 8)
if err != nil {
return err
}
r, err = c.FileControl(schema, FCNTL_RESERVE_BYTES)
if err != nil {
return err
}
}
if r != 8 {
// Invalid value.
return util.ErrorString("sqlite3: reserve bytes must be 8, is: " + strconv.Itoa(r.(int)))
}
// VACUUM the database.
if schema != "" {
err = c.Exec(`VACUUM ` + QuoteIdentifier(schema))
} else {
err = c.Exec(`VACUUM`)
}
if err != nil {
return err
}
// Checkpoint the WAL.
_, _, err = c.WALCheckpoint(schema, CHECKPOINT_RESTART)
return err
}

108
conn.go
View File

@@ -8,9 +8,10 @@ import (
"strings"
"time"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/vfs"
"github.com/tetratelabs/wazero/api"
)
// Conn is a database connection handle.
@@ -24,7 +25,7 @@ type Conn struct {
pending *Stmt
stmts []*Stmt
timer *time.Timer
busy func(int) bool
busy func(context.Context, int) bool
log func(xErrorCode, string)
collation func(*Conn, string)
wal func(*Conn, string, int) error
@@ -38,14 +39,20 @@ type Conn struct {
handle uint32
}
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE], [OPEN_URI] and [OPEN_NOFOLLOW].
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE] and [OPEN_URI].
func Open(filename string) (*Conn, error) {
return newConn(filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI|OPEN_NOFOLLOW)
return newConn(context.Background(), filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
}
// OpenContext is like [Open] but includes a context,
// which is used to interrupt the process of opening the connectiton.
func OpenContext(ctx context.Context, filename string) (*Conn, error) {
return newConn(ctx, filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
}
// OpenFlags opens an SQLite database file as specified by the filename argument.
//
// If none of the required flags is used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
// If none of the required flags are used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
// If a URI filename is used, PRAGMA statements to execute can be specified using "_pragma":
//
// sqlite3.Open("file:demo.db?_pragma=busy_timeout(10000)")
@@ -55,25 +62,33 @@ func OpenFlags(filename string, flags OpenFlag) (*Conn, error) {
if flags&(OPEN_READONLY|OPEN_READWRITE|OPEN_CREATE) == 0 {
flags |= OPEN_READWRITE | OPEN_CREATE
}
return newConn(filename, flags)
return newConn(context.Background(), filename, flags)
}
type connKey struct{}
func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
sqlite, err := instantiateSQLite()
func newConn(ctx context.Context, filename string, flags OpenFlag) (res *Conn, _ error) {
err := ctx.Err()
if err != nil {
return nil, err
}
c := &Conn{interrupt: ctx}
c.sqlite, err = instantiateSQLite()
if err != nil {
return nil, err
}
defer func() {
if conn == nil {
sqlite.close()
if res == nil {
c.Close()
c.sqlite.close()
} else {
c.interrupt = context.Background()
}
}()
c := &Conn{sqlite: sqlite}
c.arena = c.newArena(1024)
c.ctx = context.WithValue(c.ctx, connKey{}, c)
c.arena = c.newArena(1024)
c.handle, err = c.openDB(filename, flags)
if err == nil {
err = initExtensions(c)
@@ -98,6 +113,7 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
return 0, err
}
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
if flags|OPEN_URI != 0 && strings.HasPrefix(filename, "file:") {
var pragmas strings.Builder
if _, after, ok := strings.Cut(filename, "?"); ok {
@@ -109,6 +125,7 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
}
}
if pragmas.Len() != 0 {
c.checkInterrupt(handle)
pragmaPtr := c.arena.string(pragmas.String())
r := c.call("sqlite3_exec", uint64(handle), uint64(pragmaPtr), 0, 0, 0)
if err := c.sqlite.error(r, handle, pragmas.String()); err != nil {
@@ -118,7 +135,6 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
}
}
}
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
return handle, nil
}
@@ -160,10 +176,10 @@ func (c *Conn) Close() error {
//
// https://sqlite.org/c3ref/exec.html
func (c *Conn) Exec(sql string) error {
c.checkInterrupt()
defer c.arena.mark()()
sqlPtr := c.arena.string(sql)
c.checkInterrupt(c.handle)
r := c.call("sqlite3_exec", uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
return c.error(r, sql)
}
@@ -189,6 +205,7 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
tailPtr := c.arena.new(ptrlen)
sqlPtr := c.arena.string(sql)
c.checkInterrupt(c.handle)
r := c.call("sqlite3_prepare_v3", uint64(c.handle),
uint64(sqlPtr), uint64(len(sql)+1), uint64(flags),
uint64(stmtPtr), uint64(tailPtr))
@@ -301,8 +318,7 @@ func (c *Conn) ReleaseMemory() error {
return c.error(r)
}
// GetInterrupt gets the context set with [Conn.SetInterrupt],
// or nil if none was set.
// GetInterrupt gets the context set with [Conn.SetInterrupt].
func (c *Conn) GetInterrupt() context.Context {
return c.interrupt
}
@@ -322,9 +338,11 @@ func (c *Conn) GetInterrupt() context.Context {
//
// https://sqlite.org/c3ref/interrupt.html
func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
// Is it the same context?
if ctx == c.interrupt {
return ctx
old = c.interrupt
c.interrupt = ctx
if ctx == old || ctx.Done() == old.Done() {
return old
}
// A busy SQL statement prevents SQLite from ignoring an interrupt
@@ -333,32 +351,29 @@ func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
defer c.arena.mark()()
stmtPtr := c.arena.new(ptrlen)
loopPtr := c.arena.string(`WITH RECURSIVE c(x) AS (VALUES(0) UNION ALL SELECT x FROM c) SELECT x FROM c`)
c.call("sqlite3_prepare_v3", uint64(c.handle), uint64(loopPtr), math.MaxUint64, 0, uint64(stmtPtr), 0)
c.call("sqlite3_prepare_v3", uint64(c.handle), uint64(loopPtr), math.MaxUint64,
uint64(PREPARE_PERSISTENT), uint64(stmtPtr), 0)
c.pending = &Stmt{c: c}
c.pending.handle = util.ReadUint32(c.mod, stmtPtr)
}
old = c.interrupt
c.interrupt = ctx
if old != nil && old.Done() != nil && (ctx == nil || ctx.Err() == nil) {
if old.Done() != nil && ctx.Err() == nil {
c.pending.Reset()
}
if ctx != nil && ctx.Done() != nil {
if ctx.Done() != nil {
c.pending.Step()
}
return old
}
func (c *Conn) checkInterrupt() {
if c.interrupt != nil && c.interrupt.Err() != nil {
c.call("sqlite3_interrupt", uint64(c.handle))
func (c *Conn) checkInterrupt(handle uint32) {
if c.interrupt.Err() != nil {
c.call("sqlite3_interrupt", uint64(handle))
}
}
func progressCallback(ctx context.Context, mod api.Module, pDB uint32) (interrupt uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB &&
c.interrupt != nil && c.interrupt.Err() != nil {
func progressCallback(ctx context.Context, mod api.Module, _ uint32) (interrupt uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.interrupt.Err() != nil {
interrupt = 1
}
return interrupt
@@ -373,9 +388,8 @@ func (c *Conn) BusyTimeout(timeout time.Duration) error {
return c.error(r)
}
func timeoutCallback(ctx context.Context, mod api.Module, pDB uint32, count, tmout int32) (retry uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok &&
(c.interrupt == nil || c.interrupt.Err() == nil) {
func timeoutCallback(ctx context.Context, mod api.Module, count, tmout int32) (retry uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.interrupt.Err() == nil {
const delays = "\x01\x02\x05\x0a\x0f\x14\x19\x19\x19\x32\x32\x64"
const totals = "\x00\x01\x03\x08\x12\x21\x35\x4e\x67\x80\xb2\xe4"
const ndelay = int32(len(delays) - 1)
@@ -391,7 +405,7 @@ func timeoutCallback(ctx context.Context, mod api.Module, pDB uint32, count, tmo
if delay = min(delay, tmout-prior); delay > 0 {
delay := time.Duration(delay) * time.Millisecond
if c.interrupt == nil || c.interrupt.Done() == nil {
if c.interrupt.Done() == nil {
time.Sleep(delay)
return 1
}
@@ -414,7 +428,7 @@ func timeoutCallback(ctx context.Context, mod api.Module, pDB uint32, count, tmo
// BusyHandler registers a callback to handle [BUSY] errors.
//
// https://sqlite.org/c3ref/busy_handler.html
func (c *Conn) BusyHandler(cb func(count int) (retry bool)) error {
func (c *Conn) BusyHandler(cb func(ctx context.Context, count int) (retry bool)) error {
var enable uint64
if cb != nil {
enable = 1
@@ -428,9 +442,12 @@ func (c *Conn) BusyHandler(cb func(count int) (retry bool)) error {
}
func busyCallback(ctx context.Context, mod api.Module, pDB uint32, count int32) (retry uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil &&
(c.interrupt == nil || c.interrupt.Err() == nil) {
if c.busy(int(count)) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil {
interrupt := c.interrupt
if interrupt == nil {
interrupt = context.Background()
}
if interrupt.Err() == nil && c.busy(interrupt, int(count)) {
retry = 1
}
}
@@ -442,8 +459,8 @@ func busyCallback(ctx context.Context, mod api.Module, pDB uint32, count int32)
// https://sqlite.org/c3ref/db_status.html
func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int, err error) {
defer c.arena.mark()()
hiPtr := c.arena.new(4)
curPtr := c.arena.new(4)
hiPtr := c.arena.new(intlen)
curPtr := c.arena.new(intlen)
var i uint64
if reset {
@@ -469,8 +486,8 @@ func (c *Conn) TableColumnMetadata(schema, table, column string) (declType, coll
declTypePtr := c.arena.new(ptrlen)
collSeqPtr := c.arena.new(ptrlen)
notNullPtr := c.arena.new(ptrlen)
primaryKeyPtr := c.arena.new(ptrlen)
autoIncPtr := c.arena.new(ptrlen)
primaryKeyPtr := c.arena.new(ptrlen)
if schema != "" {
schemaPtr = c.arena.string(schema)
}
@@ -504,10 +521,3 @@ func (c *Conn) stmtsIter(yield func(*Stmt) bool) {
}
}
}
// DriverConn is implemented by the SQLite [database/sql] driver connection.
//
// Deprecated: use [github.com/ncruces/go-sqlite3/driver.Conn] instead.
type DriverConn interface {
Raw() *Conn
}

View File

@@ -1,4 +1,4 @@
//go:build (go1.23 || goexperiment.rangefunc) && !vet
//go:build go1.23
package sqlite3

View File

@@ -1,4 +1,4 @@
//go:build !(go1.23 || goexperiment.rangefunc) || vet
//go:build !go1.23
package sqlite3

View File

@@ -13,6 +13,7 @@ const (
_MAX_FUNCTION_ARG = 100
ptrlen = 4
intlen = 4
)
// ErrorCode is a result code that [Error.Code] might return.
@@ -177,6 +178,7 @@ const (
DETERMINISTIC FunctionFlag = 0x000000800
DIRECTONLY FunctionFlag = 0x000080000
INNOCUOUS FunctionFlag = 0x000200000
SELFORDER1 FunctionFlag = 0x002000000
// SUBTYPE FunctionFlag = 0x000100000
// RESULT_SUBTYPE FunctionFlag = 0x001000000
)
@@ -245,6 +247,7 @@ const (
DBCONFIG_TRUSTED_SCHEMA DBConfig = 1017
DBCONFIG_STMT_SCANSTATUS DBConfig = 1018
DBCONFIG_REVERSE_SCANORDER DBConfig = 1019
// DBCONFIG_MAX DBConfig = 1019
)
// FcntlOpcode are the available opcodes for [Conn.FileControl].

View File

@@ -40,14 +40,14 @@
// When using a custom time struct, you'll have to implement
// [database/sql/driver.Valuer] and [database/sql.Scanner].
//
// The Value method should ideally serialise to a time [format] supported by SQLite.
// The Value method should ideally encode to a time [format] supported by SQLite.
// This ensures SQL date and time functions work as they should,
// and that your schema works with other SQLite tools.
// [sqlite3.TimeFormat.Encode] may help.
//
// The Scan method needs to take into account that the value it receives can be of differing types.
// It can already be a [time.Time], if the driver decoded the value according to "_timefmt" rules.
// Or it can be a: string, int64, float64, []byte, nil,
// Or it can be a: string, int64, float64, []byte, or nil,
// depending on the column type and what whoever wrote the value.
// [sqlite3.TimeFormat.Decode] may help.
//
@@ -202,19 +202,19 @@ func (n *connector) Driver() driver.Driver {
return n.driver
}
func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) {
c := &conn{
txLock: n.txLock,
tmRead: n.tmRead,
tmWrite: n.tmWrite,
}
c.Conn, err = sqlite3.Open(n.name)
c.Conn, err = sqlite3.OpenContext(ctx, n.name)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
if res == nil {
c.Close()
}
}()
@@ -239,6 +239,7 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
if err != nil {
return nil, err
}
defer s.Close()
if s.Step() && s.ColumnBool(0) {
c.readOnly = '1'
} else {
@@ -466,6 +467,7 @@ func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (drive
defer s.Stmt.Conn().SetInterrupt(old)
err = s.Stmt.Exec()
s.Stmt.ClearBindings()
if err != nil {
return nil, err
}
@@ -488,7 +490,7 @@ func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
if arg.Name == "" {
ids = append(ids, arg.Ordinal)
} else {
for _, prefix := range []string{":", "@", "$"} {
for _, prefix := range [...]string{":", "@", "$"} {
if id := s.Stmt.BindIndex(prefix + arg.Name); id != 0 {
ids = append(ids, id)
}
@@ -522,9 +524,9 @@ func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
default:
panic(util.AssertErr())
}
}
if err != nil {
return err
if err != nil {
return err
}
}
}
return nil
@@ -595,10 +597,11 @@ func (r *rows) Close() error {
func (r *rows) Columns() []string {
if r.names == nil {
count := r.Stmt.ColumnCount()
r.names = make([]string, count)
for i := range r.names {
r.names[i] = r.Stmt.ColumnName(i)
names := make([]string, count)
for i := range names {
names[i] = r.Stmt.ColumnName(i)
}
r.names = names
}
return r.names
}
@@ -606,26 +609,29 @@ func (r *rows) Columns() []string {
func (r *rows) loadTypes() {
if r.nulls == nil {
count := r.Stmt.ColumnCount()
r.nulls = make([]bool, count)
r.types = make([]string, count)
for i := range r.nulls {
nulls := make([]bool, count)
types := make([]string, count)
for i := range nulls {
if col := r.Stmt.ColumnOriginName(i); col != "" {
r.types[i], _, r.nulls[i], _, _, _ = r.Stmt.Conn().TableColumnMetadata(
types[i], _, nulls[i], _, _, _ = r.Stmt.Conn().TableColumnMetadata(
r.Stmt.ColumnDatabaseName(i),
r.Stmt.ColumnTableName(i),
col)
}
}
r.nulls = nulls
r.types = types
}
}
func (r *rows) declType(index int) string {
if r.types == nil {
count := r.Stmt.ColumnCount()
r.types = make([]string, count)
for i := range r.types {
r.types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
types := make([]string, count)
for i := range types {
types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
}
r.types = types
}
return r.types[index]
}
@@ -665,27 +671,23 @@ func (r *rows) Next(dest []driver.Value) error {
for i := range dest {
if t, ok := r.decodeTime(i, dest[i]); ok {
dest[i] = t
continue
}
if s, ok := dest[i].(string); ok {
t, ok := maybeTime(s)
if ok {
dest[i] = t
}
}
}
return err
}
func (r *rows) decodeTime(i int, v any) (_ time.Time, ok bool) {
switch r.tmRead {
case sqlite3.TimeFormatDefault, time.RFC3339Nano:
// handled by maybeTime
return
}
switch v.(type) {
case int64, float64, string:
switch v := v.(type) {
case int64, float64:
// could be a time value
case string:
if r.tmWrite != "" && r.tmWrite != time.RFC3339 && r.tmWrite != time.RFC3339Nano {
break
}
t, ok := maybeTime(v)
if ok {
return t, true
}
default:
return
}

144
driver/example2_test.go Normal file
View File

@@ -0,0 +1,144 @@
//go:build (linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock) && !sqlite3_nosys
package driver_test
// Adapted from: https://go.dev/doc/tutorial/database-access
import (
"database/sql"
"database/sql/driver"
"fmt"
"log"
"time"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Example_customTime() {
db, err := sql.Open("sqlite3", "file:/time.db?vfs=memdb")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE data (
id INTEGER PRIMARY KEY,
date_time TEXT
) STRICT;
`)
if err != nil {
log.Fatal(err)
}
// This one will be returned as string to [sql.Scanner] because it doesn't
// pass the driver's round-trip test when it tries to figure out if it's
// a time. 2009-11-17T20:34:58.650Z goes in, but parsing and formatting
// it with [time.RFC3338Nano] results in 2009-11-17T20:34:58.65Z. Though
// the times are identical, the trailing zero is lost in the string
// representation so the driver considers the conversion unsuccessful.
c1 := CustomTime{time.Date(
2009, 11, 17, 20, 34, 58, 650000000, time.UTC)}
// Store our custom time in the database.
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c1)
if err != nil {
log.Fatal(err)
}
var strc1 string
// Retrieve it as a string, the result of Value().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&strc1)
if err != nil {
log.Fatal(err)
}
fmt.Println("in db:", strc1)
var resc1 CustomTime
// Retrieve it as our custom time type, going through Scan().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&resc1)
if err != nil {
log.Fatal(err)
}
fmt.Println("custom time:", resc1)
// This one will be returned as [time.Time] to [sql.Scanner] because it does
// pass the driver's round-trip test when it tries to figure out if it's
// a time. 2009-11-17T20:34:58.651Z goes in, and parsing and formatting
// it with [time.RFC3339Nano] results in 2009-11-17T20:34:58.651Z.
c2 := CustomTime{time.Date(
2009, 11, 17, 20, 34, 58, 651000000, time.UTC)}
// Store our custom time in the database.
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c2)
if err != nil {
log.Fatal(err)
}
var strc2 string
// Retrieve it as a string, the result of Value().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&strc2)
if err != nil {
log.Fatal(err)
}
fmt.Println("in db:", strc2)
var resc2 CustomTime
// Retrieve it as our custom time type, going through Scan().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&resc2)
if err != nil {
log.Fatal(err)
}
fmt.Println("custom time:", resc2)
// Output:
// in db: 2009-11-17T20:34:58.650Z
// scan type string: 2009-11-17T20:34:58.650Z
// custom time: 2009-11-17 20:34:58.65 +0000 UTC
// in db: 2009-11-17T20:34:58.651Z
// scan type time: 2009-11-17 20:34:58.651 +0000 UTC
// custom time: 2009-11-17 20:34:58.651 +0000 UTC
}
type CustomTime struct{ time.Time }
func (c CustomTime) Value() (driver.Value, error) {
return sqlite3.TimeFormat7TZ.Encode(c.UTC()), nil
}
func (c *CustomTime) Scan(value any) error {
switch v := value.(type) {
case nil:
*c = CustomTime{time.Time{}}
case time.Time:
fmt.Println("scan type time:", v)
*c = CustomTime{v}
case string:
fmt.Println("scan type string:", v)
t, err := sqlite3.TimeFormat7TZ.Decode(v)
if err != nil {
return err
}
*c = CustomTime{t}
default:
panic("unsupported value type")
}
return nil
}

View File

@@ -6,13 +6,10 @@ package driver_test
import (
"database/sql"
"database/sql/driver"
"fmt"
"log"
"os"
"time"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
@@ -153,129 +150,3 @@ func addAlbum(alb Album) (int64, error) {
}
return id, nil
}
func Example_customTime() {
db, err := sql.Open("sqlite3", "file:/time.db?vfs=memdb")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE data (
id INTEGER PRIMARY KEY,
date_time TEXT
) STRICT;
`)
if err != nil {
log.Fatal(err)
}
// This one will be returned as string to [sql.Scanner] because it doesn't
// pass the driver's round-trip test when it tries to figure out if it's
// a time. 2009-11-17T20:34:58.650Z goes in, but parsing and formatting
// it with [time.RFC3338Nano] results in 2009-11-17T20:34:58.65Z. Though
// the times are identical, the trailing zero is lost in the string
// representation so the driver considers the conversion unsuccessful.
c1 := CustomTime{time.Date(
2009, 11, 17, 20, 34, 58, 650000000, time.UTC)}
// Store our custom time in the database.
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c1)
if err != nil {
log.Fatal(err)
}
var strc1 string
// Retrieve it as a string, the result of Value().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&strc1)
if err != nil {
log.Fatal(err)
}
fmt.Println("in db:", strc1)
var resc1 CustomTime
// Retrieve it as our custom time type, going through Scan().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&resc1)
if err != nil {
log.Fatal(err)
}
fmt.Println("custom time:", resc1)
// This one will be returned as [time.Time] to [sql.Scanner] because it does
// pass the driver's round-trip test when it tries to figure out if it's
// a time. 2009-11-17T20:34:58.651Z goes in, and parsing and formatting
// it with [time.RFC3339Nano] results in 2009-11-17T20:34:58.651Z.
c2 := CustomTime{time.Date(
2009, 11, 17, 20, 34, 58, 651000000, time.UTC)}
// Store our custom time in the database.
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c2)
if err != nil {
log.Fatal(err)
}
var strc2 string
// Retrieve it as a string, the result of Value().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&strc2)
if err != nil {
log.Fatal(err)
}
fmt.Println("in db:", strc2)
var resc2 CustomTime
// Retrieve it as our custom time type, going through Scan().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&resc2)
if err != nil {
log.Fatal(err)
}
fmt.Println("custom time:", resc2)
// Output:
// in db: 2009-11-17T20:34:58.650Z
// scan type string: 2009-11-17T20:34:58.650Z
// custom time: 2009-11-17 20:34:58.65 +0000 UTC
// in db: 2009-11-17T20:34:58.651Z
// scan type time: 2009-11-17 20:34:58.651 +0000 UTC
// custom time: 2009-11-17 20:34:58.651 +0000 UTC
}
type CustomTime struct{ time.Time }
func (c CustomTime) Value() (driver.Value, error) {
return sqlite3.TimeFormat7TZ.Encode(c.UTC()), nil
}
func (c *CustomTime) Scan(value any) error {
switch v := value.(type) {
case nil:
*c = CustomTime{time.Time{}}
case time.Time:
fmt.Println("scan type time:", v)
*c = CustomTime{v}
case string:
fmt.Println("scan type string:", v)
t, err := sqlite3.TimeFormat7TZ.Decode(v)
if err != nil {
return err
}
*c = CustomTime{t}
default:
panic("unsupported value type")
}
return nil
}

View File

@@ -1,6 +1,6 @@
# Embeddable Wasm build of SQLite
This folder includes an embeddable Wasm build of SQLite 3.46.1 for use with
This folder includes an embeddable Wasm build of SQLite 3.47.0 for use with
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
The following optional features are compiled in:
@@ -9,6 +9,7 @@ The following optional features are compiled in:
- [JSON](https://sqlite.org/json1.html)
- [R*Tree](https://sqlite.org/rtree.html)
- [GeoPoly](https://sqlite.org/geopoly.html)
- [Spellfix1](https://sqlite.org/spellfix1.html)
- [soundex](https://sqlite.org/lang_corefunc.html#soundex)
- [stat4](https://sqlite.org/compile.html#enable_stat4)
- [base64](https://github.com/sqlite/sqlite/blob/master/ext/misc/base64.c)
@@ -35,6 +36,6 @@ You can use your own custom build of SQLite.
Examples of custom builds of SQLite are:
- [`github.com/ncruces/go-sqlite3/embed/bcw2`](https://github.com/ncruces/go-sqlite3/tree/main/embed/bcw2)
built from a branch supporting [`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md)
and [Wal2](https://www.sqlite.org/cgi/src/doc/wal2/doc/wal2.md).
and [Wal2](https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md).
- [`github.com/asg017/sqlite-vec-go-bindings/ncruces`](https://github.com/asg017/sqlite-vec-go-bindings)
which includes the [`sqlite-vec`](https://github.com/asg017/sqlite-vec) vector search extension.

View File

@@ -1,8 +1,8 @@
# Embeddable Wasm build of SQLite
This folder includes an embeddable Wasm build of SQLite 3.46.1, including the experimental
This folder includes an embeddable Wasm build of SQLite 3.47.0, including the experimental
[`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md) and
[Wal2](https://www.sqlite.org/cgi/src/doc/wal2/doc/wal2.md) patches.
[Wal2](https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md) patches.
> [!IMPORTANT]
> This package is experimental.

Binary file not shown.

View File

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

View File

@@ -13,7 +13,8 @@ mkdir -p build/ext/
cp "$ROOT"/sqlite3/*.[ch] build/
cp "$ROOT"/sqlite3/*.patch build/
curl -# https://www.sqlite.org/src/tarball/sqlite.tar.gz?r=bedrock-3.46 | tar xz
# https://sqlite.org/src/info/d2d954d43abe20a3
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=d2d954d4 | tar xz
cd sqlite
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
@@ -33,6 +34,7 @@ mv sqlite/ext/misc/decimal.c build/ext/
mv sqlite/ext/misc/ieee754.c build/ext/
mv sqlite/ext/misc/regexp.c build/ext/
mv sqlite/ext/misc/series.c build/ext/
mv sqlite/ext/misc/spellfix.c build/ext/
mv sqlite/ext/misc/uint.c build/ext/
cd build
@@ -44,7 +46,7 @@ cd ~-
-o bcw2.wasm "build/main.c" \
-I"build" \
-mexec-model=reactor \
-matomics -msimd128 -mmutable-globals \
-matomics -msimd128 -mmutable-globals -mmultivalue \
-mbulk-memory -mreference-types \
-mnontrapping-fptoint -msign-ext \
-fno-stack-protector -fno-stack-clash-protection \

View File

@@ -6,7 +6,7 @@
// import _ "github.com/ncruces/go-sqlite3/embed/bcw2"
//
// [BEGIN CONCURRENT]: https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md
// [Wal2]: https://www.sqlite.org/cgi/src/doc/wal2/doc/wal2.md
// [Wal2]: https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md
package bcw2
import (

View File

@@ -14,7 +14,7 @@ trap 'rm -f sqlite3.tmp' EXIT
-o sqlite3.wasm "$ROOT/sqlite3/main.c" \
-I"$ROOT/sqlite3" \
-mexec-model=reactor \
-matomics -msimd128 -mmutable-globals \
-matomics -msimd128 -mmutable-globals -mmultivalue \
-mbulk-memory -mreference-types \
-mnontrapping-fptoint -msign-ext \
-fno-stack-protector -fno-stack-clash-protection \

View File

@@ -51,6 +51,7 @@ sqlite3_create_collation_go
sqlite3_create_function_go
sqlite3_create_module_go
sqlite3_create_window_function_go
sqlite3_data_count
sqlite3_database_file_object
sqlite3_db_cacheflush
sqlite3_db_config

View File

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

Binary file not shown.

37
ext/README.md Normal file
View File

@@ -0,0 +1,37 @@
# Go SQLite Extensions
This folder collects optional SQLite extensions
you can load into your database connections.
### Extensions
- [`github.com/ncruces/go-sqlite3/ext/array`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/array)
provides the [`array`](https://sqlite.org/carray.html) table-valued function.
- [`github.com/ncruces/go-sqlite3/ext/blobio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blobio)
simplifies [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html).
- [`github.com/ncruces/go-sqlite3/ext/bloom`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/bloom)
provides a [Bloom filter](https://github.com/nalgeon/sqlean/issues/27#issuecomment-1002267134) virtual table.
- [`github.com/ncruces/go-sqlite3/ext/closure`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/closure)
provides a transitive closure virtual table.
- [`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/fileio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/fileio)
reads, writes and lists files.
- [`github.com/ncruces/go-sqlite3/ext/hash`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/hash)
provides cryptographic hash functions.
- [`github.com/ncruces/go-sqlite3/ext/lines`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/lines)
reads data [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/regexp`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/regexp)
provides regular expression functions.
- [`github.com/ncruces/go-sqlite3/ext/statement`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/statement)
creates [parameterized views](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](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html) functions.
- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions.
- [`github.com/ncruces/go-sqlite3/ext/uuid`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/uuid)
generates [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier).
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
maps multidimensional data to one dimension.

View File

@@ -13,6 +13,7 @@ import (
"strconv"
"github.com/dchest/siphash"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
@@ -34,7 +35,7 @@ type bloom struct {
}
func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom, err error) {
t := bloom{
b := bloom{
db: db,
schema: schema,
storage: table + "_storage",
@@ -54,30 +55,30 @@ func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom,
}
if len(arg) > 1 {
t.prob, err = strconv.ParseFloat(arg[1], 64)
b.prob, err = strconv.ParseFloat(arg[1], 64)
if err != nil {
return nil, err
}
if t.prob <= 0 || t.prob >= 1 {
if b.prob <= 0 || b.prob >= 1 {
return nil, util.ErrorString("bloom: probability must be in the range (0,1)")
}
} else {
t.prob = 0.01
b.prob = 0.01
}
if len(arg) > 2 {
t.hashes, err = strconv.Atoi(arg[2])
b.hashes, err = strconv.Atoi(arg[2])
if err != nil {
return nil, err
}
if t.hashes <= 0 {
if b.hashes <= 0 {
return nil, util.ErrorString("bloom: number of hash functions must be positive")
}
} else {
t.hashes = max(1, numHashes(t.prob))
b.hashes = max(1, numHashes(b.prob))
}
t.bytes = numBytes(nelem, t.prob)
b.bytes = numBytes(nelem, b.prob)
err = db.DeclareVTab(
`CREATE TABLE x(present, word HIDDEN NOT NULL PRIMARY KEY) WITHOUT ROWID`)
@@ -87,7 +88,7 @@ func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom,
err = db.Exec(fmt.Sprintf(
`CREATE TABLE %s.%s (data BLOB, p REAL, n INTEGER, m INTEGER, k INTEGER)`,
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage)))
sqlite3.QuoteIdentifier(b.schema), sqlite3.QuoteIdentifier(b.storage)))
if err != nil {
return nil, err
}
@@ -98,17 +99,17 @@ func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom,
err = db.Exec(fmt.Sprintf(
`INSERT INTO %s.%s (rowid, data, p, n, m, k)
VALUES (1, zeroblob(%d), %f, %d, %d, %d)`,
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage),
t.bytes, t.prob, nelem, 8*t.bytes, t.hashes))
sqlite3.QuoteIdentifier(b.schema), sqlite3.QuoteIdentifier(b.storage),
b.bytes, b.prob, nelem, 8*b.bytes, b.hashes))
if err != nil {
t.Destroy()
b.Destroy()
return nil, err
}
return &t, nil
return &b, nil
}
func connect(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom, err error) {
t := bloom{
b := bloom{
db: db,
schema: schema,
storage: table + "_storage",
@@ -122,7 +123,7 @@ func connect(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom
load, _, err := db.Prepare(fmt.Sprintf(
`SELECT m/8, p, k FROM %s.%s WHERE rowid = 1`,
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage)))
sqlite3.QuoteIdentifier(b.schema), sqlite3.QuoteIdentifier(b.storage)))
if err != nil {
return nil, err
}
@@ -135,10 +136,10 @@ func connect(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom
return nil, sqlite3.CORRUPT_VTAB
}
t.bytes = load.ColumnInt64(0)
t.prob = load.ColumnFloat(1)
t.hashes = load.ColumnInt(2)
return &t, nil
b.bytes = load.ColumnInt64(0)
b.prob = load.ColumnFloat(1)
b.hashes = load.ColumnInt(2)
return &b, nil
}
func (b *bloom) Destroy() error {

Binary file not shown.

View File

@@ -12,7 +12,7 @@ import (
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/vtabutil"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
const (
@@ -39,17 +39,17 @@ func Register(db *sqlite3.Conn) error {
)
for _, arg := range arg {
key, val := vtabutil.NamedArg(arg)
key, val := sql3util.NamedArg(arg)
if done.Contains(key) {
return nil, fmt.Errorf("transitive_closure: more than one %q parameter", key)
}
switch key {
case "tablename":
table = vtabutil.Unquote(val)
table = sql3util.Unquote(val)
case "idcolumn":
column = vtabutil.Unquote(val)
column = sql3util.Unquote(val)
case "parentcolumn":
parent = vtabutil.Unquote(val)
parent = sql3util.Unquote(val)
default:
return nil, fmt.Errorf("transitive_closure: unknown %q parameter", key)
}
@@ -92,8 +92,10 @@ func (c *closure) BestIndex(idx *sqlite3.IndexInfo) error {
case sqlite3.INDEX_CONSTRAINT_EQ:
plan |= 1
cost /= 100
idx.ConstraintUsage[i].ArgvIndex = 1
idx.ConstraintUsage[i].Omit = true
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
ArgvIndex: 1,
Omit: true,
}
}
continue
}
@@ -116,8 +118,10 @@ func (c *closure) BestIndex(idx *sqlite3.IndexInfo) error {
plan |= posi << 8
cost /= 5
posi += 1
idx.ConstraintUsage[i].ArgvIndex = posi
idx.ConstraintUsage[i].Omit = true
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
ArgvIndex: posi,
Omit: true,
}
}
continue
}
@@ -126,8 +130,10 @@ func (c *closure) BestIndex(idx *sqlite3.IndexInfo) error {
case sqlite3.INDEX_CONSTRAINT_EQ:
plan |= posi << 12
posi += 1
idx.ConstraintUsage[i].ArgvIndex = posi
idx.ConstraintUsage[i].Omit = true
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
ArgvIndex: posi,
Omit: true,
}
}
continue
}
@@ -136,8 +142,10 @@ func (c *closure) BestIndex(idx *sqlite3.IndexInfo) error {
case sqlite3.INDEX_CONSTRAINT_EQ:
plan |= posi << 16
posi += 1
idx.ConstraintUsage[i].ArgvIndex = posi
idx.ConstraintUsage[i].Omit = true
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
ArgvIndex: posi,
Omit: true,
}
}
continue
}

View File

@@ -4,8 +4,7 @@ import (
"fmt"
"strconv"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/vtabutil"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
func uintArg(key, val string) (int, error) {
@@ -20,7 +19,7 @@ func boolArg(key, val string) (bool, error) {
if val == "" {
return true, nil
}
b, ok := util.ParseBool(val)
b, ok := sql3util.ParseBool(val)
if ok {
return b, nil
}
@@ -28,7 +27,7 @@ func boolArg(key, val string) (bool, error) {
}
func runeArg(key, val string) (rune, error) {
r, _, tail, err := strconv.UnquoteChar(vtabutil.Unquote(val), 0)
r, _, tail, err := strconv.UnquoteChar(sql3util.Unquote(val), 0)
if tail != "" || err != nil {
return 0, fmt.Errorf("csv: invalid %q parameter: %s", key, val)
}

View File

@@ -3,7 +3,7 @@ package csv
import (
"testing"
"github.com/ncruces/go-sqlite3/util/vtabutil"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
func Test_uintArg(t *testing.T) {
@@ -24,7 +24,7 @@ func Test_uintArg(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.arg, func(t *testing.T) {
key, val := vtabutil.NamedArg(tt.arg)
key, val := sql3util.NamedArg(tt.arg)
if key != tt.key {
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
}
@@ -62,7 +62,7 @@ func Test_boolArg(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.arg, func(t *testing.T) {
key, val := vtabutil.NamedArg(tt.arg)
key, val := sql3util.NamedArg(tt.arg)
if key != tt.key {
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
}
@@ -96,7 +96,7 @@ func Test_runeArg(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.arg, func(t *testing.T) {
key, val := vtabutil.NamedArg(tt.arg)
key, val := sql3util.NamedArg(tt.arg)
if key != tt.key {
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
}

View File

@@ -18,7 +18,7 @@ import (
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/osutil"
"github.com/ncruces/go-sqlite3/util/vtabutil"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
// Register registers the CSV virtual table.
@@ -30,7 +30,7 @@ func Register(db *sqlite3.Conn) error {
// RegisterFS registers the CSV virtual table.
// If a filename is specified, fsys is used to open the file.
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (res *table, err error) {
var (
filename string
data string
@@ -44,17 +44,17 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
)
for _, arg := range arg {
key, val := vtabutil.NamedArg(arg)
key, val := sql3util.NamedArg(arg)
if done.Contains(key) {
return nil, fmt.Errorf("csv: more than one %q parameter", key)
}
switch key {
case "filename":
filename = vtabutil.Unquote(val)
filename = sql3util.Unquote(val)
case "data":
data = vtabutil.Unquote(val)
data = sql3util.Unquote(val)
case "schema":
schema = vtabutil.Unquote(val)
schema = sql3util.Unquote(val)
case "header":
header, err = boolArg(key, val)
case "columns":
@@ -76,7 +76,7 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
return nil, util.ErrorString(`csv: must specify either "filename" or "data" but not both`)
}
table := &table{
t := &table{
fsys: fsys,
name: filename,
data: data,
@@ -88,7 +88,7 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
if schema == "" {
var row []string
if header || columns < 0 {
csv, c, err := table.newReader()
csv, c, err := t.newReader()
defer c.Close()
if err != nil {
return nil, err
@@ -100,22 +100,20 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
}
schema = getSchema(header, columns, row)
} else {
defer func() {
if err == nil {
table.typs, err = getColumnAffinities(schema)
}
}()
t.typs, err = getColumnAffinities(schema)
if err != nil {
return nil, err
}
}
err = db.DeclareVTab(schema)
if err == nil {
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
}
if err != nil {
return nil, err
}
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
if err != nil {
return nil, err
}
return table, nil
return t, nil
}
return sqlite3.CreateModule(db, "csv", declare, declare)

View File

@@ -3,7 +3,7 @@ package csv
import (
"strings"
"github.com/ncruces/go-sqlite3/util/vtabutil"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
type affinity byte
@@ -17,13 +17,14 @@ const (
)
func getColumnAffinities(schema string) ([]affinity, error) {
tab, err := vtabutil.Parse(schema)
tab, err := sql3util.ParseTable(schema)
if err != nil {
return nil, err
}
types := make([]affinity, len(tab.Columns))
for i, col := range tab.Columns {
columns := tab.Columns
types := make([]affinity, len(columns))
for i, col := range columns {
types[i] = getAffinity(col.Type)
}
return types, nil

View File

@@ -1,4 +1,4 @@
//go:build !(go1.23 || goexperiment.rangefunc) || vet
//go:build !go1.23
package fileio

View File

@@ -31,7 +31,9 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
db.CreateFunction("lsmode", 1, sqlite3.DETERMINISTIC, lsmode),
sqlite3.CreateModule(db, "fsdir", nil, func(db *sqlite3.Conn, _, _, _ string, _ ...string) (fsdir, error) {
err := db.DeclareVTab(`CREATE TABLE x(name,mode,mtime TIMESTAMP,data,path HIDDEN,dir HIDDEN)`)
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
if err == nil {
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
}
return fsdir{fsys}, err
}))
}

View File

@@ -1,4 +1,4 @@
//go:build !(go1.23 || goexperiment.rangefunc) || vet
//go:build !go1.23
package fileio

View File

@@ -1,4 +1,4 @@
//go:build (go1.23 || goexperiment.rangefunc) && !vet
//go:build go1.23
package fileio

View File

@@ -29,7 +29,7 @@ import (
// Register registers cryptographic hash functions for a database connection.
func Register(db *sqlite3.Conn) error {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
var errs util.ErrorJoiner
if crypto.MD4.Available() {

View File

@@ -7,15 +7,16 @@ import (
_ "crypto/sha512"
"testing"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
_ "golang.org/x/crypto/blake2b"
_ "golang.org/x/crypto/blake2s"
_ "golang.org/x/crypto/md4"
_ "golang.org/x/crypto/ripemd160"
_ "golang.org/x/crypto/sha3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func TestRegister(t *testing.T) {

View File

@@ -39,13 +39,17 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
sqlite3.CreateModule(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)
if err == nil {
err = db.VTabConfig(sqlite3.VTAB_INNOCUOUS)
}
return lines{}, err
}),
sqlite3.CreateModule(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)
if err == nil {
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
}
return lines{fsys}, err
}))
}

View File

@@ -25,15 +25,15 @@ type table struct {
cols []*sqlite3.Value
}
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (res *table, err error) {
if len(arg) != 3 {
return nil, fmt.Errorf("pivot: wrong number of arguments")
}
table := &table{db: db}
t := &table{db: db}
defer func() {
if err != nil {
table.Close()
if res == nil {
t.Close()
}
}()
@@ -42,17 +42,17 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
create.WriteString("CREATE TABLE x(")
// Row key query.
table.scan = "SELECT * FROM\n" + arg[0]
stmt, _, err := db.Prepare(table.scan)
t.scan = "SELECT * FROM\n" + arg[0]
stmt, _, err := db.Prepare(t.scan)
if err != nil {
return nil, err
}
defer stmt.Close()
table.keys = make([]string, stmt.ColumnCount())
for i := range table.keys {
t.keys = make([]string, stmt.ColumnCount())
for i := range t.keys {
name := sqlite3.QuoteIdentifier(stmt.ColumnName(i))
table.keys[i] = name
t.keys[i] = name
create.WriteString(sep)
create.WriteString(name)
sep = ","
@@ -70,15 +70,15 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
}
for stmt.Step() {
name := sqlite3.QuoteIdentifier(stmt.ColumnText(1))
table.cols = append(table.cols, stmt.ColumnValue(0).Dup())
t.cols = append(t.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)
t.cell = "SELECT * FROM\n" + arg[2]
stmt, _, err = db.Prepare(t.cell)
if err != nil {
return nil, err
}
@@ -86,8 +86,8 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
if stmt.ColumnCount() != 1 {
return nil, util.ErrorString("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)
if stmt.BindCount() != len(t.keys)+1 {
return nil, fmt.Errorf("pivot: cell query expects %d bound parameters", len(t.keys)+1)
}
create.WriteByte(')')
@@ -95,12 +95,12 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
if err != nil {
return nil, err
}
return table, nil
return t, nil
}
func (t *table) Close() error {
for i := range t.cols {
t.cols[i].Close()
for _, c := range t.cols {
c.Close()
}
return nil
}

View File

@@ -20,7 +20,7 @@ import (
// Register registers Unicode aware functions for a database connection.
func Register(db *sqlite3.Conn) error {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
return errors.Join(
db.CreateFunction("regexp", 2, flags, regex),
db.CreateFunction("regexp_like", 2, flags, regexLike),

View File

@@ -34,7 +34,7 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (*table, error) {
sql := "SELECT * FROM\n" + arg[0]
stmt, _, err := db.Prepare(sql)
stmt, _, err := db.PrepareFlags(sql, sqlite3.PREPARE_PERSISTENT)
if err != nil {
return nil, err
}

View File

@@ -26,21 +26,21 @@ func (b *boolean) Value(ctx sqlite3.Context) {
}
func (b *boolean) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
if arg[0].Type() == sqlite3.NULL {
return
}
if arg[0].Bool() {
a := arg[0]
if a.Bool() {
b.count++
}
b.total++
if a.Type() != sqlite3.NULL {
b.total++
}
}
func (b *boolean) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
if arg[0].Type() == sqlite3.NULL {
return
}
if arg[0].Bool() {
a := arg[0]
if a.Bool() {
b.count--
}
b.total--
if a.Type() != sqlite3.NULL {
b.total--
}
}

View File

@@ -13,6 +13,7 @@ import (
const (
median = iota
percentile_100
percentile_cont
percentile_disc
)
@@ -28,17 +29,25 @@ type percentile struct {
}
func (q *percentile) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
if a := arg[0]; a.NumericType() != sqlite3.NULL {
q.nums = append(q.nums, a.Float())
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
q.nums = append(q.nums, f)
}
if q.kind != median {
q.arg1 = append(q.arg1[:0], arg[1].RawText()...)
if q.kind != median && q.arg1 == nil {
q.arg1 = append(q.arg1, arg[1].RawText()...)
}
}
func (q *percentile) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
// Implementing inverse allows certain queries that don't really need it to succeed.
ctx.ResultError(util.ErrorString("percentile: may not be used as a window function"))
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
i := slices.Index(q.nums, f)
l := len(q.nums) - 1
q.nums[i] = q.nums[l]
q.nums = q.nums[:l]
}
}
func (q *percentile) Value(ctx sqlite3.Context) {
@@ -52,13 +61,13 @@ func (q *percentile) Value(ctx sqlite3.Context) {
floats []float64
)
if q.kind == median {
float, err = getPercentile(q.nums, 0.5, false)
float, err = q.at(0.5)
ctx.ResultFloat(float)
} else if err = json.Unmarshal(q.arg1, &float); err == nil {
float, err = getPercentile(q.nums, float, q.kind == percentile_disc)
float, err = q.at(float)
ctx.ResultFloat(float)
} else if err = json.Unmarshal(q.arg1, &floats); err == nil {
err = getPercentiles(q.nums, floats, q.kind == percentile_disc)
err = q.atMore(floats)
ctx.ResultJSON(floats)
}
if err != nil {
@@ -66,25 +75,28 @@ func (q *percentile) Value(ctx sqlite3.Context) {
}
}
func getPercentile(nums []float64, pos float64, disc bool) (float64, error) {
func (q *percentile) at(pos float64) (float64, error) {
if q.kind == percentile_100 {
pos = pos / 100
}
if pos < 0 || pos > 1 {
return 0, util.ErrorString("invalid pos")
}
i, f := math.Modf(pos * float64(len(nums)-1))
m0 := quick.Select(nums, int(i))
i, f := math.Modf(pos * float64(len(q.nums)-1))
m0 := quick.Select(q.nums, int(i))
if f == 0 || disc {
if f == 0 || q.kind == percentile_disc {
return m0, nil
}
m1 := slices.Min(nums[int(i)+1:])
return math.FMA(f, m1, math.FMA(-f, m0, m0)), nil
m1 := slices.Min(q.nums[int(i)+1:])
return util.Lerp(m0, m1, f), nil
}
func getPercentiles(nums []float64, pos []float64, disc bool) error {
func (q *percentile) atMore(pos []float64) error {
for i := range pos {
v, err := getPercentile(nums, pos[i], disc)
v, err := q.at(pos[i])
if err != nil {
return err
}

View File

@@ -31,6 +31,7 @@ func TestRegister_percentile(t *testing.T) {
stmt, _, err := db.Prepare(`
SELECT
median(x),
percentile(x, 50),
percentile_disc(x, 0.5),
percentile_cont(x, '[0.25, 0.5, 0.75]')
FROM data`)
@@ -41,11 +42,14 @@ func TestRegister_percentile(t *testing.T) {
if got := stmt.ColumnFloat(0); got != 10 {
t.Errorf("got %v, want 10", got)
}
if got := stmt.ColumnFloat(1); got != 7 {
if got := stmt.ColumnFloat(1); got != 10 {
t.Errorf("got %v, want 10", got)
}
if got := stmt.ColumnFloat(2); got != 7 {
t.Errorf("got %v, want 7", got)
}
var got []float64
if err := stmt.ColumnJSON(2, &got); err != nil {
if err := stmt.ColumnJSON(3, &got); err != nil {
t.Error(err)
}
if !slices.Equal(got, []float64{6.25, 10, 13.75}) {
@@ -54,9 +58,44 @@ func TestRegister_percentile(t *testing.T) {
}
stmt.Close()
stmt, _, err = db.Prepare(`
SELECT
median(x) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)
FROM data`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 5.5 {
t.Errorf("got %v, want 5.5", got)
}
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 7 {
t.Errorf("got %v, want 7", got)
}
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 10 {
t.Errorf("got %v, want 10", got)
}
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 14.5 {
t.Errorf("got %v, want 14.5", got)
}
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 16 {
t.Errorf("got %v, want 16", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`
SELECT
median(x),
percentile(x, 50),
percentile_disc(x, 0.5),
percentile_cont(x, '[0.25, 0.5, 0.75]')
FROM data
@@ -71,8 +110,11 @@ func TestRegister_percentile(t *testing.T) {
if got := stmt.ColumnFloat(1); got != 4 {
t.Errorf("got %v, want 4", got)
}
if got := stmt.ColumnFloat(2); got != 4 {
t.Errorf("got %v, want 4", got)
}
var got []float64
if err := stmt.ColumnJSON(2, &got); err != nil {
if err := stmt.ColumnJSON(3, &got); err != nil {
t.Error(err)
}
if !slices.Equal(got, []float64{4, 4, 4}) {
@@ -84,6 +126,7 @@ func TestRegister_percentile(t *testing.T) {
stmt, _, err = db.Prepare(`
SELECT
median(x),
percentile(x, 50),
percentile_disc(x, 0.5),
percentile_cont(x, '[0.25, 0.5, 0.75]')
FROM data
@@ -101,6 +144,9 @@ func TestRegister_percentile(t *testing.T) {
if got := stmt.ColumnType(2); got != sqlite3.NULL {
t.Error("want NULL")
}
if got := stmt.ColumnType(3); got != sqlite3.NULL {
t.Error("want NULL")
}
}
stmt.Close()

View File

@@ -52,7 +52,8 @@ import (
// Register registers statistics functions.
func Register(db *sqlite3.Conn) error {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
const order = sqlite3.SELFORDER1 | flags
return errors.Join(
db.CreateWindowFunction("var_pop", 1, flags, newVariance(var_pop)),
db.CreateWindowFunction("var_samp", 1, flags, newVariance(var_samp)),
@@ -71,9 +72,10 @@ func Register(db *sqlite3.Conn) error {
db.CreateWindowFunction("regr_intercept", 2, flags, newCovariance(regr_intercept)),
db.CreateWindowFunction("regr_count", 2, flags, newCovariance(regr_count)),
db.CreateWindowFunction("regr_json", 2, flags, newCovariance(regr_json)),
db.CreateWindowFunction("median", 1, flags, newPercentile(median)),
db.CreateWindowFunction("percentile_cont", 2, flags, newPercentile(percentile_cont)),
db.CreateWindowFunction("percentile_disc", 2, flags, newPercentile(percentile_disc)),
db.CreateWindowFunction("median", 1, order, newPercentile(median)),
db.CreateWindowFunction("percentile", 2, order, newPercentile(percentile_100)),
db.CreateWindowFunction("percentile_cont", 2, order, newPercentile(percentile_cont)),
db.CreateWindowFunction("percentile_disc", 2, order, newPercentile(percentile_disc)),
db.CreateWindowFunction("every", 1, flags, newBoolean(every)),
db.CreateWindowFunction("some", 1, flags, newBoolean(some)))
}
@@ -121,14 +123,18 @@ func (fn *variance) Value(ctx sqlite3.Context) {
}
func (fn *variance) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
if a := arg[0]; a.NumericType() != sqlite3.NULL {
fn.enqueue(a.Float())
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
fn.enqueue(f)
}
}
func (fn *variance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
if a := arg[0]; a.NumericType() != sqlite3.NULL {
fn.dequeue(a.Float())
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
fn.dequeue(f)
}
}
@@ -177,15 +183,23 @@ func (fn *covariance) Value(ctx sqlite3.Context) {
}
func (fn *covariance) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
a, b := arg[0], arg[1]
if a.NumericType() != sqlite3.NULL && b.NumericType() != sqlite3.NULL {
fn.enqueue(a.Float(), b.Float())
b, a := arg[1], arg[0] // avoid a bounds check
fa := a.Float()
fb := b.Float()
if true &&
(fa != 0.0 || a.NumericType() != sqlite3.NULL) &&
(fb != 0.0 || b.NumericType() != sqlite3.NULL) {
fn.enqueue(fa, fb)
}
}
func (fn *covariance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
a, b := arg[0], arg[1]
if a.NumericType() != sqlite3.NULL && b.NumericType() != sqlite3.NULL {
fn.dequeue(a.Float(), b.Float())
b, a := arg[1], arg[0] // avoid a bounds check
fa := a.Float()
fb := b.Float()
if true &&
(fa != 0.0 || a.NumericType() != sqlite3.NULL) &&
(fb != 0.0 || b.NumericType() != sqlite3.NULL) {
fn.dequeue(fa, fb)
}
}

View File

@@ -5,6 +5,10 @@
// - LIKE and REGEXP operators,
// - collation sequences.
//
// It also provides, from PostgreSQL:
// - unaccent(),
// - initcap().
//
// The implementation is not 100% compatible with the [ICU extension]:
// - upper() and lower() use [strings.ToUpper], [strings.ToLower] and [cases];
// - the LIKE operator follows [strings.EqualFold] rules;
@@ -21,26 +25,44 @@ import (
"errors"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
"golang.org/x/text/cases"
"golang.org/x/text/collate"
"golang.org/x/text/language"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Set RegisterLike to false to not register a Unicode aware LIKE operator.
// Overriding the built-in LIKE operator disables the [LIKE optimization].
//
// [LIKE optimization]: https://sqlite.org/optoverview.html#the_like_optimization
var RegisterLike = true
// Register registers Unicode aware functions for a database connection.
func Register(db *sqlite3.Conn) error {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
return errors.Join(
db.CreateFunction("like", 2, flags, like),
db.CreateFunction("like", 3, flags, like),
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
var errs util.ErrorJoiner
if RegisterLike {
errs.Join(
db.CreateFunction("like", 2, flags, like),
db.CreateFunction("like", 3, flags, like))
}
errs.Join(
db.CreateFunction("upper", 1, flags, upper),
db.CreateFunction("upper", 2, flags, upper),
db.CreateFunction("lower", 1, flags, lower),
db.CreateFunction("lower", 2, flags, lower),
db.CreateFunction("regexp", 2, flags, regex),
db.CreateFunction("initcap", 1, flags, initcap),
db.CreateFunction("initcap", 2, flags, initcap),
db.CreateFunction("unaccent", 1, flags, unaccent),
db.CreateFunction("icu_load_collation", 2, sqlite3.DIRECTONLY,
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
name := arg[1].Text()
@@ -48,12 +70,13 @@ func Register(db *sqlite3.Conn) error {
return
}
err := RegisterCollation(db, arg[0].Text(), name)
err := RegisterCollation(ctx.Conn(), arg[0].Text(), name)
if err != nil {
ctx.ResultError(err)
return // notest
}
}))
return errors.Join(errs...)
}
// RegisterCollation registers a Unicode collation sequence for a database connection.
@@ -65,6 +88,15 @@ func RegisterCollation(db *sqlite3.Conn, locale, name string) error {
return db.CreateCollation(name, collate.New(tag).Compare)
}
// RegisterCollationsNeeded registers Unicode collation sequences on demand for a database connection.
func RegisterCollationsNeeded(db *sqlite3.Conn) error {
return db.CollationNeeded(func(db *sqlite3.Conn, name string) {
if tag, err := language.Parse(name); err == nil {
db.CreateCollation(name, collate.New(tag).Compare)
}
})
}
func upper(ctx sqlite3.Context, arg ...sqlite3.Value) {
if len(arg) == 1 {
ctx.ResultRawText(bytes.ToUpper(arg[0].RawText()))
@@ -103,6 +135,35 @@ func lower(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
}
func initcap(ctx sqlite3.Context, arg ...sqlite3.Value) {
if len(arg) == 1 {
ctx.ResultRawText(bytes.Title(arg[0].RawText()))
return
}
cs, ok := ctx.GetAuxData(1).(cases.Caser)
if !ok {
t, err := language.Parse(arg[1].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
c := cases.Title(t)
ctx.SetAuxData(1, c)
cs = c
}
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
}
func unaccent(ctx sqlite3.Context, arg ...sqlite3.Value) {
unaccent := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
res, _, err := transform.Bytes(unaccent, arg[0].RawText())
if err != nil {
ctx.ResultError(err) // notest
} else {
ctx.ResultRawText(res)
}
}
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, ok := ctx.GetAuxData(0).(*regexp.Regexp)
if !ok {

View File

@@ -47,6 +47,9 @@ func TestRegister(t *testing.T) {
{`upper('istanbul', 'tr-TR')`, "İSTANBUL"},
{`lower('Dünyanın İlk Borsası', 'tr-TR')`, "dünyanın ilk borsası"},
{`upper('Dünyanın İlk Borsası', 'tr-TR')`, "DÜNYANIN İLK BORSASI"},
{`initcap('Kad je hladno Marko nosi džemper')`, "Kad Je Hladno Marko Nosi Džemper"},
{`initcap('Kad je hladno Marko nosi džemper', 'hr-HR')`, "Kad Je Hladno Marko Nosi Džemper"},
{`unaccent('Hôtel')`, "Hotel"},
{`'Hello' REGEXP 'ell'`, "1"},
{`'Hello' REGEXP 'el.'`, "1"},
{`'Hello' LIKE 'hel_'`, "0"},
@@ -92,7 +95,7 @@ func TestRegister_collation(t *testing.T) {
t.Fatal(err)
}
err = db.Exec(`SELECT icu_load_collation('fr_FR', 'french')`)
err = db.Exec(`SELECT icu_load_collation('fr-FR', 'french')`)
if err != nil {
t.Fatal(err)
}
@@ -127,6 +130,57 @@ func TestRegister_collation(t *testing.T) {
}
}
func TestRegisterCollationsNeeded(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
RegisterCollationsNeeded(db)
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`INSERT INTO words (word) VALUES ('côte'), ('cote'), ('coter'), ('coté'), ('cotée'), ('côté')`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT word FROM words ORDER BY word COLLATE fr_FR`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
got, want := []string{}, []string{"cote", "coté", "côte", "côté", "cotée", "coter"}
for stmt.Step() {
got = append(got, stmt.ColumnText(0))
}
if err := stmt.Err(); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, want) {
t.Error("not equal")
}
err = stmt.Close()
if err != nil {
t.Fatal(err)
}
err = db.Close()
if err != nil {
t.Fatal(err)
}
}
func TestRegister_error(t *testing.T) {
t.Parallel()

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"github.com/google/uuid"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
@@ -27,7 +28,7 @@ import (
//
// Converts a UUID into a 16-byte blob.
func Register(db *sqlite3.Conn) error {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
return errors.Join(
db.CreateFunction("uuid", 0, sqlite3.INNOCUOUS, generate),
db.CreateFunction("uuid", 1, sqlite3.INNOCUOUS, generate),

View File

@@ -4,6 +4,7 @@ import (
"testing"
"github.com/google/uuid"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"

View File

@@ -12,7 +12,7 @@ import (
// Register registers the zorder and unzorder SQL functions.
func Register(db *sqlite3.Conn) error {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
return errors.Join(
db.CreateFunction("zorder", -1, flags, zorder),
db.CreateFunction("unzorder", 3, flags, unzorder))
@@ -47,9 +47,9 @@ func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
func unzorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
z := arg[0].Int64()
n := arg[1].Int64()
i := arg[2].Int64()
n := arg[1].Int64()
z := arg[0].Int64()
var k int
var x int64

31
func.go
View File

@@ -4,8 +4,9 @@ import (
"context"
"sync"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
)
// CollationNeeded registers a callback to be invoked
@@ -33,16 +34,23 @@ func (c *Conn) CollationNeeded(cb func(db *Conn, name string)) error {
// one or more unknown collating sequences.
func (c Conn) AnyCollationNeeded() error {
r := c.call("sqlite3_anycollseq_init", uint64(c.handle), 0, 0)
return c.error(r)
if err := c.error(r); err != nil {
return err
}
c.collation = nil
return nil
}
// 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 {
var funcPtr uint32
defer c.arena.mark()()
namePtr := c.arena.string(name)
funcPtr := util.AddHandle(c.ctx, fn)
if fn != nil {
funcPtr = util.AddHandle(c.ctx, fn)
}
r := c.call("sqlite3_create_collation_go",
uint64(c.handle), uint64(namePtr), uint64(funcPtr))
return c.error(r)
@@ -52,9 +60,12 @@ func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
//
// https://sqlite.org/c3ref/create_function.html
func (c *Conn) CreateFunction(name string, nArg int, flag FunctionFlag, fn ScalarFunction) error {
var funcPtr uint32
defer c.arena.mark()()
namePtr := c.arena.string(name)
funcPtr := util.AddHandle(c.ctx, fn)
if fn != nil {
funcPtr = util.AddHandle(c.ctx, fn)
}
r := c.call("sqlite3_create_function_go",
uint64(c.handle), uint64(namePtr), uint64(nArg),
uint64(flag), uint64(funcPtr))
@@ -71,10 +82,13 @@ type ScalarFunction func(ctx Context, arg ...Value)
//
// https://sqlite.org/c3ref/create_function.html
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn func() AggregateFunction) error {
var funcPtr uint32
defer c.arena.mark()()
call := "sqlite3_create_aggregate_function_go"
namePtr := c.arena.string(name)
funcPtr := util.AddHandle(c.ctx, fn)
if fn != nil {
funcPtr = util.AddHandle(c.ctx, fn)
}
call := "sqlite3_create_aggregate_function_go"
if _, ok := fn().(WindowFunction); ok {
call = "sqlite3_create_window_function_go"
}
@@ -184,11 +198,12 @@ func callbackAggregate(db *Conn, pAgg, pApp uint32) (AggregateFunction, uint32)
// We need to create the aggregate.
fn := util.GetHandle(db.ctx, pApp).(func() AggregateFunction)()
handle := util.AddHandle(db.ctx, fn)
if pAgg != 0 {
handle := util.AddHandle(db.ctx, fn)
util.WriteUint32(db.mod, pAgg, handle)
return fn, handle
}
return fn, handle
return fn, 0
}
func callbackArgs(db *Conn, arg []Value, pArg uint32) {

8
go.mod
View File

@@ -10,11 +10,11 @@ require (
github.com/ncruces/julianday v1.0.0
github.com/ncruces/sort v0.1.2
github.com/psanford/httpreadat v0.1.0
github.com/tetratelabs/wazero v1.8.0
golang.org/x/crypto v0.27.0
github.com/tetratelabs/wazero v1.8.1
golang.org/x/crypto v0.28.0
golang.org/x/sync v0.8.0
golang.org/x/sys v0.25.0
golang.org/x/text v0.18.0
golang.org/x/sys v0.26.0
golang.org/x/text v0.19.0
lukechampine.com/adiantum v1.1.1
)

16
go.sum
View File

@@ -8,15 +8,15 @@ github.com/ncruces/sort v0.1.2 h1:zKQ9CA4fpHPF6xsUhRTfi5EEryspuBpe/QA4VWQOV1U=
github.com/ncruces/sort v0.1.2/go.mod h1:vEJUTBJtebIuCMmXD18GKo5GJGhsay+xZFOoBEIXFmE=
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=

View File

@@ -1,10 +1,13 @@
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=

View File

@@ -3,8 +3,9 @@ package gormlite
import (
"errors"
"github.com/ncruces/go-sqlite3"
"gorm.io/gorm"
"github.com/ncruces/go-sqlite3"
)
func (_Dialector) Translate(err error) error {

View File

@@ -3,9 +3,10 @@ package gormlite
import (
"testing"
"github.com/ncruces/go-sqlite3/vfs/memdb"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func TestErrorTranslator(t *testing.T) {

View File

@@ -13,7 +13,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
)

View File

@@ -6,11 +6,11 @@ github.com/ncruces/go-sqlite3 v0.18.4 h1:Je8o3y33MDwPYY/Cacas8yCsuoUzpNY/AgoSlN2
github.com/ncruces/go-sqlite3 v0.18.4/go.mod h1:4HLag13gq1k10s4dfGBhMfRVsssJRT9/5hYqVM9RUYo=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

View File

@@ -4,6 +4,21 @@ package alloc
import "github.com/tetratelabs/wazero/experimental"
func Virtual(cap, max uint64) experimental.LinearMemory {
return Slice(cap, max)
func NewMemory(cap, max uint64) experimental.LinearMemory {
return &sliceMemory{make([]byte, 0, cap)}
}
type sliceMemory struct {
buf []byte
}
func (b *sliceMemory) Free() {}
func (b *sliceMemory) Reallocate(size uint64) []byte {
if cap := uint64(cap(b.buf)); size > cap {
b.buf = append(b.buf[:cap], make([]byte, size-cap)...)
} else {
b.buf = b.buf[:size]
}
return b.buf
}

View File

@@ -1,24 +0,0 @@
//go:build !(darwin || linux) || !(amd64 || arm64 || riscv64 || ppc64le) || sqlite3_noshm || sqlite3_nosys
package alloc
import "github.com/tetratelabs/wazero/experimental"
func Slice(cap, _ uint64) experimental.LinearMemory {
return &sliceMemory{make([]byte, 0, cap)}
}
type sliceMemory struct {
buf []byte
}
func (b *sliceMemory) Free() {}
func (b *sliceMemory) Reallocate(size uint64) []byte {
if cap := uint64(cap(b.buf)); size > cap {
b.buf = append(b.buf[:cap], make([]byte, size-cap)...)
} else {
b.buf = b.buf[:size]
}
return b.buf
}

View File

@@ -9,6 +9,6 @@ import (
func TestVirtual(t *testing.T) {
defer func() { _ = recover() }()
alloc.Virtual(math.MaxInt+2, math.MaxInt+2)
alloc.NewMemory(math.MaxInt+2, math.MaxInt+2)
t.Error("want panic")
}

View File

@@ -9,7 +9,7 @@ import (
"golang.org/x/sys/unix"
)
func Virtual(_, max uint64) experimental.LinearMemory {
func NewMemory(_, max uint64) experimental.LinearMemory {
// Round up to the page size.
rnd := uint64(unix.Getpagesize() - 1)
max = (max + rnd) &^ rnd
@@ -47,7 +47,7 @@ func (m *mmappedMemory) Reallocate(size uint64) []byte {
// Commit additional memory up to new bytes.
err := unix.Mprotect(m.buf[com:new], unix.PROT_READ|unix.PROT_WRITE)
if err != nil {
panic(err)
return nil
}
// Update committed memory.

View File

@@ -11,7 +11,7 @@ import (
"golang.org/x/sys/windows"
)
func Virtual(_, max uint64) experimental.LinearMemory {
func NewMemory(_, max uint64) experimental.LinearMemory {
// Round up to the page size.
rnd := uint64(windows.Getpagesize() - 1)
max = (max + rnd) &^ rnd
@@ -56,7 +56,7 @@ func (m *virtualMemory) Reallocate(size uint64) []byte {
// Commit additional memory up to new bytes.
_, err := windows.VirtualAlloc(m.addr, uintptr(new), windows.MEM_COMMIT, windows.PAGE_READWRITE)
if err != nil {
panic(err)
return nil
}
// Update committed memory.

View File

@@ -1,24 +1,19 @@
package testcfg
import (
"math/bits"
"os"
"path/filepath"
"github.com/ncruces/go-sqlite3"
"github.com/tetratelabs/wazero"
"github.com/ncruces/go-sqlite3"
)
// notest
func init() {
if bits.UintSize < 64 {
return
}
sqlite3.RuntimeConfig = wazero.NewRuntimeConfig().
WithMemoryCapacityFromMax(true).
WithMemoryLimitPages(1024)
WithMemoryLimitPages(512)
if os.Getenv("CI") != "" {
path := filepath.Join(os.TempDir(), "wazero")

View File

@@ -1,22 +0,0 @@
package util
import "strings"
func ParseBool(s string) (b, ok bool) {
if len(s) == 0 {
return false, false
}
if s[0] == '0' {
return false, true
}
if '1' <= s[0] && s[0] <= '9' {
return true, true
}
switch strings.ToLower(s) {
case "true", "yes", "on":
return true, true
case "false", "no", "off":
return false, true
}
return false, false
}

View File

@@ -1,28 +0,0 @@
package util
import "testing"
func TestParseBool(t *testing.T) {
tests := []struct {
str string
val bool
ok bool
}{
{"", false, false},
{"0", false, true},
{"1", true, true},
{"9", true, true},
{"T", false, false},
{"true", true, true},
{"FALSE", false, true},
{"false?", false, false},
}
for _, tt := range tests {
t.Run(tt.str, func(t *testing.T) {
gotVal, gotOK := ParseBool(tt.str)
if gotVal != tt.val || gotOK != tt.ok {
t.Errorf("ParseBool(%q) = (%v, %v) want (%v, %v)", tt.str, gotVal, gotOK, tt.val, tt.ok)
}
})
}
}

View File

@@ -26,6 +26,7 @@ func ExportFuncVI[T0 i32](mod wazero.HostModuleBuilder, name string, fn func(con
type funcVII[T0, T1 i32] func(context.Context, api.Module, T0, T1)
func (fn funcVII[T0, T1]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[1] // prevent bounds check on every slice access
fn(ctx, mod, T0(stack[0]), T1(stack[1]))
}
@@ -39,6 +40,7 @@ func ExportFuncVII[T0, T1 i32](mod wazero.HostModuleBuilder, name string, fn fun
type funcVIII[T0, T1, T2 i32] func(context.Context, api.Module, T0, T1, T2)
func (fn funcVIII[T0, T1, T2]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[2] // prevent bounds check on every slice access
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]))
}
@@ -52,6 +54,7 @@ func ExportFuncVIII[T0, T1, T2 i32](mod wazero.HostModuleBuilder, name string, f
type funcVIIII[T0, T1, T2, T3 i32] func(context.Context, api.Module, T0, T1, T2, T3)
func (fn funcVIIII[T0, T1, T2, T3]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[3] // prevent bounds check on every slice access
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]))
}
@@ -65,6 +68,7 @@ func ExportFuncVIIII[T0, T1, T2, T3 i32](mod wazero.HostModuleBuilder, name stri
type funcVIIIII[T0, T1, T2, T3, T4 i32] func(context.Context, api.Module, T0, T1, T2, T3, T4)
func (fn funcVIIIII[T0, T1, T2, T3, T4]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[4] // prevent bounds check on every slice access
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4]))
}
@@ -78,6 +82,7 @@ func ExportFuncVIIIII[T0, T1, T2, T3, T4 i32](mod wazero.HostModuleBuilder, name
type funcVIIIIJ[T0, T1, T2, T3 i32, T4 i64] func(context.Context, api.Module, T0, T1, T2, T3, T4)
func (fn funcVIIIIJ[T0, T1, T2, T3, T4]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[4] // prevent bounds check on every slice access
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4]))
}
@@ -104,6 +109,7 @@ func ExportFuncII[TR, T0 i32](mod wazero.HostModuleBuilder, name string, fn func
type funcIII[TR, T0, T1 i32] func(context.Context, api.Module, T0, T1) TR
func (fn funcIII[TR, T0, T1]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[1] // prevent bounds check on every slice access
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1])))
}
@@ -117,6 +123,7 @@ func ExportFuncIII[TR, T0, T1 i32](mod wazero.HostModuleBuilder, name string, fn
type funcIIII[TR, T0, T1, T2 i32] func(context.Context, api.Module, T0, T1, T2) TR
func (fn funcIIII[TR, T0, T1, T2]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[2] // prevent bounds check on every slice access
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2])))
}
@@ -130,6 +137,7 @@ func ExportFuncIIII[TR, T0, T1, T2 i32](mod wazero.HostModuleBuilder, name strin
type funcIIIII[TR, T0, T1, T2, T3 i32] func(context.Context, api.Module, T0, T1, T2, T3) TR
func (fn funcIIIII[TR, T0, T1, T2, T3]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[3] // prevent bounds check on every slice access
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3])))
}
@@ -143,6 +151,7 @@ func ExportFuncIIIII[TR, T0, T1, T2, T3 i32](mod wazero.HostModuleBuilder, name
type funcIIIIII[TR, T0, T1, T2, T3, T4 i32] func(context.Context, api.Module, T0, T1, T2, T3, T4) TR
func (fn funcIIIIII[TR, T0, T1, T2, T3, T4]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[4] // prevent bounds check on every slice access
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4])))
}
@@ -156,6 +165,7 @@ func ExportFuncIIIIII[TR, T0, T1, T2, T3, T4 i32](mod wazero.HostModuleBuilder,
type funcIIIIIII[TR, T0, T1, T2, T3, T4, T5 i32] func(context.Context, api.Module, T0, T1, T2, T3, T4, T5) TR
func (fn funcIIIIIII[TR, T0, T1, T2, T3, T4, T5]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[5] // prevent bounds check on every slice access
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4]), T5(stack[5])))
}
@@ -169,6 +179,7 @@ func ExportFuncIIIIIII[TR, T0, T1, T2, T3, T4, T5 i32](mod wazero.HostModuleBuil
type funcIIIIJ[TR, T0, T1, T2 i32, T3 i64] func(context.Context, api.Module, T0, T1, T2, T3) TR
func (fn funcIIIIJ[TR, T0, T1, T2, T3]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[3] // prevent bounds check on every slice access
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3])))
}
@@ -182,6 +193,7 @@ func ExportFuncIIIIJ[TR, T0, T1, T2 i32, T3 i64](mod wazero.HostModuleBuilder, n
type funcIIJ[TR, T0 i32, T1 i64] func(context.Context, api.Module, T0, T1) TR
func (fn funcIIJ[TR, T0, T1]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[1] // prevent bounds check on every slice access
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1])))
}

View File

@@ -35,17 +35,22 @@ func DelHandle(ctx context.Context, id uint32) error {
s := ctx.Value(moduleKey{}).(*moduleState)
a := s.handles[^id]
s.handles[^id] = nil
s.holes++
if l := uint32(len(s.handles)); l == ^id {
s.handles = s.handles[:l-1]
} else {
s.holes++
}
if c, ok := a.(io.Closer); ok {
return c.Close()
}
return nil
}
func AddHandle(ctx context.Context, a any) (id uint32) {
func AddHandle(ctx context.Context, a any) uint32 {
if a == nil {
panic(NilErr)
}
s := ctx.Value(moduleKey{}).(*moduleState)
// Find an empty slot.

29
internal/util/math.go Normal file
View File

@@ -0,0 +1,29 @@
package util
import "math"
func abs(n int) int {
if n < 0 {
return -n
}
return n
}
func GCD(m, n int) int {
for n != 0 {
m, n = n, m%n
}
return abs(m)
}
func LCM(m, n int) int {
if n == 0 {
return 0
}
return abs(n) * (abs(m) / GCD(m, n))
}
// https://developer.nvidia.com/blog/lerp-faster-cuda/
func Lerp(v0, v1, t float64) float64 {
return math.FMA(t, v1, math.FMA(-t, v0, v0))
}

View File

@@ -1,4 +1,4 @@
package adiantum
package util
import (
"math"
@@ -25,7 +25,7 @@ func Test_abs(t *testing.T) {
}
}
func Test_gcd(t *testing.T) {
func Test_GCD(t *testing.T) {
tests := []struct {
arg1 int
arg2 int
@@ -46,14 +46,14 @@ func Test_gcd(t *testing.T) {
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
if got := gcd(tt.arg1, tt.arg2); got != tt.want {
if got := GCD(tt.arg1, tt.arg2); got != tt.want {
t.Errorf("gcd(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want)
}
})
}
}
func Test_lcm(t *testing.T) {
func Test_LCM(t *testing.T) {
tests := []struct {
arg1 int
arg2 int
@@ -74,7 +74,7 @@ func Test_lcm(t *testing.T) {
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
if got := lcm(tt.arg1, tt.arg2); got != tt.want {
if got := LCM(tt.arg1, tt.arg2); got != tt.want {
t.Errorf("lcm(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want)
}
})

View File

@@ -1,4 +1,4 @@
//go:build unix && (amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_noshm || sqlite3_nosys)
//go:build unix && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_noshm || sqlite3_nosys)
package util
@@ -7,17 +7,10 @@ import (
"os"
"unsafe"
"github.com/ncruces/go-sqlite3/internal/alloc"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"golang.org/x/sys/unix"
)
func withAllocator(ctx context.Context) context.Context {
return experimental.WithMemoryAllocator(ctx,
experimental.MemoryAllocatorFunc(alloc.Virtual))
}
type mmapState struct {
regions []*MappedRegion
}

View File

@@ -1,22 +1,5 @@
//go:build !unix || !(amd64 || arm64 || riscv64 || ppc64le) || sqlite3_noshm || sqlite3_nosys
//go:build !unix || !(386 || arm || amd64 || arm64 || riscv64 || ppc64le) || sqlite3_noshm || sqlite3_nosys
package util
import (
"context"
"github.com/ncruces/go-sqlite3/internal/alloc"
"github.com/tetratelabs/wazero/experimental"
)
type mmapState struct{}
func withAllocator(ctx context.Context) context.Context {
return experimental.WithMemoryAllocator(ctx,
experimental.MemoryAllocatorFunc(func(cap, max uint64) experimental.LinearMemory {
if cap == max {
return alloc.Virtual(cap, max)
}
return alloc.Slice(cap, max)
}))
}

View File

@@ -4,6 +4,8 @@ import (
"context"
"github.com/tetratelabs/wazero/experimental"
"github.com/ncruces/go-sqlite3/internal/alloc"
)
type moduleKey struct{}
@@ -14,7 +16,7 @@ type moduleState struct {
func NewContext(ctx context.Context) context.Context {
state := new(moduleState)
ctx = withAllocator(ctx)
ctx = experimental.WithMemoryAllocator(ctx, experimental.MemoryAllocatorFunc(alloc.NewMemory))
ctx = experimental.WithCloseNotifier(ctx, state)
ctx = context.WithValue(ctx, moduleKey{}, state)
return ctx

View File

@@ -1,15 +1,13 @@
package util_test
package util
import (
"math"
"testing"
"github.com/ncruces/go-sqlite3/internal/util"
)
func TestUnwrapPointer(t *testing.T) {
p := util.Pointer[float64]{Value: math.Pi}
if got := util.UnwrapPointer(p); got != math.Pi {
p := Pointer[float64]{Value: math.Pi}
if got := UnwrapPointer(p); got != math.Pi {
t.Errorf("want π, got %v", got)
}
}

View File

@@ -3,6 +3,7 @@ package sqlite3
import (
"bytes"
"math"
"reflect"
"strconv"
"strings"
"time"
@@ -13,6 +14,9 @@ import (
// Quote escapes and quotes a value
// making it safe to embed in SQL text.
// Strings with embedded NUL characters are truncated.
//
// https://sqlite.org/lang_corefunc.html#quote
func Quote(value any) string {
switch v := value.(type) {
case nil:
@@ -42,8 +46,8 @@ func Quote(value any) string {
return "'" + v.Format(time.RFC3339Nano) + "'"
case string:
if strings.IndexByte(v, 0) >= 0 {
break
if i := strings.IndexByte(v, 0); i >= 0 {
v = v[:i]
}
buf := make([]byte, 2+len(v)+strings.Count(v, "'"))
@@ -57,13 +61,13 @@ func Quote(value any) string {
buf[i] = b
i += 1
}
buf[i] = '\''
buf[len(buf)-1] = '\''
return unsafe.String(&buf[0], len(buf))
case []byte:
buf := make([]byte, 3+2*len(v))
buf[0] = 'x'
buf[1] = '\''
buf[0] = 'x'
i := 2
for _, b := range v {
const hex = "0123456789ABCDEF"
@@ -71,26 +75,50 @@ func Quote(value any) string {
buf[i+1] = hex[b%16]
i += 2
}
buf[i] = '\''
buf[len(buf)-1] = '\''
return unsafe.String(&buf[0], len(buf))
case ZeroBlob:
if v > ZeroBlob(1e9-3)/2 {
break
}
buf := bytes.Repeat([]byte("0"), int(3+2*int64(v)))
buf[0] = 'x'
buf[1] = '\''
buf[0] = 'x'
buf[len(buf)-1] = '\''
return unsafe.String(&buf[0], len(buf))
}
v := reflect.ValueOf(value)
k := v.Kind()
if k == reflect.Interface || k == reflect.Pointer {
if v.IsNil() {
return "NULL"
}
v = v.Elem()
k = v.Kind()
}
switch {
case v.CanInt():
return strconv.FormatInt(v.Int(), 10)
case v.CanUint():
return strconv.FormatUint(v.Uint(), 10)
case v.CanFloat():
return Quote(v.Float())
case k == reflect.Bool:
return Quote(v.Bool())
case k == reflect.String:
return Quote(v.String())
case (k == reflect.Slice || k == reflect.Array && v.CanAddr()) &&
v.Type().Elem().Kind() == reflect.Uint8:
return Quote(v.Bytes())
}
panic(util.ValueErr)
}
// QuoteIdentifier escapes and quotes an identifier
// making it safe to embed in SQL text.
// Strings with embedded NUL characters panic.
func QuoteIdentifier(id string) string {
if strings.IndexByte(id, 0) >= 0 {
panic(util.ValueErr)
@@ -107,6 +135,6 @@ func QuoteIdentifier(id string) string {
buf[i] = b
i += 1
}
buf[i] = '"'
buf[len(buf)-1] = '"'
return unsafe.String(&buf[0], len(buf))
}

View File

@@ -9,11 +9,12 @@ import (
"sync"
"unsafe"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/vfs"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/vfs"
)
// Configure SQLite Wasm.
@@ -49,10 +50,15 @@ func compileSQLite() {
cfg := RuntimeConfig
if cfg == nil {
cfg = wazero.NewRuntimeConfig()
if bits.UintSize >= 64 {
cfg = cfg.WithMemoryLimitPages(4096) // 256MB
} else {
cfg = cfg.WithMemoryLimitPages(512) // 32MB
}
}
cfg = cfg.WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads)
instance.runtime = wazero.NewRuntimeWithConfig(ctx,
cfg.WithCoreFeatures(api.CoreFeaturesV2|experimental.CoreFeaturesThreads))
instance.runtime = wazero.NewRuntimeWithConfig(ctx, cfg)
env := instance.runtime.NewHostModuleBuilder("env")
env = vfs.ExportHostFunctions(env)
@@ -131,7 +137,7 @@ func (sqlt *sqlite) error(rc uint64, handle uint32, sql ...string) error {
err.msg = util.ReadString(sqlt.mod, uint32(r), _MAX_LENGTH)
}
if sql != nil {
if len(sql) != 0 {
if r := sqlt.call("sqlite3_error_offset", uint64(handle)); r != math.MaxUint32 {
err.sql = sql[0][r:]
}
@@ -301,7 +307,7 @@ func (a *arena) string(s string) uint32 {
func exportCallbacks(env wazero.HostModuleBuilder) wazero.HostModuleBuilder {
util.ExportFuncII(env, "go_progress_handler", progressCallback)
util.ExportFuncIIII(env, "go_busy_timeout", timeoutCallback)
util.ExportFuncIII(env, "go_busy_timeout", timeoutCallback)
util.ExportFuncIII(env, "go_busy_handler", busyCallback)
util.ExportFuncII(env, "go_commit_hook", commitCallback)
util.ExportFuncVI(env, "go_rollback_hook", rollbackCallback)

View File

@@ -2,12 +2,12 @@
#include "sqlite3.h"
int sqlite3_bind_text_go(sqlite3_stmt* stmt, int i, const char* zData,
int sqlite3_bind_text_go(sqlite3_stmt *stmt, int i, const char *zData,
sqlite3_uint64 nData) {
return sqlite3_bind_text64(stmt, i, zData, nData, &sqlite3_free, SQLITE_UTF8);
}
int sqlite3_bind_blob_go(sqlite3_stmt* stmt, int i, const char* zData,
int sqlite3_bind_blob_go(sqlite3_stmt *stmt, int i, const char *zData,
sqlite3_uint64 nData) {
return sqlite3_bind_blob64(stmt, i, zData, nData, &sqlite3_free);
}

View File

@@ -1,8 +1,8 @@
# Replace sqliteDefaultBusyCallback.
# This patch allows Go to handle (and interrupt) sqlite3_busy_timeout.
# Replace sqliteDefaultBusyCallback, so Go can
# handle, and interrupt, sqlite3_busy_timeout.
--- sqlite3.c.orig
+++ sqlite3.c
@@ -181614,7 +181614,7 @@
@@ -182908,7 +182908,7 @@
if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;
#endif
if( ms>0 ){

View File

@@ -22,6 +22,7 @@ int sqlite3_columns_go(sqlite3_stmt *stmt, int nCol, char *aType,
switch (aType[i] = sqlite3_column_type(stmt, i)) {
default: // SQLITE_NULL
aData[i] = (union sqlite3_data){};
continue;
case SQLITE_INTEGER:
aData[i].i = sqlite3_column_int64(stmt, i);
continue;

View File

@@ -3,33 +3,43 @@ set -euo pipefail
cd -P -- "$(dirname -- "$0")"
curl -#OL "https://sqlite.org/2024/sqlite-amalgamation-3460100.zip"
curl -#OL "https://sqlite.org/2024/sqlite-amalgamation-3470000.zip"
unzip -d . sqlite-amalgamation-*.zip
mv sqlite-amalgamation-*/sqlite3* .
mv sqlite-amalgamation-*/sqlite3.c .
mv sqlite-amalgamation-*/sqlite3.h .
mv sqlite-amalgamation-*/sqlite3ext.h .
rm -rf sqlite-amalgamation-*
# To test a snapshot:
# curl -# https://sqlite.org/snapshot/sqlite-snapshot-202410081727.tar.gz | tar xz
# mv sqlite-snapshot-*/sqlite3.c .
# mv sqlite-snapshot-*/sqlite3.h .
# mv sqlite-snapshot-*/sqlite3ext.h .
# rm -rf sqlite-snapshot-*
cat *.patch | patch --no-backup-if-mismatch
mkdir -p ext/
cd ext/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/anycollseq.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/base64.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/decimal.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/ieee754.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/regexp.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/series.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/uint.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/ext/misc/anycollseq.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/ext/misc/base64.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/ext/misc/decimal.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/ext/misc/ieee754.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/ext/misc/regexp.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/ext/misc/series.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/ext/misc/spellfix.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/ext/misc/uint.c"
cd ~-
cd ../vfs/tests/mptest/testdata/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/mptest.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/config01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/config02.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/crash01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/crash02.subtest"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/multiwrite01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/mptest/mptest.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/mptest/config01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/mptest/config02.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/mptest/crash01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/mptest/crash02.subtest"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/mptest/multiwrite01.test"
cd ~-
cd ../vfs/tests/speedtest1/testdata/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/test/speedtest1.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.0/test/speedtest1.c"
cd ~-

View File

@@ -53,6 +53,9 @@ int sqlite3_collation_needed_go(sqlite3 *db, bool enable) {
}
int sqlite3_create_collation_go(sqlite3 *db, const char *name, go_handle app) {
if (app == NULL) {
return sqlite3_create_collation_v2(db, name, SQLITE_UTF8, NULL, NULL, NULL);
}
int rc = sqlite3_create_collation_v2(db, name, SQLITE_UTF8, app, go_compare,
go_destroy);
if (rc) go_destroy(app);
@@ -61,6 +64,10 @@ int sqlite3_create_collation_go(sqlite3 *db, const char *name, go_handle app) {
int sqlite3_create_function_go(sqlite3 *db, const char *name, int argc,
int flags, go_handle app) {
if (app == NULL) {
return sqlite3_create_function_v2(db, name, argc, SQLITE_UTF8 | flags, NULL,
NULL, NULL, NULL, NULL);
}
return sqlite3_create_function_v2(db, name, argc, SQLITE_UTF8 | flags, app,
go_func_wrapper, /*step=*/NULL,
/*final=*/NULL, go_destroy);
@@ -68,6 +75,10 @@ int sqlite3_create_function_go(sqlite3 *db, const char *name, int argc,
int sqlite3_create_aggregate_function_go(sqlite3 *db, const char *name,
int argc, int flags, go_handle app) {
if (app == NULL) {
return sqlite3_create_function_v2(db, name, argc, SQLITE_UTF8 | flags, NULL,
NULL, NULL, NULL, NULL);
}
return sqlite3_create_function_v2(db, name, argc, SQLITE_UTF8 | flags, app,
/*func=*/NULL, go_step_wrapper,
go_final_wrapper, go_destroy);
@@ -75,6 +86,10 @@ int sqlite3_create_aggregate_function_go(sqlite3 *db, const char *name,
int sqlite3_create_window_function_go(sqlite3 *db, const char *name, int argc,
int flags, go_handle app) {
if (app == NULL) {
return sqlite3_create_window_function(db, name, argc, SQLITE_UTF8 | flags,
NULL, NULL, NULL, NULL, NULL, NULL);
}
return sqlite3_create_window_function(
db, name, argc, SQLITE_UTF8 | flags, app, go_step_wrapper,
go_final_wrapper, go_value_wrapper, go_inverse_wrapper, go_destroy);

View File

@@ -4,7 +4,7 @@
int go_progress_handler(void *);
int go_busy_handler(void *, int);
int go_busy_timeout(void *, int count, int tmout);
int go_busy_timeout(int count, int tmout);
int go_commit_hook(void *);
void go_rollback_hook(void *);
@@ -20,7 +20,7 @@ unsigned int go_autovacuum_pages(void *, const char *, unsigned int,
unsigned int, unsigned int);
void sqlite3_progress_handler_go(sqlite3 *db, int n) {
sqlite3_progress_handler(db, n, go_progress_handler, /*arg=*/db);
sqlite3_progress_handler(db, n, go_progress_handler, /*arg=*/NULL);
}
int sqlite3_busy_handler_go(sqlite3 *db, bool enable) {
@@ -57,15 +57,16 @@ int sqlite3_config_log_go(bool enable) {
}
int sqlite3_autovacuum_pages_go(sqlite3 *db, go_handle app) {
int rc = sqlite3_autovacuum_pages(db, go_autovacuum_pages, app, go_destroy);
if (rc) go_destroy(app);
return rc;
if (app == NULL) {
return sqlite3_autovacuum_pages(db, NULL, NULL, NULL);
}
return sqlite3_autovacuum_pages(db, go_autovacuum_pages, app, go_destroy);
}
#ifndef sqliteBusyCallback
static int sqliteBusyCallback(sqlite3 *db, int count) {
return go_busy_timeout(db, count, db->busyTimeout);
return go_busy_timeout(count, db->busyTimeout);
}
#endif

View File

@@ -7,6 +7,7 @@
#include "ext/ieee754.c"
#include "ext/regexp.c"
#include "ext/series.c"
#include "ext/spellfix.c"
#include "ext/uint.c"
// Bindings
#include "bind.c"
@@ -26,6 +27,7 @@ __attribute__((constructor)) void init() {
sqlite3_auto_extension((void (*)(void))sqlite3_ieee_init);
sqlite3_auto_extension((void (*)(void))sqlite3_regexp_init);
sqlite3_auto_extension((void (*)(void))sqlite3_series_init);
sqlite3_auto_extension((void (*)(void))sqlite3_spellfix_init);
sqlite3_auto_extension((void (*)(void))sqlite3_uint_init);
sqlite3_auto_extension((void (*)(void))sqlite3_time_init);
}

View File

@@ -1,13 +1,14 @@
#include <math.h>
#include <stdlib.h>
#include "sqlite3.h"
void sqlite3_result_text_go(sqlite3_context* ctx, const char* zData,
void sqlite3_result_text_go(sqlite3_context *ctx, const char *zData,
sqlite3_uint64 nData) {
sqlite3_result_text64(ctx, zData, nData, &sqlite3_free, SQLITE_UTF8);
}
void sqlite3_result_blob_go(sqlite3_context* ctx, const void* zData,
void sqlite3_result_blob_go(sqlite3_context *ctx, const void *zData,
sqlite3_uint64 nData) {
sqlite3_result_blob64(ctx, zData, nData, &sqlite3_free);
}

View File

@@ -89,59 +89,13 @@ struct go_file {
go_handle handle;
};
int sqlite3_os_init() {
static sqlite3_vfs os_vfs = {
.iVersion = 2,
.szOsFile = sizeof(struct go_file),
.mxPathname = 1024,
.zName = "os",
.xOpen = go_open_wrapper,
.xDelete = go_delete,
.xAccess = go_access,
.xFullPathname = go_full_pathname,
.xRandomness = go_randomness,
.xSleep = go_sleep,
.xCurrentTimeInt64 = go_current_time_64,
};
return sqlite3_vfs_register(&os_vfs, /*default=*/true);
}
int localtime_s(struct tm *const pTm, time_t const *const pTime) {
return go_localtime(pTm, (sqlite3_int64)*pTime);
}
sqlite3_vfs *sqlite3_vfs_find(const char *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)) {
return it;
}
}
for (sqlite3_vfs **ptr = &go_vfs_list; *ptr;) {
sqlite3_vfs *it = *ptr;
if (go_vfs_find(it->zName)) {
ptr = &it->pNext;
} else {
*ptr = it->pNext;
free(it);
}
}
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){
if (!zVfsName || !strcmp(zVfsName, "os")) {
static sqlite3_vfs os_vfs = {
.iVersion = 2,
.szOsFile = sizeof(struct go_file),
.mxPathname = 1024,
.zName = name,
.pNext = head,
.zName = "os",
.xOpen = go_open_wrapper,
.xDelete = go_delete,
@@ -152,9 +106,60 @@ sqlite3_vfs *sqlite3_vfs_find(const char *zVfsName) {
.xSleep = go_sleep,
.xCurrentTimeInt64 = go_current_time_64,
};
return go_vfs_list;
return &os_vfs;
}
return sqlite3_vfs_find_orig(zVfsName);
if (!go_vfs_find(zVfsName)) {
return NULL;
}
static sqlite3_vfs *go_vfs_list;
for (sqlite3_vfs *it = go_vfs_list; it; it = it->pNext) {
if (!strcmp(zVfsName, it->zName)) {
return it;
}
}
for (sqlite3_vfs **ptr = &go_vfs_list; *ptr;) {
sqlite3_vfs *it = *ptr;
if (go_vfs_find(it->zName)) {
ptr = &it->pNext;
} else {
*ptr = it->pNext;
free(it);
}
}
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 = 1024,
.zName = name,
.pNext = head,
.xOpen = go_open_wrapper,
.xDelete = go_delete,
.xAccess = go_access,
.xFullPathname = go_full_pathname,
.xRandomness = go_randomness,
.xSleep = go_sleep,
.xCurrentTimeInt64 = go_current_time_64,
};
return go_vfs_list;
}
int localtime_s(struct tm *const pTm, time_t const *const pTime) {
return go_localtime(pTm, (sqlite3_int64)*pTime);
}
int sqlite3_os_init() {
return SQLITE_OK;
}
static_assert(offsetof(sqlite3_vfs, zName) == 16, "Unexpected offset");

View File

@@ -1,13 +1,21 @@
# Wrap sqlite3_vfs_find.
# This patch allows Go VFSes to be (un)registered.
# Remove VFS registration. Go handles it.
--- sqlite3.c.orig
+++ sqlite3.c
@@ -26396,7 +26396,7 @@
** Locate a VFS by name. If no name is given, simply return the
** first VFS on the list.
@@ -26594,7 +26594,7 @@
sqlite3_free(p);
return sqlite3_os_init();
}
-
+#if 0 // Go handles VFS registration.
/*
** The list of all registered VFS implementations.
*/
-SQLITE_API sqlite3_vfs *sqlite3_vfs_find(const char *zVfs){
+SQLITE_API sqlite3_vfs *sqlite3_vfs_find_orig(const char *zVfs){
sqlite3_vfs *pVfs = 0;
#if SQLITE_THREADSAFE
sqlite3_mutex *mutex;
@@ -26691,7 +26691,7 @@
sqlite3_mutex_leave(mutex);
return SQLITE_OK;
}
-
+#endif
/************** End of os.c **************************************************/
/************** Begin file fault.c *******************************************/
/*

View File

@@ -163,6 +163,10 @@ static int go_vtab_shadown_name_wrapper(const char *zName) { return true; }
int sqlite3_create_module_go(sqlite3 *db, const char *zName, int flags,
go_handle handle) {
if (handle == NULL) {
return sqlite3_create_module_v2(db, zName, NULL, NULL, NULL);
}
struct go_module *mod = malloc(sizeof(struct go_module));
if (mod == NULL) {
go_destroy(handle);

View File

@@ -6,12 +6,10 @@ import (
"testing"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/tetratelabs/wazero"
)
func init() {
Path = "./embed/sqlite3.wasm"
RuntimeConfig = wazero.NewRuntimeConfig().WithMemoryLimitPages(1024)
}
func Test_sqlite_error_OOM(t *testing.T) {

57
stmt.go
View File

@@ -30,12 +30,13 @@ func (s *Stmt) Close() error {
}
r := s.c.call("sqlite3_finalize", uint64(s.handle))
for i := range s.c.stmts {
if s == s.c.stmts[i] {
l := len(s.c.stmts) - 1
s.c.stmts[i] = s.c.stmts[l]
s.c.stmts[l] = nil
s.c.stmts = s.c.stmts[:l]
stmts := s.c.stmts
for i := range stmts {
if s == stmts[i] {
l := len(stmts) - 1
stmts[i] = stmts[l]
stmts[l] = nil
s.c.stmts = stmts[:l]
break
}
}
@@ -105,7 +106,7 @@ func (s *Stmt) Busy() bool {
//
// https://sqlite.org/c3ref/step.html
func (s *Stmt) Step() bool {
s.c.checkInterrupt()
s.c.checkInterrupt(s.c.handle)
r := s.c.call("sqlite3_step", uint64(s.handle))
switch r {
case _ROW:
@@ -376,6 +377,15 @@ func (s *Stmt) BindValue(param int, value Value) error {
return s.c.error(r)
}
// DataCount resets the number of columns in a result set.
//
// https://sqlite.org/c3ref/data_count.html
func (s *Stmt) DataCount() int {
r := s.c.call("sqlite3_data_count",
uint64(s.handle))
return int(int32(r))
}
// ColumnCount returns the number of columns in a result set.
//
// https://sqlite.org/c3ref/column_count.html
@@ -630,7 +640,7 @@ func (s *Stmt) Columns(dest []any) error {
defer s.c.arena.mark()()
count := uint64(len(dest))
typePtr := s.c.arena.new(count)
dataPtr := s.c.arena.new(8 * count)
dataPtr := s.c.arena.new(count * 8)
r := s.c.call("sqlite3_columns_go",
uint64(s.handle), count, uint64(typePtr), uint64(dataPtr))
@@ -639,26 +649,31 @@ func (s *Stmt) Columns(dest []any) error {
}
types := util.View(s.c.mod, typePtr, count)
// Avoid bounds checks on types below.
if len(types) != len(dest) {
panic(util.AssertErr())
}
for i := range dest {
switch types[i] {
case byte(INTEGER):
dest[i] = int64(util.ReadUint64(s.c.mod, dataPtr+8*uint32(i)))
continue
dest[i] = int64(util.ReadUint64(s.c.mod, dataPtr))
case byte(FLOAT):
dest[i] = util.ReadFloat64(s.c.mod, dataPtr+8*uint32(i))
continue
dest[i] = util.ReadFloat64(s.c.mod, dataPtr)
case byte(NULL):
dest[i] = nil
continue
}
ptr := util.ReadUint32(s.c.mod, dataPtr+8*uint32(i)+0)
len := util.ReadUint32(s.c.mod, dataPtr+8*uint32(i)+4)
buf := util.View(s.c.mod, ptr, uint64(len))
if types[i] == byte(TEXT) {
dest[i] = string(buf)
} else {
dest[i] = buf
default:
ptr := util.ReadUint32(s.c.mod, dataPtr+0)
len := util.ReadUint32(s.c.mod, dataPtr+4)
buf := util.View(s.c.mod, ptr, uint64(len))
if types[i] == byte(TEXT) {
dest[i] = string(buf)
} else {
dest[i] = buf
}
}
dataPtr += 8
}
return nil
}

75
tests/cksm_test.go Normal file
View File

@@ -0,0 +1,75 @@
package tests
import (
_ "embed"
"strings"
"testing"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/util/ioutil"
"github.com/ncruces/go-sqlite3/vfs/memdb"
"github.com/ncruces/go-sqlite3/vfs/readervfs"
)
//go:embed testdata/cksm.db
var cksmDB string
func Test_fileformat(t *testing.T) {
t.Parallel()
readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(cksmDB)))
db, err := driver.Open("file:test.db?vfs=reader")
if err != nil {
t.Fatal(err)
}
defer db.Close()
var enabled bool
err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled)
if err != nil {
t.Fatal(err)
}
if !enabled {
t.Error("want true")
}
db.SetMaxIdleConns(0) // Clears the page cache.
_, err = db.Exec(`PRAGMA integrity_check`)
if err != nil {
t.Fatal(err)
}
}
func Test_enable(t *testing.T) {
t.Parallel()
db, err := driver.Open(memdb.TestDB(t),
func(db *sqlite3.Conn) error {
return db.EnableChecksums("main")
})
if err != nil {
t.Fatal(err)
}
defer db.Close()
var enabled bool
err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled)
if err != nil {
t.Fatal(err)
}
if !enabled {
t.Error("want true")
}
db.SetMaxIdleConns(0) // Clears the page cache.
_, err = db.Exec(`PRAGMA integrity_check`)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -60,6 +60,11 @@ func TestConn_Config(t *testing.T) {
if o != false {
t.Error("want false")
}
_, err = db.Config(0)
if !errors.Is(err, sqlite3.MISUSE) {
t.Errorf("got %v, want MISUSE", err)
}
}
func TestConn_ConfigLog(t *testing.T) {

View File

@@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
@@ -148,7 +149,10 @@ func TestConn_SetInterrupt(t *testing.T) {
defer stmt.Close()
db.SetInterrupt(ctx)
go cancel()
go func() {
time.Sleep(time.Millisecond)
cancel()
}()
// Interrupting works.
err = stmt.Exec()

View File

@@ -1,19 +1,18 @@
package tests
import (
_ "embed"
"os"
"path/filepath"
"testing"
_ "embed"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs"
_ "github.com/ncruces/go-sqlite3/vfs/adiantum"
"github.com/ncruces/go-sqlite3/vfs/memdb"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
_ "github.com/ncruces/go-sqlite3/vfs/xts"
)
//go:embed testdata/wal.db
@@ -76,6 +75,13 @@ func TestDB_adiantum(t *testing.T) {
"&vfs=adiantum&textkey=correct+horse+battery+staple")
}
func TestDB_xts(t *testing.T) {
t.Parallel()
tmp := filepath.Join(t.TempDir(), "test.db")
testDB(t, "file:"+filepath.ToSlash(tmp)+"?nolock=1"+
"&vfs=xts&textkey=correct+horse+battery+staple")
}
func TestDB_nolock(t *testing.T) {
t.Parallel()
tmp := filepath.Join(t.TempDir(), "test.db")

View File

@@ -209,6 +209,24 @@ func TestCreateFunction_error(t *testing.T) {
stmt.Step()
}
func TestCreateFunction_delete(t *testing.T) {
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
err = db.CreateFunction("regexp", 2, 0, nil)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`SELECT 'a' REGEXP 'a|b'`)
if err == nil {
t.Error("want error")
}
}
func TestOverloadFunction(t *testing.T) {
t.Parallel()

View File

@@ -1,6 +1,7 @@
package tests
import (
"context"
"errors"
"io"
"log"
@@ -19,6 +20,7 @@ import (
"github.com/ncruces/go-sqlite3/vfs"
_ "github.com/ncruces/go-sqlite3/vfs/adiantum"
"github.com/ncruces/go-sqlite3/vfs/memdb"
_ "github.com/ncruces/go-sqlite3/vfs/xts"
)
func TestMain(m *testing.M) {
@@ -99,9 +101,9 @@ func Test_adiantum(t *testing.T) {
var iter int
if testing.Short() {
iter = 1000
iter = 500
} else {
iter = 5000
iter = 2500
}
name := "file:" +
@@ -115,6 +117,29 @@ func Test_adiantum(t *testing.T) {
testIntegrity(t, name)
}
func Test_xts(t *testing.T) {
if !vfs.SupportsFileLocking {
t.Skip("skipping without locks")
}
var iter int
if testing.Short() {
iter = 500
} else {
iter = 2500
}
name := "file:" +
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db")) +
"?vfs=xts" +
"&_pragma=hexkey(e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855)" +
"&_pragma=busy_timeout(10000)" +
"&_pragma=journal_mode(truncate)" +
"&_pragma=synchronous(off)"
testParallel(t, name, iter)
testIntegrity(t, name)
}
func TestMultiProcess(t *testing.T) {
if !vfs.SupportsFileLocking {
t.Skip("skipping without locks")
@@ -224,9 +249,13 @@ func testParallel(t testing.TB, name string, n int) {
}
defer db.Close()
err = db.BusyHandler(func(count int) (retry bool) {
time.Sleep(time.Millisecond)
return true
err = db.BusyHandler(func(ctx context.Context, count int) (retry bool) {
select {
case <-time.After(time.Millisecond):
return true
case <-ctx.Done():
return false
}
})
if err != nil {
return err

View File

@@ -1,6 +1,8 @@
package tests
import (
"database/sql"
"encoding/json"
"math"
"reflect"
"testing"
@@ -19,8 +21,8 @@ func TestQuote(t *testing.T) {
{`a'bc`, "'a''bc'"},
{"\x07bc", "'\abc'"},
{"\x1c\n", "'\x1c\n'"},
{"\xB0\x00\x0B", "'\xB0'"},
{[]byte("\xB0\x00\x0B"), "x'B0000B'"},
{"\xB0\x00\x0B", ""},
{0, "0"},
{true, "1"},
@@ -33,7 +35,13 @@ func TestQuote(t *testing.T) {
{int64(math.MaxInt64), "9223372036854775807"},
{time.Unix(0, 0).UTC(), "'1970-01-01T00:00:00Z'"},
{sqlite3.ZeroBlob(4), "x'00000000'"},
{sqlite3.ZeroBlob(1e9), ""},
{int8(0), "0"},
{uint(0), "0"},
{float32(0), "0"},
{(*string)(nil), "NULL"},
{json.Number("0"), "'0'"},
{&sql.RawBytes{'0'}, "x'30'"},
{t, ""}, // panic
}
for _, tt := range tests {
@@ -62,7 +70,7 @@ func TestQuoteIdentifier(t *testing.T) {
{`a'bc`, `"a'bc"`},
{"\x07bc", "\"\abc\""},
{"\x1c\n", "\"\x1c\n\""},
{"\xB0\x00\x0B", ""},
{"\xB0\x00\x0B", ""}, // panic
}
for _, tt := range tests {

View File

@@ -146,6 +146,12 @@ func TestStmt(t *testing.T) {
if got := stmt.ReadOnly(); got != true {
t.Error("got false, want true")
}
if got := stmt.DataCount(); got != 0 {
t.Errorf("got %d, want 0", got)
}
if got := stmt.ColumnCount(); got != 1 {
t.Errorf("got %d, want 1", got)
}
if got := stmt.ColumnName(0); got != "c" {
t.Errorf(`got %q, want "c"`, got)
}
@@ -503,6 +509,10 @@ func TestStmt(t *testing.T) {
}
}
if got := stmt.DataCount(); got != 1 {
t.Errorf("got %d, want 1", got)
}
db.Stmts()(func(s *sqlite3.Stmt) bool {
if s != stmt {
t.Error()

BIN
tests/testdata/cksm.db vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
tests/testdata/wal.db vendored

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More