Compare commits

...

21 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
123 changed files with 1968 additions and 702 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:
@@ -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

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

138
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
@@ -311,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
}

17
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.
@@ -204,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))
@@ -457,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 {
@@ -484,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)
}
@@ -519,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

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

@@ -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:
@@ -36,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

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

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

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.
@@ -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":

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,7 +17,7 @@ const (
)
func getColumnAffinities(schema string) ([]affinity, error) {
tab, err := vtabutil.Parse(schema)
tab, err := sql3util.ParseTable(schema)
if err != nil {
return nil, err
}

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

@@ -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
)
@@ -39,8 +40,14 @@ func (q *percentile) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
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) {
@@ -54,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 {
@@ -68,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

@@ -53,6 +53,7 @@ import (
// Register registers statistics functions.
func Register(db *sqlite3.Conn) error {
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)))
}

View File

@@ -28,14 +28,15 @@ import (
"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.

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"github.com/google/uuid"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)

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

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

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

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

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

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

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

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

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

@@ -3,34 +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/spellfix.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

@@ -1,29 +0,0 @@
--- sqlite3.c.orig
+++ sqlite3.c
@@ -35561,7 +35561,7 @@
if( e==0 ){
*pResult = s;
- }else if( sqlite3Config.bUseLongDouble ){
+ }else if( sizeof(LONGDOUBLE_TYPE)>8 && sqlite3Config.bUseLongDouble ){
LONGDOUBLE_TYPE r = (LONGDOUBLE_TYPE)s;
if( e>0 ){
while( e>=100 ){ e-=100; r *= 1.0e+100L; }
@@ -35967,7 +35967,7 @@
/* Multiply r by powers of ten until it lands somewhere in between
** 1.0e+19 and 1.0e+17.
*/
- if( sqlite3Config.bUseLongDouble ){
+ if( sizeof(LONGDOUBLE_TYPE)>8 && sqlite3Config.bUseLongDouble ){
LONGDOUBLE_TYPE rr = r;
if( rr>=1.0e+19 ){
while( rr>=1.0e+119L ){ exp+=100; rr *= 1.0e-100L; }
@@ -89354,7 +89354,7 @@
** than NULL */
return 1;
}
- if( sqlite3Config.bUseLongDouble ){
+ if( sizeof(LONGDOUBLE_TYPE)>8 && sqlite3Config.bUseLongDouble ){
LONGDOUBLE_TYPE x = (LONGDOUBLE_TYPE)i;
testcase( x<r );
testcase( x>r );

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

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

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

@@ -20,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) {
@@ -100,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:" +
@@ -116,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")

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.

View File

@@ -223,6 +223,9 @@ func TestDB_timeCollation(t *testing.T) {
}
func TestDB_isoWeek(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
t.Parallel()
db, err := sqlite3.Open(":memory:")

View File

@@ -77,7 +77,7 @@ func TestWAL_readonly(t *testing.T) {
// Select the data using the second (readonly) connection.
var name string
err = db2.QueryRow("SELECT name FROM t").Scan(&name)
err = db2.QueryRow(`SELECT name FROM t`).Scan(&name)
if err != nil {
t.Fatal(err)
}
@@ -95,7 +95,7 @@ func TestWAL_readonly(t *testing.T) {
}
// Select the data using the second (readonly) connection.
err = db2.QueryRow("SELECT name FROM t").Scan(&name)
err = db2.QueryRow(`SELECT name FROM t`).Scan(&name)
if err != nil {
t.Fatal(err)
}
@@ -104,7 +104,7 @@ func TestWAL_readonly(t *testing.T) {
}
}
func TestConn_WalCheckpoint(t *testing.T) {
func TestConn_WALCheckpoint(t *testing.T) {
if !vfs.SupportsFileLocking {
t.Skip("skipping without locks")
}
@@ -118,13 +118,13 @@ func TestConn_WalCheckpoint(t *testing.T) {
}
defer db.Close()
err = db.WalAutoCheckpoint(1000)
err = db.WALAutoCheckpoint(1000)
if err != nil {
t.Fatal(err)
}
db.WalHook(func(db *sqlite3.Conn, schema string, pages int) error {
log, ckpt, err := db.WalCheckpoint(schema, sqlite3.CHECKPOINT_FULL)
db.WALHook(func(db *sqlite3.Conn, schema string, pages int) error {
log, ckpt, err := db.WALCheckpoint(schema, sqlite3.CHECKPOINT_FULL)
t.Log(log, ckpt, err)
return err
})

12
txn.go
View File

@@ -8,8 +8,9 @@ import (
"strconv"
"strings"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Txn is an in-progress database transaction.
@@ -142,7 +143,7 @@ func (c *Conn) Savepoint() Savepoint {
// Names can be reused, but this makes catching bugs more likely.
name = QuoteIdentifier(name + "_" + strconv.Itoa(int(rand.Int31())))
err := c.txnExecInterrupted("SAVEPOINT " + name)
err := c.txnExecInterrupted(`SAVEPOINT ` + name)
if err != nil {
panic(err)
}
@@ -186,7 +187,7 @@ func (s Savepoint) Release(errp *error) {
if s.c.GetAutocommit() { // There is nothing to commit.
return
}
*errp = s.c.Exec("RELEASE " + s.name)
*errp = s.c.Exec(`RELEASE ` + s.name)
if *errp == nil {
return
}
@@ -198,8 +199,7 @@ func (s Savepoint) Release(errp *error) {
return
}
// ROLLBACK and RELEASE even if interrupted.
err := s.c.txnExecInterrupted("ROLLBACK TO " +
s.name + "; RELEASE " + s.name)
err := s.c.txnExecInterrupted(`ROLLBACK TO ` + s.name + `; RELEASE ` + s.name)
if err != nil {
panic(err)
}
@@ -212,7 +212,7 @@ func (s Savepoint) Release(errp *error) {
// https://sqlite.org/lang_transaction.html
func (s Savepoint) Rollback() error {
// ROLLBACK even if interrupted.
return s.c.txnExecInterrupted("ROLLBACK TO " + s.name)
return s.c.txnExecInterrupted(`ROLLBACK TO ` + s.name)
}
func (c *Conn) txnExecInterrupted(sql string) error {

4
util/README.md Normal file
View File

@@ -0,0 +1,4 @@
# Go SQLite Utilities
This folder collects additional SQLite utilities
that help extension writers provide a consistent developer experience.

View File

@@ -1,4 +1,4 @@
// Package fsutil implements filesystem utility functions.
// Package fsutil implements filesystem utilities.
package fsutil
import (

View File

@@ -1,2 +1,2 @@
// Package ioutil implements I/O utility functions.
// Package ioutil implements I/O utilities.
package ioutil

View File

@@ -1,2 +1,2 @@
// Package osutil implements operating system utility functions.
// Package osutil implements operating system utilities.
package osutil

View File

@@ -1,6 +1,7 @@
# Virtual Table utility functions
# SQLite utility functions
This package implements utilities mostly useful to virtual table implementations.
This package implements assorted SQLite utilities
useful to extension writers.
It also wraps a [parser](https://github.com/marcobambini/sqlite-createtable-parser)
for the [`CREATE`](https://sqlite.org/lang_createtable.html) and

65
util/sql3util/arg.go Normal file
View File

@@ -0,0 +1,65 @@
package sql3util
import "strings"
// NamedArg splits an named arg into a key and value,
// around an equals sign.
// Spaces are trimmed around both key and value.
func NamedArg(arg string) (key, val string) {
key, val, _ = strings.Cut(arg, "=")
key = strings.TrimSpace(key)
val = strings.TrimSpace(val)
return
}
// Unquote unquotes a string.
//
// https://sqlite.org/lang_keywords.html
func Unquote(val string) string {
if len(val) < 2 {
return val
}
fst := val[0]
lst := val[len(val)-1]
rst := val[1 : len(val)-1]
if fst == '[' && lst == ']' {
return rst
}
if fst != lst {
return val
}
var old, new string
switch fst {
default:
return val
case '`':
old, new = "``", "`"
case '"':
old, new = `""`, `"`
case '\'':
old, new = `''`, `'`
}
return strings.ReplaceAll(rst, old, new)
}
// ParseBool parses a boolean.
//
// https://sqlite.org/pragma.html#syntax
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
}

55
util/sql3util/arg_test.go Normal file
View File

@@ -0,0 +1,55 @@
package sql3util_test
import (
"testing"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
func TestUnquote(t *testing.T) {
tests := []struct {
val string
want string
}{
{"a", "a"},
{"abc", "abc"},
{"abba", "abba"},
{"`ab``c`", "ab`c"},
{"'ab''c'", "ab'c"},
{"'ab``c'", "ab``c"},
{"[ab``c]", "ab``c"},
{`"ab""c"`, `ab"c`},
}
for _, tt := range tests {
t.Run(tt.val, func(t *testing.T) {
if got := sql3util.Unquote(tt.val); got != tt.want {
t.Errorf("Unquote(%s) = %s, want %s", tt.val, got, tt.want)
}
})
}
}
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 := sql3util.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

@@ -1,4 +1,4 @@
package vtabutil
package sql3util
const (
_NONE = iota

View File

@@ -1,14 +1,14 @@
package vtabutil
package sql3util
import (
"context"
_ "embed"
"sync"
_ "embed"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
)
const (
@@ -24,11 +24,11 @@ var (
compiled wazero.CompiledModule
)
// Parse parses a [CREATE] or [ALTER TABLE] command.
// ParseTable parses a [CREATE] or [ALTER TABLE] command.
//
// [CREATE]: https://sqlite.org/lang_createtable.html
// [ALTER TABLE]: https://sqlite.org/lang_altertable.html
func Parse(sql string) (_ *Table, err error) {
func ParseTable(sql string) (_ *Table, err error) {
once.Do(func() {
ctx := context.Background()
cfg := wazero.NewRuntimeConfigInterpreter()

View File

@@ -1,13 +1,13 @@
package vtabutil_test
package sql3util_test
import (
"testing"
"github.com/ncruces/go-sqlite3/util/vtabutil"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
func TestParse(t *testing.T) {
tab, err := vtabutil.Parse(`CREATE TABLE child(x REFERENCES parent)`)
tab, err := sql3util.ParseTable(`CREATE TABLE child(x REFERENCES parent)`)
if err != nil {
t.Fatal(err)
}

View File

@@ -0,0 +1,9 @@
// Package sql3util implements SQLite utilities.
package sql3util
// ValidPageSize returns true if s is a valid page size.
//
// https://sqlite.org/fileformat.html#pages
func ValidPageSize(s int) bool {
return 512 <= s && s <= 65536 && s&(s-1) == 0
}

162
util/vfsutil/wrap.go Normal file
View File

@@ -0,0 +1,162 @@
// Package vfsutil implements virtual filesystem utilities.
package vfsutil
import (
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/vfs"
)
// UnwrapFile unwraps a [vfs.File],
// possibly implementing [vfs.FileUnwrap],
// to a concrete type.
func UnwrapFile[T vfs.File](f vfs.File) (_ T, _ bool) {
for {
switch t := f.(type) {
default:
return
case T:
return t, true
case vfs.FileUnwrap:
f = t.Unwrap()
}
}
}
// WrapOpenFilename helps wrap [vfs.VFSFilename].
func WrapOpenFilename(f vfs.VFS, name *vfs.Filename, flags vfs.OpenFlag) (file vfs.File, _ vfs.OpenFlag, err error) {
if f, ok := f.(vfs.VFSFilename); ok {
return f.OpenFilename(name, flags)
}
return f.Open(name.String(), flags)
}
// WrapLockState helps wrap [vfs.FileLockState].
func WrapLockState(f vfs.File) vfs.LockLevel {
if f, ok := f.(vfs.FileLockState); ok {
return f.LockState()
}
return vfs.LOCK_EXCLUSIVE + 1 // UNKNOWN_LOCK
}
// WrapPersistentWAL helps wrap [vfs.FilePersistentWAL].
func WrapPersistentWAL(f vfs.File) bool {
if f, ok := f.(vfs.FilePersistentWAL); ok {
return f.PersistentWAL()
}
return false
}
// WrapSetPersistentWAL helps wrap [vfs.FilePersistentWAL].
func WrapSetPersistentWAL(f vfs.File, keepWAL bool) {
if f, ok := f.(vfs.FilePersistentWAL); ok {
f.SetPersistentWAL(keepWAL)
}
}
// WrapPowersafeOverwrite helps wrap [vfs.FilePowersafeOverwrite].
func WrapPowersafeOverwrite(f vfs.File) bool {
if f, ok := f.(vfs.FilePowersafeOverwrite); ok {
return f.PowersafeOverwrite()
}
return false
}
// WrapSetPowersafeOverwrite helps wrap [vfs.FilePowersafeOverwrite].
func WrapSetPowersafeOverwrite(f vfs.File, psow bool) {
if f, ok := f.(vfs.FilePowersafeOverwrite); ok {
f.SetPowersafeOverwrite(psow)
}
}
// WrapChunkSize helps wrap [vfs.FileChunkSize].
func WrapChunkSize(f vfs.File, size int) {
if f, ok := f.(vfs.FileChunkSize); ok {
f.ChunkSize(size)
}
}
// WrapSizeHint helps wrap [vfs.FileSizeHint].
func WrapSizeHint(f vfs.File, size int64) error {
if f, ok := f.(vfs.FileSizeHint); ok {
return f.SizeHint(size)
}
return sqlite3.NOTFOUND
}
// WrapHasMoved helps wrap [vfs.FileHasMoved].
func WrapHasMoved(f vfs.File) (bool, error) {
if f, ok := f.(vfs.FileHasMoved); ok {
return f.HasMoved()
}
return false, sqlite3.NOTFOUND
}
// WrapOverwrite helps wrap [vfs.FileOverwrite].
func WrapOverwrite(f vfs.File) error {
if f, ok := f.(vfs.FileOverwrite); ok {
return f.Overwrite()
}
return sqlite3.NOTFOUND
}
// WrapCommitPhaseTwo helps wrap [vfs.FileCommitPhaseTwo].
func WrapCommitPhaseTwo(f vfs.File) error {
if f, ok := f.(vfs.FileCommitPhaseTwo); ok {
return f.CommitPhaseTwo()
}
return sqlite3.NOTFOUND
}
// WrapBeginAtomicWrite helps wrap [vfs.FileBatchAtomicWrite].
func WrapBeginAtomicWrite(f vfs.File) error {
if f, ok := f.(vfs.FileBatchAtomicWrite); ok {
return f.BeginAtomicWrite()
}
return sqlite3.NOTFOUND
}
// WrapCommitAtomicWrite helps wrap [vfs.FileBatchAtomicWrite].
func WrapCommitAtomicWrite(f vfs.File) error {
if f, ok := f.(vfs.FileBatchAtomicWrite); ok {
return f.CommitAtomicWrite()
}
return sqlite3.NOTFOUND
}
// WrapRollbackAtomicWrite helps wrap [vfs.FileBatchAtomicWrite].
func WrapRollbackAtomicWrite(f vfs.File) error {
if f, ok := f.(vfs.FileBatchAtomicWrite); ok {
return f.RollbackAtomicWrite()
}
return sqlite3.NOTFOUND
}
// WrapCheckpointStart helps wrap [vfs.FileCheckpoint].
func WrapCheckpointStart(f vfs.File) {
if f, ok := f.(vfs.FileCheckpoint); ok {
f.CheckpointStart()
}
}
// WrapCheckpointDone helps wrap [vfs.FileCheckpoint].
func WrapCheckpointDone(f vfs.File) {
if f, ok := f.(vfs.FileCheckpoint); ok {
f.CheckpointDone()
}
}
// WrapPragma helps wrap [vfs.FilePragma].
func WrapPragma(f vfs.File, name, value string) (string, error) {
if f, ok := f.(vfs.FilePragma); ok {
return f.Pragma(name, value)
}
return "", sqlite3.NOTFOUND
}
// WrapSharedMemory helps wrap [vfs.FileSharedMemory].
func WrapSharedMemory(f vfs.File) vfs.SharedMemory {
if f, ok := f.(vfs.FileSharedMemory); ok {
return f.SharedMemory()
}
return nil
}

View File

@@ -1,35 +0,0 @@
package vtabutil
import "strings"
// NamedArg splits an named arg into a key and value,
// around an equals sign.
// Spaces are trimmed around both key and value.
func NamedArg(arg string) (key, val string) {
key, val, _ = strings.Cut(arg, "=")
key = strings.TrimSpace(key)
val = strings.TrimSpace(val)
return
}
// Unquote unquotes a string.
func Unquote(val string) string {
if len(val) < 2 {
return val
}
if val[0] != val[len(val)-1] {
return val
}
var old, new string
switch val[0] {
default:
return val
case '"':
old, new = `""`, `"`
case '`':
old, new = "``", "`"
case '\'':
old, new = `''`, `'`
}
return strings.ReplaceAll(val[1:len(val)-1], old, new)
}

View File

@@ -1,2 +0,0 @@
// Package vtabutil implements virtual table utility functions.
package vtabutil

View File

@@ -4,7 +4,7 @@ This package implements the SQLite [OS Interface](https://sqlite.org/vfs.html) (
It replaces the default SQLite VFS with a **pure Go** implementation,
and exposes [interfaces](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs#VFS)
that should allow you to implement your own custom VFSes.
that should allow you to implement your own [custom VFSes](#custom-vfses).
Since it is a from scratch reimplementation,
there are naturally some ways it deviates from the original.
@@ -16,12 +16,12 @@ The main differences are [file locking](#file-locking) and [WAL mode](#write-ahe
POSIX advisory locks, which SQLite uses on Unix, are
[broken by design](https://github.com/sqlite/sqlite/blob/b74eb0/src/os_unix.c#L1073-L1161).
On Linux and macOS, this module uses
On Linux and macOS, this package uses
[OFD locks](https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html)
to synchronize access to database files.
OFD locks are fully compatible with POSIX advisory locks.
This module can also use
This package can also use
[BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2),
albeit with reduced concurrency (`BEGIN IMMEDIATE` behaves like `BEGIN EXCLUSIVE`).
On BSD, macOS, and illumos, BSD locks are fully compatible with POSIX advisory locks;
@@ -30,7 +30,7 @@ elsewhere, they are very likely broken.
BSD locks are the default on BSD and illumos,
but you can opt into them with the `sqlite3_flock` build tag.
On Windows, this module uses `LockFileEx` and `UnlockFileEx`,
On Windows, this package uses `LockFileEx` and `UnlockFileEx`,
like SQLite.
Otherwise, file locking is not supported, and you must use
@@ -46,18 +46,14 @@ to check if your build supports file locking.
### Write-Ahead Logging
On 64-bit little-endian Unix, this module uses `mmap` to implement
On little-endian Unix, this package uses `mmap` to implement
[shared-memory for the WAL-index](https://sqlite.org/wal.html#implementation_of_shared_memory_for_the_wal_index),
like SQLite.
To allow `mmap` to work, each connection needs to reserve up to 4GB of address space.
To limit the address space each connection reserves,
use [`WithMemoryLimitPages`](../tests/testcfg/testcfg.go).
With [BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2)
a WAL database can only be accessed by a single proccess.
Other processes that attempt to access a database locked with BSD locks,
will fail with the `SQLITE_PROTOCOL` error code.
will fail with the [`SQLITE_PROTOCOL`](https://sqlite.org/rescode.html#protocol) error code.
Otherwise, [WAL support is limited](https://sqlite.org/wal.html#noshm),
and `EXCLUSIVE` locking mode must be set to create, read, and write WAL databases.
@@ -71,9 +67,22 @@ to check if your build supports shared memory.
### Batch-Atomic Write
On 64-bit Linux, this module supports [batch-atomic writes](https://sqlite.org/cgi/src/technote/714)
On 64-bit Linux, this package supports
[batch-atomic writes](https://sqlite.org/cgi/src/technote/714)
on the F2FS filesystem.
### Checksums
This package can be [configured](https://pkg.go.dev/github.com/ncruces/go-sqlite3#Conn.EnableChecksums)
to add an 8-byte checksum to the end of every page in an SQLite database.
The checksum is added as each page is written
and verified as each page is read.\
The checksum is intended to help detect database corruption
caused by random bit-flips in the mass storage device.
The implementation is compatible with SQLite's
[Checksum VFS Shim](https://sqlite.org/cksumvfs.html).
### Build Tags
The VFS can be customized with a few build tags:
@@ -90,3 +99,14 @@ The VFS can be customized with a few build tags:
> [`unix-flock` VFS](https://sqlite.org/compile.html#enable_locking_style).
> If incompatible file locking is used, accessing databases concurrently with
> _other_ SQLite libraries will eventually corrupt data.
### Custom VFSes
- [`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.
- [`github.com/ncruces/go-sqlite3/vfs/xts`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/xts)
wraps a VFS to offer encryption at rest.

View File

@@ -11,7 +11,7 @@ The default Adiantum construction uses XChaCha12 for its stream cipher,
AES for its block cipher, and NH and Poly1305 for hashing.\
Additionally, we use [Argon2id](https://pkg.go.dev/golang.org/x/crypto/argon2#hdr-Argon2id)
to derive 256-bit keys from plain text where needed.
File contents are encrypted in 4K blocks, matching the
File contents are encrypted in 4 KiB blocks, matching the
[default](https://sqlite.org/pgszchng2016.html) SQLite page size.
The VFS encrypts all files _except_
@@ -39,10 +39,12 @@ This means that an adversary who can get ahold of multiple snapshots
(e.g. backups) of a database file can learn precisely:
which blocks changed, which ones didn't, which got reverted.
This is slightly weaker than other forms of SQLite encryption
that include *some* nondeterminism; with limited nondeterminism,
an adversary can't distinguish between
blocks that actually changed, and blocks that got reverted.
This is weaker than other forms of SQLite encryption
that include *some* nondeterminism.
With limited nondeterminism, an adversary can't distinguish between
pages that actually changed, and pages that got reverted;
a `VACUUM` can fully rebuild the database file,
preventing this differential analysis.
> [!CAUTION]
> This package does not claim protect databases against tampering or forgery.
@@ -52,7 +54,11 @@ if you're keeping `"adiantum"` encrypted backups of your database,
and want to protect against forgery, you should sign your backups,
and verify signatures before restoring them.
This is slightly weaker than other forms of SQLite encryption
that include block-level [MACs](https://en.wikipedia.org/wiki/Message_authentication_code).
Block-level MACs can protect against forging individual blocks,
This is weaker than other forms of SQLite encryption
that include page-level [MACs](https://en.wikipedia.org/wiki/Message_authentication_code).
Page-level MACs can protect against forging individual pages,
but can't prevent them from being reverted to former versions of themselves.
> [!TIP]
> The [`"xts"`](../xts/README.md) VFS also offers encryption at rest.
> AES-XTS uses _only_ NIST and FIPS 140 approved cryptographic primitives.

View File

@@ -20,8 +20,10 @@ import (
var testDB string
func Test_fileformat(t *testing.T) {
t.Parallel()
readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(testDB)))
adiantum.Register("radiantum", vfs.Find("reader"), nil)
vfs.Register("radiantum", adiantum.Wrap(vfs.Find("reader"), nil))
db, err := driver.Open("file:test.db?vfs=radiantum")
if err != nil {
@@ -42,6 +44,11 @@ func Test_fileformat(t *testing.T) {
if version != 0xBADDB {
t.Error(version)
}
_, err = db.Exec(`PRAGMA integrity_check`)
if err != nil {
t.Error(err)
}
}
func Benchmark_nokey(b *testing.B) {
@@ -57,6 +64,7 @@ func Benchmark_nokey(b *testing.B) {
db.Close()
}
}
func Benchmark_hexkey(b *testing.B) {
tmp := filepath.Join(b.TempDir(), "test.db")
sqlite3.Initialize()

View File

@@ -34,25 +34,31 @@
package adiantum
import (
"github.com/ncruces/go-sqlite3/vfs"
"lukechampine.com/adiantum/hbsh"
"github.com/ncruces/go-sqlite3/vfs"
)
func init() {
Register("adiantum", vfs.Find(""), nil)
vfs.Register("adiantum", Wrap(vfs.Find(""), nil))
}
// Register registers an encrypting VFS, wrapping a base VFS,
// and possibly using a custom HBSH cipher construction.
// Wrap wraps a base VFS to create an encrypting VFS,
// possibly using a custom HBSH cipher construction.
//
// To use the default Adiantum construction, set cipher to nil.
func Register(name string, base vfs.VFS, cipher HBSHCreator) {
//
// The default construction uses a 32 byte key/hexkey.
// If a textkey is provided, the default KDF is Argon2id
// with 64 MiB of memory, 3 iterations, and 4 threads.
func Wrap(base vfs.VFS, cipher HBSHCreator) vfs.VFS {
if cipher == nil {
cipher = adiantumCreator{}
}
vfs.Register(name, &hbshVFS{
return &hbshVFS{
VFS: base,
hbsh: cipher,
})
init: cipher,
}
}
// HBSHCreator creates an [hbsh.HBSH] cipher

View File

@@ -7,16 +7,17 @@ import (
"log"
"os"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/vfs"
"github.com/ncruces/go-sqlite3/vfs/adiantum"
"golang.org/x/crypto/argon2"
"lukechampine.com/adiantum/hbsh"
"lukechampine.com/adiantum/hpolyc"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/vfs"
"github.com/ncruces/go-sqlite3/vfs/adiantum"
)
func ExampleRegister_hpolyc() {
adiantum.Register("hpolyc", vfs.Find(""), hpolycCreator{})
vfs.Register("hpolyc", adiantum.Wrap(vfs.Find(""), hpolycCreator{}))
db, err := sqlite3.Open("file:demo.db?vfs=hpolyc" +
"&textkey=correct+horse+battery+staple")

View File

@@ -5,15 +5,17 @@ import (
"encoding/hex"
"io"
"lukechampine.com/adiantum/hbsh"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/vfsutil"
"github.com/ncruces/go-sqlite3/vfs"
"lukechampine.com/adiantum/hbsh"
)
type hbshVFS struct {
vfs.VFS
hbsh HBSHCreator
init HBSHCreator
}
func (h *hbshVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
@@ -22,11 +24,7 @@ func (h *hbshVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag,
}
func (h *hbshVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs.File, _ vfs.OpenFlag, err error) {
if hf, ok := h.VFS.(vfs.VFSFilename); ok {
file, flags, err = hf.OpenFilename(name, flags)
} else {
file, flags, err = h.VFS.Open(name.String(), flags)
}
file, flags, err = vfsutil.WrapOpenFilename(h.VFS, name, flags)
// Encrypt everything except super journals and memory files.
if err != nil || flags&(vfs.OPEN_SUPER_JOURNAL|vfs.OPEN_MEMORY) != 0 {
@@ -34,40 +32,57 @@ func (h *hbshVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs
}
var hbsh *hbsh.HBSH
if f, ok := name.DatabaseFile().(*hbshFile); ok {
if f, ok := vfsutil.UnwrapFile[*hbshFile](name.DatabaseFile()); ok {
hbsh = f.hbsh
} else {
var key []byte
if params := name.URIParameters(); name == nil {
key = h.hbsh.KDF("") // Temporary files get a random key.
key = h.init.KDF("") // Temporary files get a random key.
} else if t, ok := params["key"]; ok {
key = []byte(t[0])
} else if t, ok := params["hexkey"]; ok {
key, _ = hex.DecodeString(t[0])
} else if t, ok := params["textkey"]; ok {
key = h.hbsh.KDF(t[0])
} else if t, ok := params["textkey"]; ok && len(t[0]) > 0 {
key = h.init.KDF(t[0])
} else if flags&vfs.OPEN_MAIN_DB != 0 {
// Main datatabases may have their key specified as a PRAGMA.
return &hbshFile{File: file, reset: h.hbsh}, flags, nil
// Main databases may have their key specified as a PRAGMA.
return &hbshFile{File: file, init: h.init}, flags, nil
}
hbsh = h.hbsh.HBSH(key)
hbsh = h.init.HBSH(key)
}
if hbsh == nil {
file.Close()
return nil, flags, sqlite3.CANTOPEN
}
return &hbshFile{File: file, hbsh: hbsh, reset: h.hbsh}, flags, nil
return &hbshFile{File: file, hbsh: hbsh, init: h.init}, flags, nil
}
// Larger blocks improve both security (wide-block cipher)
// and throughput (cheap hashes amortize the block cipher's cost).
// Use the default SQLite page size;
// smaller pages pay the cost of unaligned access.
// https://sqlite.org/pgszchng2016.html
const (
tweakSize = 8
blockSize = 4096
)
// Ensure blockSize is a power of two.
var _ [0]struct{} = [blockSize & (blockSize - 1)]struct{}{}
func roundDown(i int64) int64 {
return i &^ (blockSize - 1)
}
func roundUp[T int | int64](i T) T {
return (i + (blockSize - 1)) &^ (blockSize - 1)
}
type hbshFile struct {
vfs.File
init HBSHCreator
hbsh *hbsh.HBSH
reset HBSHCreator
tweak [tweakSize]byte
block [blockSize]byte
}
@@ -80,15 +95,14 @@ func (h *hbshFile) Pragma(name string, value string) (string, error) {
case "hexkey":
key, _ = hex.DecodeString(value)
case "textkey":
key = h.reset.KDF(value)
default:
if f, ok := h.File.(vfs.FilePragma); ok {
return f.Pragma(name, value)
if len(value) > 0 {
key = h.init.KDF(value)
}
return "", sqlite3.NOTFOUND
default:
return vfsutil.WrapPragma(h.File, name, value)
}
if h.hbsh = h.reset.HBSH(key); h.hbsh != nil {
if h.hbsh = h.init.HBSH(key); h.hbsh != nil {
return "ok", nil
}
return "", sqlite3.CANTOPEN
@@ -99,14 +113,14 @@ func (h *hbshFile) ReadAt(p []byte, off int64) (n int, err error) {
// Only OPEN_MAIN_DB can have a missing key.
if off == 0 && len(p) == 100 {
// SQLite is trying to read the header of a database file.
// Pretend the file is empty so the key may specified as a PRAGMA.
// Pretend the file is empty so the key may be specified as a PRAGMA.
return 0, io.EOF
}
return 0, sqlite3.CANTOPEN
}
min := (off) &^ (blockSize - 1) // round down
max := (off + int64(len(p)) + (blockSize - 1)) &^ (blockSize - 1) // round up
min := roundDown(off)
max := roundUp(off + int64(len(p)))
// Read one block at a time.
for ; min < max; min += blockSize {
@@ -135,8 +149,8 @@ func (h *hbshFile) WriteAt(p []byte, off int64) (n int, err error) {
return 0, sqlite3.READONLY
}
min := (off) &^ (blockSize - 1) // round down
max := (off + int64(len(p)) + (blockSize - 1)) &^ (blockSize - 1) // round up
min := roundDown(off)
max := roundUp(off + int64(len(p)))
// Write one block at a time.
for ; min < max; min += blockSize {
@@ -181,99 +195,82 @@ func (h *hbshFile) WriteAt(p []byte, off int64) (n int, err error) {
return n, nil
}
func (h *hbshFile) Truncate(size int64) error {
size = (size + (blockSize - 1)) &^ (blockSize - 1) // round up
return h.File.Truncate(size)
}
func (h *hbshFile) SectorSize() int {
return lcm(h.File.SectorSize(), blockSize)
}
func (h *hbshFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
var _ [0]struct{} = [blockSize - 4096]struct{}{} // Ensure blockSize is 4K.
return h.File.DeviceCharacteristics() & (0 |
// The only safe flags are these:
vfs.IOCAP_ATOMIC4K |
vfs.IOCAP_UNDELETABLE_WHEN_OPEN |
vfs.IOCAP_IMMUTABLE |
vfs.IOCAP_BATCH_ATOMIC)
}
// Wrap optional methods.
func (h *hbshFile) SectorSize() int {
return util.LCM(h.File.SectorSize(), blockSize)
}
func (h *hbshFile) SharedMemory() vfs.SharedMemory {
if f, ok := h.File.(vfs.FileSharedMemory); ok {
return f.SharedMemory()
}
return nil
func (h *hbshFile) Truncate(size int64) error {
return h.File.Truncate(roundUp(size))
}
func (h *hbshFile) ChunkSize(size int) {
if f, ok := h.File.(vfs.FileChunkSize); ok {
size = (size + (blockSize - 1)) &^ (blockSize - 1) // round up
f.ChunkSize(size)
}
vfsutil.WrapChunkSize(h.File, roundUp(size))
}
func (h *hbshFile) SizeHint(size int64) error {
if f, ok := h.File.(vfs.FileSizeHint); ok {
size = (size + (blockSize - 1)) &^ (blockSize - 1) // round up
return f.SizeHint(size)
}
return sqlite3.NOTFOUND
return vfsutil.WrapSizeHint(h.File, roundUp(size))
}
func (h *hbshFile) Unwrap() vfs.File {
return h.File
}
func (h *hbshFile) SharedMemory() vfs.SharedMemory {
return vfsutil.WrapSharedMemory(h.File)
}
// Wrap optional methods.
func (h *hbshFile) LockState() vfs.LockLevel {
return vfsutil.WrapLockState(h.File) // notest
}
func (h *hbshFile) PersistentWAL() bool {
return vfsutil.WrapPersistentWAL(h.File) // notest
}
func (h *hbshFile) SetPersistentWAL(keepWAL bool) {
vfsutil.WrapSetPersistentWAL(h.File, keepWAL) // notest
}
func (h *hbshFile) HasMoved() (bool, error) {
if f, ok := h.File.(vfs.FileHasMoved); ok {
return f.HasMoved()
}
return false, sqlite3.NOTFOUND
return vfsutil.WrapHasMoved(h.File) // notest
}
func (h *hbshFile) Overwrite() error {
if f, ok := h.File.(vfs.FileOverwrite); ok {
return f.Overwrite()
}
return sqlite3.NOTFOUND
return vfsutil.WrapOverwrite(h.File) // notest
}
func (h *hbshFile) CommitPhaseTwo() error {
if f, ok := h.File.(vfs.FileCommitPhaseTwo); ok {
return f.CommitPhaseTwo()
}
return sqlite3.NOTFOUND
return vfsutil.WrapCommitPhaseTwo(h.File) // notest
}
func (h *hbshFile) BeginAtomicWrite() error {
if f, ok := h.File.(vfs.FileBatchAtomicWrite); ok {
return f.BeginAtomicWrite()
}
return sqlite3.NOTFOUND
return vfsutil.WrapBeginAtomicWrite(h.File) // notest
}
func (h *hbshFile) CommitAtomicWrite() error {
if f, ok := h.File.(vfs.FileBatchAtomicWrite); ok {
return f.CommitAtomicWrite()
}
return sqlite3.NOTFOUND
return vfsutil.WrapCommitAtomicWrite(h.File) // notest
}
func (h *hbshFile) RollbackAtomicWrite() error {
if f, ok := h.File.(vfs.FileBatchAtomicWrite); ok {
return f.RollbackAtomicWrite()
}
return sqlite3.NOTFOUND
return vfsutil.WrapRollbackAtomicWrite(h.File) // notest
}
func (h *hbshFile) CheckpointDone() error {
if f, ok := h.File.(vfs.FileCheckpoint); ok {
return f.CheckpointDone()
}
return sqlite3.NOTFOUND
func (h *hbshFile) CheckpointStart() {
vfsutil.WrapCheckpointStart(h.File) // notest
}
func (h *hbshFile) CheckpointStart() error {
if f, ok := h.File.(vfs.FileCheckpoint); ok {
return f.CheckpointStart()
}
return sqlite3.NOTFOUND
func (h *hbshFile) CheckpointDone() {
vfsutil.WrapCheckpointDone(h.File) // notest
}

View File

@@ -1,22 +0,0 @@
package adiantum
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))
}

Binary file not shown.

View File

@@ -49,6 +49,13 @@ type File interface {
DeviceCharacteristics() DeviceCharacteristic
}
// FileUnwrap should be implemented by a File
// that wraps another File implementation.
type FileUnwrap interface {
File
Unwrap() File
}
// FileLockState extends File to implement the
// SQLITE_FCNTL_LOCKSTATE file control opcode.
//
@@ -58,6 +65,26 @@ type FileLockState interface {
LockState() LockLevel
}
// FilePersistentWAL extends File to implement the
// SQLITE_FCNTL_PERSIST_WAL file control opcode.
//
// https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlpersistwal
type FilePersistentWAL interface {
File
PersistentWAL() bool
SetPersistentWAL(bool)
}
// FilePowersafeOverwrite extends File to implement the
// SQLITE_FCNTL_POWERSAFE_OVERWRITE file control opcode.
//
// https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlpowersafeoverwrite
type FilePowersafeOverwrite interface {
File
PowersafeOverwrite() bool
SetPowersafeOverwrite(bool)
}
// FileChunkSize extends File to implement the
// SQLITE_FCNTL_CHUNK_SIZE file control opcode.
//
@@ -94,26 +121,6 @@ type FileOverwrite interface {
Overwrite() error
}
// FilePersistentWAL extends File to implement the
// SQLITE_FCNTL_PERSIST_WAL file control opcode.
//
// https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlpersistwal
type FilePersistentWAL interface {
File
PersistentWAL() bool
SetPersistentWAL(bool)
}
// FilePowersafeOverwrite extends File to implement the
// SQLITE_FCNTL_POWERSAFE_OVERWRITE file control opcode.
//
// https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlpowersafeoverwrite
type FilePowersafeOverwrite interface {
File
PowersafeOverwrite() bool
SetPowersafeOverwrite(bool)
}
// FileCommitPhaseTwo extends File to implement the
// SQLITE_FCNTL_COMMIT_PHASETWO file control opcode.
//
@@ -135,15 +142,6 @@ type FileBatchAtomicWrite interface {
RollbackAtomicWrite() error
}
// FilePragma extends File to implement the
// SQLITE_FCNTL_PRAGMA file control opcode.
//
// https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlpragma
type FilePragma interface {
File
Pragma(name, value string) (string, error)
}
// FileCheckpoint extends File to implement the
// SQLITE_FCNTL_CKPT_START and SQLITE_FCNTL_CKPT_DONE
// file control opcodes.
@@ -151,8 +149,17 @@ type FilePragma interface {
// https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlckptstart
type FileCheckpoint interface {
File
CheckpointDone() error
CheckpointStart() error
CheckpointStart()
CheckpointDone()
}
// FilePragma extends File to implement the
// SQLITE_FCNTL_PRAGMA file control opcode.
//
// https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlpragma
type FilePragma interface {
File
Pragma(name, value string) (string, error)
}
// FileSharedMemory extends File to possibly implement
@@ -171,5 +178,16 @@ type SharedMemory interface {
shmMap(context.Context, api.Module, int32, int32, bool) (uint32, _ErrorCode)
shmLock(int32, int32, _ShmFlag) _ErrorCode
shmUnmap(bool)
shmBarrier()
io.Closer
}
type blockingSharedMemory interface {
SharedMemory
shmEnableBlocking(block bool)
}
type fileControl interface {
File
fileControl(ctx context.Context, mod api.Module, op _FcntlOpcode, pArg uint32) _ErrorCode
}

149
vfs/cksm.go Normal file
View File

@@ -0,0 +1,149 @@
package vfs
import (
"bytes"
"context"
_ "embed"
"encoding/binary"
"strconv"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
func cksmWrapFile(name *Filename, flags OpenFlag, file File) File {
// Checksum only main databases and WALs.
if flags&(OPEN_MAIN_DB|OPEN_WAL) == 0 {
return file
}
cksm := cksmFile{File: file}
if flags&OPEN_WAL != 0 {
main, _ := name.DatabaseFile().(cksmFile)
cksm.cksmFlags = main.cksmFlags
} else {
cksm.cksmFlags = new(cksmFlags)
cksm.isDB = true
}
return cksm
}
type cksmFile struct {
File
*cksmFlags
isDB bool
}
type cksmFlags struct {
computeCksm bool
verifyCksm bool
inCkpt bool
pageSize int
}
func (c cksmFile) ReadAt(p []byte, off int64) (n int, err error) {
n, err = c.File.ReadAt(p, off)
// SQLite is reading the header of a database file.
if c.isDB && off == 0 && len(p) >= 100 &&
bytes.HasPrefix(p, []byte("SQLite format 3\000")) {
c.init(p)
}
// Verify checksums.
if c.verifyCksm && !c.inCkpt && len(p) == c.pageSize {
cksm1 := cksmCompute(p[:len(p)-8])
cksm2 := *(*[8]byte)(p[len(p)-8:])
if cksm1 != cksm2 {
return 0, _IOERR_DATA
}
}
return n, err
}
func (c cksmFile) WriteAt(p []byte, off int64) (n int, err error) {
// SQLite is writing the first page of a database file.
if c.isDB && off == 0 && len(p) >= 100 &&
bytes.HasPrefix(p, []byte("SQLite format 3\000")) {
c.init(p)
}
// Compute checksums.
if c.computeCksm && !c.inCkpt && len(p) == c.pageSize {
*(*[8]byte)(p[len(p)-8:]) = cksmCompute(p[:len(p)-8])
}
return c.File.WriteAt(p, off)
}
func (c cksmFile) Pragma(name string, value string) (string, error) {
switch name {
case "checksum_verification":
b, ok := sql3util.ParseBool(value)
if ok {
c.verifyCksm = b && c.computeCksm
}
if !c.verifyCksm {
return "0", nil
}
return "1", nil
case "page_size":
if c.computeCksm {
// Do not allow page size changes on a checksum database.
return strconv.Itoa(c.pageSize), nil
}
}
return "", _NOTFOUND
}
func (c cksmFile) fileControl(ctx context.Context, mod api.Module, op _FcntlOpcode, pArg uint32) _ErrorCode {
switch op {
case _FCNTL_CKPT_START:
c.inCkpt = true
case _FCNTL_CKPT_DONE:
c.inCkpt = false
}
if rc := vfsFileControlImpl(ctx, mod, c, op, pArg); rc != _NOTFOUND {
return rc
}
return vfsFileControlImpl(ctx, mod, c.File, op, pArg)
}
func (f *cksmFlags) init(header []byte) {
f.pageSize = 256 * int(binary.LittleEndian.Uint16(header[16:18]))
if r := header[20] == 8; r != f.computeCksm {
f.computeCksm = r
f.verifyCksm = r
}
}
func cksmCompute(a []byte) (cksm [8]byte) {
var s1, s2 uint32
for len(a) >= 8 {
s1 += binary.LittleEndian.Uint32(a[0:4]) + s2
s2 += binary.LittleEndian.Uint32(a[4:8]) + s1
a = a[8:]
}
if len(a) != 0 {
panic(util.AssertErr())
}
binary.LittleEndian.PutUint32(cksm[0:4], s1)
binary.LittleEndian.PutUint32(cksm[4:8], s2)
return
}
func (c cksmFile) SharedMemory() SharedMemory {
if f, ok := c.File.(FileSharedMemory); ok {
return f.SharedMemory()
}
return nil
}
func (c cksmFile) Unwrap() File {
return c.File
}

View File

@@ -51,6 +51,7 @@ const (
_IOERR_BEGIN_ATOMIC _ErrorCode = util.IOERR_BEGIN_ATOMIC
_IOERR_COMMIT_ATOMIC _ErrorCode = util.IOERR_COMMIT_ATOMIC
_IOERR_ROLLBACK_ATOMIC _ErrorCode = util.IOERR_ROLLBACK_ATOMIC
_IOERR_DATA _ErrorCode = util.IOERR_DATA
_BUSY_SNAPSHOT _ErrorCode = util.BUSY_SNAPSHOT
_CANTOPEN_FULLPATH _ErrorCode = util.CANTOPEN_FULLPATH
_CANTOPEN_ISDIR _ErrorCode = util.CANTOPEN_ISDIR

View File

@@ -4,8 +4,9 @@ import (
"context"
"net/url"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Filename is used by SQLite to pass filenames

View File

@@ -6,8 +6,9 @@ import (
"path/filepath"
"testing"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/tetratelabs/wazero/experimental/wazerotest"
"github.com/ncruces/go-sqlite3/internal/util"
)
func Test_vfsLock(t *testing.T) {

View File

@@ -2,11 +2,10 @@ package memdb_test
import (
"database/sql"
_ "embed"
"fmt"
"log"
_ "embed"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/vfs/memdb"

View File

@@ -1,9 +1,8 @@
package memdb
import (
"testing"
_ "embed"
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
@@ -14,6 +13,8 @@ import (
var walDB []byte
func Test_wal(t *testing.T) {
t.Parallel()
Create("test.db", walDB)
db, err := sqlite3.Open("file:/test.db?vfs=memdb")

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