mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-19 09:04:16 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2545534af | ||
|
|
69e5cf706b | ||
|
|
75c1dbb052 | ||
|
|
64e2500ca8 | ||
|
|
0cd0f48365 | ||
|
|
c69ee0fe8d | ||
|
|
b9b489aae9 | ||
|
|
21de004779 | ||
|
|
9eec439d35 | ||
|
|
fefee692db | ||
|
|
f18561ee11 | ||
|
|
ace01b2927 | ||
|
|
89f750a6e9 | ||
|
|
d6aebe67cc | ||
|
|
714ea0e779 | ||
|
|
c900889848 | ||
|
|
50c8517603 | ||
|
|
c78d00dca0 | ||
|
|
ddfaf12cd8 | ||
|
|
368c900db8 | ||
|
|
e524fb185d |
4
.github/workflows/repro.sh
vendored
4
.github/workflows/repro.sh
vendored
@@ -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
|
||||
2
.github/workflows/repro.yml
vendored
2
.github/workflows/repro.yml
vendored
@@ -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
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -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:
|
||||
|
||||
45
README.md
45
README.md
@@ -1,4 +1,4 @@
|
||||
# Go bindings to SQLite using Wazero
|
||||
# Go bindings to SQLite using wazero
|
||||
|
||||
[](https://pkg.go.dev/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
|
||||
|
||||
1
blob.go
1
blob.go
@@ -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
138
config.go
@@ -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
17
conn.go
@@ -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
|
||||
}
|
||||
|
||||
3
const.go
3
const.go
@@ -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].
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
37
ext/README.md
Normal 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.
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/dchest/siphash"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
BIN
ext/bloom/testdata/bloom.db
vendored
BIN
ext/bloom/testdata/bloom.db
vendored
Binary file not shown.
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
3
func.go
3
func.go
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
29
internal/util/math.go
Normal 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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
14
sqlite.go
14
sqlite.go
@@ -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)
|
||||
|
||||
@@ -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 ){
|
||||
|
||||
@@ -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 ~-
|
||||
@@ -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 );
|
||||
107
sqlite3/vfs.c
107
sqlite3/vfs.c
@@ -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");
|
||||
|
||||
@@ -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 *******************************************/
|
||||
/*
|
||||
|
||||
@@ -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
75
tests/cksm_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
BIN
tests/testdata/cksm.db
vendored
Normal file
Binary file not shown.
BIN
tests/testdata/utf16be.db
vendored
BIN
tests/testdata/utf16be.db
vendored
Binary file not shown.
BIN
tests/testdata/wal.db
vendored
BIN
tests/testdata/wal.db
vendored
Binary file not shown.
@@ -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:")
|
||||
|
||||
@@ -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
12
txn.go
@@ -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
4
util/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Go SQLite Utilities
|
||||
|
||||
This folder collects additional SQLite utilities
|
||||
that help extension writers provide a consistent developer experience.
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package fsutil implements filesystem utility functions.
|
||||
// Package fsutil implements filesystem utilities.
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Package ioutil implements I/O utility functions.
|
||||
// Package ioutil implements I/O utilities.
|
||||
package ioutil
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Package osutil implements operating system utility functions.
|
||||
// Package osutil implements operating system utilities.
|
||||
package osutil
|
||||
|
||||
@@ -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
65
util/sql3util/arg.go
Normal 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
55
util/sql3util/arg_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package vtabutil
|
||||
package sql3util
|
||||
|
||||
const (
|
||||
_NONE = iota
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
}
|
||||
9
util/sql3util/sql3util.go
Normal file
9
util/sql3util/sql3util.go
Normal 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
162
util/vfsutil/wrap.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package vtabutil implements virtual table utility functions.
|
||||
package vtabutil
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
BIN
vfs/adiantum/testdata/test.db
vendored
BIN
vfs/adiantum/testdata/test.db
vendored
Binary file not shown.
80
vfs/api.go
80
vfs/api.go
@@ -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
149
vfs/cksm.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user