├── vfs ├── tests │ ├── mptest │ │ ├── wasm │ │ │ ├── go.mod │ │ │ ├── .gitignore │ │ │ ├── mptest.wasm │ │ │ ├── exports.txt │ │ │ ├── main.c │ │ │ └── build.sh │ │ └── testdata │ │ │ ├── .gitattributes │ │ │ ├── config01.test │ │ │ └── crash02.subtest │ └── speedtest1 │ │ └── wasm │ │ ├── go.mod │ │ ├── .gitignore │ │ ├── speedtest1.wasm │ │ ├── exports.txt │ │ ├── main.c │ │ └── build.sh ├── mvcc │ ├── testdata │ │ ├── wal.db │ │ └── test.db │ ├── README.md │ ├── mvcc_test.go │ └── example_test.go ├── xts │ ├── testdata │ │ └── test.db │ ├── aes.go │ ├── aes_test.go │ └── api.go ├── memdb │ ├── testdata │ │ ├── test.db │ │ └── wal.db │ ├── README.md │ ├── memdb_test.go │ └── example_test.go ├── adiantum │ ├── testdata │ │ └── test.db │ ├── adiantum.go │ ├── example_test.go │ ├── adiantum_test.go │ └── api.go ├── readervfs │ ├── testdata │ │ └── test.db │ ├── README.md │ ├── api.go │ ├── reader.go │ └── example_test.go ├── os_std_atomic.go ├── os_std_sync.go ├── os_std_rw.go ├── os_std_alloc.go ├── const_test.go ├── lock_other.go ├── os_std.go ├── os_unix_test.go ├── os_f2fs_linux.go ├── shm_other.go ├── shm.go ├── registry.go ├── registry_test.go ├── shm_memlk.go ├── os_ofd.go ├── os_linux.go └── os_unix.go ├── embed ├── bcw2 │ ├── .gitignore │ ├── bcw2.wasm │ ├── go.mod │ ├── init.go │ ├── go.sum │ ├── README.md │ └── bcw2_test.go ├── sqlite3.wasm ├── init_test.go ├── init.go ├── build.sh └── README.md ├── sqlite3 ├── .gitignore ├── libc │ ├── libc.wasm │ ├── benchmark.sh │ ├── math.h │ ├── build.sh │ └── stdlib.h ├── format.sh ├── include.h ├── busy_timeout.patch ├── pointer.c ├── vfs_find.patch ├── text.c ├── sqlite_cfg.h ├── main.c ├── time.c ├── tools.sh ├── sqlite_opt.h ├── download.sh ├── stmt.c └── hooks.c ├── util ├── sql3util │ ├── wasm │ │ ├── .gitignore │ │ ├── sql3parse_table.wasm │ │ ├── download.sh │ │ └── build.sh │ ├── sql3util.go │ ├── README.md │ ├── parse_test.go │ └── const.go ├── ioutil │ ├── ioutil.go │ ├── seek_test.go │ ├── size.go │ ├── seek.go │ └── size_test.go ├── README.md ├── osutil │ └── osfs.go └── fsutil │ ├── mode_test.go │ └── mode.go ├── internal ├── util │ ├── pointer.go │ ├── mmap_other.go │ ├── reflect.go │ ├── set.go │ ├── error_test.go │ ├── reflect_test.go │ ├── math.go │ ├── module.go │ ├── json.go │ ├── json_v2.go │ ├── mmap_windows.go │ ├── handle.go │ ├── math_test.go │ └── mmap_unix.go ├── alloc │ ├── alloc_test.go │ ├── alloc_other.go │ ├── alloc_unix.go │ └── alloc_windows.go ├── dotlk │ ├── dotlk_other.go │ ├── dotlk.go │ └── dotlk_unix.go └── testcfg │ └── testcfg.go ├── .github ├── FUNDING.yml ├── actions │ └── vmactions │ │ └── template.yml ├── workflows │ ├── repro.sh │ ├── build-test.sh │ ├── libc.yml │ └── repro.yml └── dependabot.yml ├── tests ├── testdata │ ├── cksm.db │ ├── wal.db │ ├── f2fs.img.gz │ ├── utf16be.db │ └── f2fs.sh ├── compile │ ├── nil │ │ └── compile_test.go │ ├── missing │ │ └── compile_test.go │ └── empty │ │ └── compile_test.go ├── vtab_test.go ├── type_test.go ├── vfs_test.go ├── ext_test.go ├── endian_test.go ├── cksm_test.go ├── json_test.go └── quote_test.go ├── ext ├── bloom │ └── testdata │ │ └── bloom.db ├── serdes │ ├── testdata │ │ └── wal.db │ └── serdes.go ├── stats │ ├── kahan.go │ ├── boolean.go │ ├── TODO.md │ ├── boolean_test.go │ ├── welford_test.go │ └── moments_test.go ├── hash │ ├── sha3.go │ ├── blake2.go │ └── sha2.go ├── csv │ ├── types_test.go │ ├── arg.go │ ├── schema.go │ ├── schema_test.go │ └── types.go ├── pivot │ └── op_test.go ├── fileio │ ├── fileio_test.go │ ├── fileio.go │ ├── fsdir_test.go │ ├── write_test.go │ └── write.go ├── zorder │ └── zorder.go └── ipaddr │ └── ipaddr_test.go ├── .gitignore ├── pointer.go ├── gormlite ├── README.md ├── go.mod ├── error_translator.go ├── tests.patch ├── test.sh ├── update.sh ├── LICENSE ├── error_translator_test.go ├── go.sum ├── ddlmod_parse_all_columns_test.go └── sqlite_test.go ├── litestream ├── README.md ├── example_test.go ├── cache.go ├── time.go └── api.go ├── go.mod ├── registry.go ├── driver ├── savepoint.go ├── time.go ├── util.go ├── savepoint_test.go ├── util_test.go └── json_test.go ├── example_test.go ├── LICENSE ├── func_seq_test.go ├── func_win_test.go └── go.sum /vfs/tests/mptest/wasm/go.mod: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vfs/tests/speedtest1/wasm/go.mod: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /embed/bcw2/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | sqlite/ -------------------------------------------------------------------------------- /vfs/tests/mptest/wasm/.gitignore: -------------------------------------------------------------------------------- 1 | mptest.c -------------------------------------------------------------------------------- /vfs/tests/mptest/testdata/.gitattributes: -------------------------------------------------------------------------------- 1 | * -crlf -------------------------------------------------------------------------------- /vfs/tests/speedtest1/wasm/.gitignore: -------------------------------------------------------------------------------- 1 | speedtest1.c -------------------------------------------------------------------------------- /sqlite3/.gitignore: -------------------------------------------------------------------------------- 1 | ext/ 2 | sqlite3.c 3 | sqlite3.h 4 | sqlite3ext.h -------------------------------------------------------------------------------- /util/sql3util/wasm/.gitignore: -------------------------------------------------------------------------------- 1 | sql3parse_table.c 2 | sql3parse_table.h -------------------------------------------------------------------------------- /internal/util/pointer.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | type Pointer struct{ Value any } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.com/donate?hosted_button_id=33P59ELZWGMK6 -------------------------------------------------------------------------------- /util/ioutil/ioutil.go: -------------------------------------------------------------------------------- 1 | // Package ioutil implements I/O utilities. 2 | package ioutil 3 | -------------------------------------------------------------------------------- /embed/sqlite3.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/embed/sqlite3.wasm -------------------------------------------------------------------------------- /embed/bcw2/bcw2.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/embed/bcw2/bcw2.wasm -------------------------------------------------------------------------------- /sqlite3/libc/libc.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/sqlite3/libc/libc.wasm -------------------------------------------------------------------------------- /tests/testdata/cksm.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/tests/testdata/cksm.db -------------------------------------------------------------------------------- /tests/testdata/wal.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/tests/testdata/wal.db -------------------------------------------------------------------------------- /internal/util/mmap_other.go: -------------------------------------------------------------------------------- 1 | //go:build !unix 2 | 3 | package util 4 | 5 | type mmapState struct{} 6 | -------------------------------------------------------------------------------- /vfs/mvcc/testdata/wal.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/vfs/mvcc/testdata/wal.db -------------------------------------------------------------------------------- /vfs/xts/testdata/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/vfs/xts/testdata/test.db -------------------------------------------------------------------------------- /ext/bloom/testdata/bloom.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/ext/bloom/testdata/bloom.db -------------------------------------------------------------------------------- /ext/serdes/testdata/wal.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/ext/serdes/testdata/wal.db -------------------------------------------------------------------------------- /tests/testdata/f2fs.img.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/tests/testdata/f2fs.img.gz -------------------------------------------------------------------------------- /tests/testdata/utf16be.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/tests/testdata/utf16be.db -------------------------------------------------------------------------------- /vfs/memdb/testdata/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/vfs/memdb/testdata/test.db -------------------------------------------------------------------------------- /vfs/memdb/testdata/wal.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/vfs/memdb/testdata/wal.db -------------------------------------------------------------------------------- /vfs/mvcc/testdata/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/vfs/mvcc/testdata/test.db -------------------------------------------------------------------------------- /vfs/adiantum/testdata/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/vfs/adiantum/testdata/test.db -------------------------------------------------------------------------------- /vfs/readervfs/testdata/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/vfs/readervfs/testdata/test.db -------------------------------------------------------------------------------- /vfs/tests/mptest/wasm/mptest.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/vfs/tests/mptest/wasm/mptest.wasm -------------------------------------------------------------------------------- /util/sql3util/wasm/sql3parse_table.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/util/sql3util/wasm/sql3parse_table.wasm -------------------------------------------------------------------------------- /vfs/tests/mptest/wasm/exports.txt: -------------------------------------------------------------------------------- 1 | aligned_alloc 2 | sqlite3_database_file_object 3 | sqlite3_free 4 | sqlite3_malloc64 5 | sqlite3_uri_key -------------------------------------------------------------------------------- /vfs/tests/speedtest1/wasm/speedtest1.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncruces/go-sqlite3/HEAD/vfs/tests/speedtest1/wasm/speedtest1.wasm -------------------------------------------------------------------------------- /vfs/tests/speedtest1/wasm/exports.txt: -------------------------------------------------------------------------------- 1 | aligned_alloc 2 | sqlite3_database_file_object 3 | sqlite3_free 4 | sqlite3_malloc64 5 | sqlite3_uri_key -------------------------------------------------------------------------------- /sqlite3/format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | shopt -s extglob 7 | clang-format -i !(sqlite3*).@(c|h) -------------------------------------------------------------------------------- /util/README.md: -------------------------------------------------------------------------------- 1 | # Go SQLite Utilities 2 | 3 | This folder collects additional SQLite utilities 4 | that help extension writers provide a consistent developer experience. -------------------------------------------------------------------------------- /vfs/os_std_atomic.go: -------------------------------------------------------------------------------- 1 | //go:build !linux || !(amd64 || arm64 || riscv64) 2 | 3 | package vfs 4 | 5 | import "os" 6 | 7 | func osBatchAtomic(*os.File) bool { 8 | return false 9 | } 10 | -------------------------------------------------------------------------------- /vfs/os_std_sync.go: -------------------------------------------------------------------------------- 1 | //go:build !(linux || darwin) || sqlite3_flock 2 | 3 | package vfs 4 | 5 | import "os" 6 | 7 | func osSync(file *os.File, _ OpenFlag, _ SyncFlag) error { 8 | return file.Sync() 9 | } 10 | -------------------------------------------------------------------------------- /vfs/readervfs/README.md: -------------------------------------------------------------------------------- 1 | # Go `reader` SQLite VFS 2 | 3 | This package implements a `"reader"` SQLite VFS 4 | that allows accessing any [`io.ReaderAt`](https://pkg.go.dev/io#ReaderAt) 5 | as an immutable SQLite database. -------------------------------------------------------------------------------- /internal/util/reflect.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "reflect" 4 | 5 | func ReflectType(v reflect.Value) reflect.Type { 6 | if v.Kind() != reflect.Invalid { 7 | return v.Type() 8 | } 9 | return nil 10 | } 11 | -------------------------------------------------------------------------------- /sqlite3/libc/benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | touch empty.S 7 | ./build.sh empty.S 8 | go test -bench=. 9 | rm -f empty.S 10 | 11 | ./build.sh 12 | go test -bench=. -------------------------------------------------------------------------------- /internal/util/set.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | type Set[E comparable] map[E]struct{} 4 | 5 | func (s Set[E]) Add(v E) { 6 | s[v] = struct{}{} 7 | } 8 | 9 | func (s Set[E]) Contains(v E) bool { 10 | _, ok := s[v] 11 | return ok 12 | } 13 | -------------------------------------------------------------------------------- /.github/actions/vmactions/template.yml: -------------------------------------------------------------------------------- 1 | name: VM Actions matrix 2 | description: VM Actions matrix template 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: ${VMACTIONS} 8 | with: 9 | usesh: true 10 | copyback: false 11 | run: . ./test.sh -------------------------------------------------------------------------------- /internal/util/error_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestErrorJoiner(t *testing.T) { 6 | var errs ErrorJoiner 7 | errs.Join(NilErr, OOMErr) 8 | for i, e := range []error{NilErr, OOMErr} { 9 | if e != errs[i] { 10 | t.Fail() 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/compile/nil/compile_test.go: -------------------------------------------------------------------------------- 1 | package compile 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | ) 8 | 9 | func TestCompile_nil(t *testing.T) { 10 | _, err := sqlite3.Open(":memory:") 11 | if err == nil { 12 | t.Error("want error") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /util/sql3util/wasm/download.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | curl -#OL "https://github.com/ncruces/sqlite-createtable-parser/raw/master/sql3parse_table.c" 7 | curl -#OL "https://github.com/ncruces/sqlite-createtable-parser/raw/master/sql3parse_table.h" -------------------------------------------------------------------------------- /util/sql3util/sql3util.go: -------------------------------------------------------------------------------- 1 | // Package sql3util implements SQLite utilities. 2 | package sql3util 3 | 4 | // ValidPageSize returns true if s is a valid page size. 5 | // 6 | // https://sqlite.org/fileformat.html#pages 7 | func ValidPageSize(s int) bool { 8 | return s&(s-1) == 0 && 512 <= s && s <= 65536 9 | } 10 | -------------------------------------------------------------------------------- /internal/alloc/alloc_test.go: -------------------------------------------------------------------------------- 1 | package alloc_test 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/ncruces/go-sqlite3/internal/alloc" 8 | ) 9 | 10 | func TestVirtual(t *testing.T) { 11 | defer func() { _ = recover() }() 12 | alloc.NewMemory(math.MaxInt+2, math.MaxInt+2) 13 | t.Error("want panic") 14 | } 15 | -------------------------------------------------------------------------------- /tests/compile/missing/compile_test.go: -------------------------------------------------------------------------------- 1 | package compile 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | ) 8 | 9 | func TestCompile_missing(t *testing.T) { 10 | sqlite3.Path = "sqlite3.wasm" 11 | _, err := sqlite3.Open(":memory:") 12 | if err == nil { 13 | t.Error("want error") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vfs/os_std_rw.go: -------------------------------------------------------------------------------- 1 | //go:build !unix && (!windows || sqlite3_dotlk) 2 | 3 | package vfs 4 | 5 | import "os" 6 | 7 | func osReadAt(file *os.File, p []byte, off int64) (int, error) { 8 | return file.ReadAt(p, off) 9 | } 10 | 11 | func osWriteAt(file *os.File, p []byte, off int64) (int, error) { 12 | return file.WriteAt(p, off) 13 | } 14 | -------------------------------------------------------------------------------- /tests/compile/empty/compile_test.go: -------------------------------------------------------------------------------- 1 | package compile 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | ) 8 | 9 | func TestCompile_empty(t *testing.T) { 10 | sqlite3.Binary = []byte("\x00asm\x01\x00\x00\x00") 11 | _, err := sqlite3.Open(":memory:") 12 | if err == nil { 13 | t.Error("want error") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/dotlk/dotlk_other.go: -------------------------------------------------------------------------------- 1 | //go:build !unix 2 | 3 | package dotlk 4 | 5 | import "os" 6 | 7 | // TryLock returns nil if it acquired the lock, 8 | // fs.ErrExist if another process has the lock. 9 | func TryLock(name string) error { 10 | f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) 11 | f.Close() 12 | return err 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/repro.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Download and build SQLite 5 | sqlite3/download.sh 6 | sqlite3/tools.sh 7 | embed/build.sh 8 | embed/bcw2/build.sh 9 | 10 | # Download and build sqlite-createtable-parser 11 | util/sql3util/wasm/download.sh 12 | util/sql3util/wasm/build.sh 13 | 14 | # Check diffs 15 | git diff --exit-code -------------------------------------------------------------------------------- /vfs/tests/speedtest1/wasm/main.c: -------------------------------------------------------------------------------- 1 | // Use the default callback, not the Go one we patched in. 2 | #define sqliteBusyCallback sqliteDefaultBusyCallback 3 | 4 | // Amalgamation 5 | #include "sqlite3.c" 6 | // VFS 7 | #include "vfs.c" 8 | 9 | // Can't have two functions with the same name. 10 | #define randomFunc randomFuncRepeatable 11 | 12 | #include "speedtest1.c" -------------------------------------------------------------------------------- /embed/bcw2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ncruces/go-sqlite3/embed/bcw2 2 | 3 | go 1.24.0 4 | 5 | require github.com/ncruces/go-sqlite3 v0.30.3 6 | 7 | require ( 8 | github.com/ncruces/julianday v1.0.0 // indirect 9 | github.com/ncruces/sort v0.1.6 // indirect 10 | github.com/tetratelabs/wazero v1.11.0 // indirect 11 | golang.org/x/sys v0.39.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /vfs/os_std_alloc.go: -------------------------------------------------------------------------------- 1 | //go:build !(linux || darwin) || sqlite3_flock 2 | 3 | package vfs 4 | 5 | import ( 6 | "io" 7 | "os" 8 | ) 9 | 10 | func osAllocate(file *os.File, size int64) error { 11 | off, err := file.Seek(0, io.SeekEnd) 12 | if err != nil { 13 | return err 14 | } 15 | if size <= off { 16 | return nil 17 | } 18 | return file.Truncate(size) 19 | } 20 | -------------------------------------------------------------------------------- /sqlite3/include.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | // https://github.com/JuliaLang/julia/blob/v1.9.4/src/julia.h#L67-L68 7 | #define container_of(ptr, type, member) \ 8 | ((type *)((char *)(ptr) - offsetof(type, member))) 9 | 10 | typedef void *go_handle; 11 | void go_destroy(go_handle); 12 | static_assert(sizeof(go_handle) == 4, "Unexpected size"); -------------------------------------------------------------------------------- /ext/stats/kahan.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | // https://en.wikipedia.org/wiki/Kahan_summation_algorithm 4 | 5 | type kahan struct{ hi, lo float64 } 6 | 7 | func (k *kahan) add(x float64) { 8 | y := k.lo + x 9 | t := k.hi + y 10 | k.lo = y - (t - k.hi) 11 | k.hi = t 12 | } 13 | 14 | func (k *kahan) sub(x float64) { 15 | y := k.lo - x 16 | t := k.hi + y 17 | k.lo = y - (t - k.hi) 18 | k.hi = t 19 | } 20 | -------------------------------------------------------------------------------- /tests/vtab_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | ) 8 | 9 | func TestCreateModule_delete(t *testing.T) { 10 | db, err := sqlite3.Open(":memory:") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | defer db.Close() 15 | 16 | err = sqlite3.CreateModule[sqlite3.VTab](db, "generate_series", nil, nil) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /util/sql3util/README.md: -------------------------------------------------------------------------------- 1 | # SQLite utility functions 2 | 3 | This package implements assorted SQLite utilities 4 | useful to extension writers. 5 | 6 | It also wraps a [parser](https://github.com/marcobambini/sqlite-createtable-parser) 7 | for the [`CREATE`](https://sqlite.org/lang_createtable.html) and 8 | [`ALTER TABLE`](https://sqlite.org/lang_altertable.html) commands, 9 | created by [Marco Bambini](https://github.com/marcobambini). -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | tools 17 | 18 | # Go workspace file 19 | go.work 20 | go.work.sum 21 | 22 | # env file 23 | .env -------------------------------------------------------------------------------- /pointer.go: -------------------------------------------------------------------------------- 1 | package sqlite3 2 | 3 | import "github.com/ncruces/go-sqlite3/internal/util" 4 | 5 | // Pointer returns a pointer to a value that can be used as an argument to 6 | // [database/sql.DB.Exec] and similar methods. 7 | // Pointer should NOT be used with [Stmt.BindPointer], 8 | // [Value.Pointer], or [Context.ResultPointer]. 9 | // 10 | // https://sqlite.org/bindptr.html 11 | func Pointer(value any) any { 12 | return util.Pointer{Value: value} 13 | } 14 | -------------------------------------------------------------------------------- /gormlite/README.md: -------------------------------------------------------------------------------- 1 | # GORM SQLite Driver 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/image)](https://pkg.go.dev/github.com/ncruces/go-sqlite3/gormlite) 4 | 5 | ## Usage 6 | 7 | ```go 8 | import ( 9 | _ "github.com/ncruces/go-sqlite3/embed" 10 | "github.com/ncruces/go-sqlite3/gormlite" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | db, err := gorm.Open(gormlite.Open("gorm.db"), &gorm.Config{}) 15 | ``` 16 | 17 | Checkout [https://gorm.io](https://gorm.io) for details. -------------------------------------------------------------------------------- /gormlite/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ncruces/go-sqlite3/gormlite 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/ncruces/go-sqlite3 v0.30.3 7 | gorm.io/gorm v1.31.1 8 | ) 9 | 10 | require ( 11 | github.com/jinzhu/inflection v1.0.0 // indirect 12 | github.com/jinzhu/now v1.1.5 // indirect 13 | github.com/ncruces/julianday v1.0.0 // indirect 14 | github.com/tetratelabs/wazero v1.11.0 // indirect 15 | golang.org/x/sys v0.39.0 // indirect 16 | golang.org/x/text v0.32.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /.github/workflows/build-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | echo 'set -eux' > test.sh 5 | 6 | for p in $(go list ./...); do 7 | dir=".${p#github.com/ncruces/go-sqlite3}" 8 | name="$(basename "$p").test" 9 | (cd ${dir}; go test -c ${BUILDFLAGS:-}) 10 | [ -f "${dir}/${name}" ] && echo "(cd ${dir}; ./${name} ${TESTFLAGS:-})" >> test.sh 11 | done 12 | 13 | if [[ -v VMACTIONS ]]; then 14 | envsubst < .github/actions/vmactions/template.yml > .github/actions/vmactions/action.yml 15 | fi -------------------------------------------------------------------------------- /internal/util/reflect_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestReflectType(t *testing.T) { 11 | tests := []any{nil, 1, math.Pi, "abc"} 12 | for _, tt := range tests { 13 | t.Run(fmt.Sprint(tt), func(t *testing.T) { 14 | want := fmt.Sprintf("%T", tt) 15 | got := fmt.Sprintf("%v", ReflectType(reflect.ValueOf(tt))) 16 | if got != want { 17 | t.Errorf("ReflectType() = %v, want %v", got, want) 18 | } 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vfs/tests/mptest/wasm/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Use the default callback, not the Go one we patched in. 4 | #define sqliteBusyCallback sqliteDefaultBusyCallback 5 | 6 | // Amalgamation 7 | #include "sqlite3.c" 8 | // VFS 9 | #include "vfs.c" 10 | 11 | __attribute__((constructor)) void init() { sqlite3_initialize(); } 12 | 13 | // Ignore these. 14 | #define sqlite3_enable_load_extension(...) 15 | #define sqlite3_trace(...) 16 | #define unlink(...) (0) 17 | #undef UNUSED_PARAMETER 18 | 19 | #include "mptest.c" -------------------------------------------------------------------------------- /.github/workflows/libc.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark libc 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel] 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | - uses: actions/setup-go@v6 19 | with: { go-version: stable } 20 | 21 | - name: Benchmark 22 | shell: bash 23 | run: sqlite3/libc/benchmark.sh 24 | -------------------------------------------------------------------------------- /embed/init_test.go: -------------------------------------------------------------------------------- 1 | package embed 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3/driver" 7 | _ "github.com/ncruces/go-sqlite3/vfs/memdb" 8 | ) 9 | 10 | func Test_init(t *testing.T) { 11 | db, err := driver.Open("file:/test.db?vfs=memdb") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | defer db.Close() 16 | 17 | var version string 18 | err = db.QueryRow(`SELECT sqlite_version()`).Scan(&version) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | if version != "3.51.1" { 23 | t.Error(version) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/util/math.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "math" 4 | 5 | func abs(n int) int { 6 | if n < 0 { 7 | return -n 8 | } 9 | return n 10 | } 11 | 12 | func GCD(m, n int) int { 13 | for n != 0 { 14 | m, n = n, m%n 15 | } 16 | return abs(m) 17 | } 18 | 19 | func LCM(m, n int) int { 20 | if n == 0 { 21 | return 0 22 | } 23 | return abs(n) * (abs(m) / GCD(m, n)) 24 | } 25 | 26 | // https://developer.nvidia.com/blog/lerp-faster-cuda/ 27 | func Lerp(v0, v1, t float64) float64 { 28 | return math.FMA(t, v1, math.FMA(-t, v0, v0)) 29 | } 30 | -------------------------------------------------------------------------------- /util/ioutil/seek_test.go: -------------------------------------------------------------------------------- 1 | package ioutil 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestNewSeekingReaderAt(t *testing.T) { 9 | reader := NewSeekingReaderAt(strings.NewReader("abc")) 10 | defer reader.Close() 11 | 12 | n, err := reader.Size() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | if n != 3 { 17 | t.Errorf("got %d", n) 18 | } 19 | 20 | var buf [3]byte 21 | r, err := reader.ReadAt(buf[:], 0) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | if r != 3 { 26 | t.Errorf("got %d", r) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /vfs/memdb/README.md: -------------------------------------------------------------------------------- 1 | # Go `memdb` SQLite VFS 2 | 3 | This package implements the [`"memdb"`](https://sqlite.org/src/doc/tip/src/memdb.c) 4 | SQLite VFS in pure Go. 5 | 6 | It has some benefits over the C version: 7 | - the memory backing the database needs not be contiguous, 8 | - the database can grow/shrink incrementally without copying, 9 | - reader-writer concurrency is slightly improved. 10 | 11 | [`memdb.TestDB`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb#TestDB) 12 | is the preferred way to setup an in-memory database for testing. -------------------------------------------------------------------------------- /sqlite3/busy_timeout.patch: -------------------------------------------------------------------------------- 1 | # Replace sqliteDefaultBusyCallback, so Go can 2 | # handle, and interrupt, sqlite3_busy_timeout. 3 | --- sqlite3.c.orig 4 | +++ sqlite3.c 5 | @@ -186667,7 +186667,7 @@ 6 | if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT; 7 | #endif 8 | if( ms>0 ){ 9 | - sqlite3_busy_handler(db, (int(*)(void*,int))sqliteDefaultBusyCallback, 10 | + sqlite3_busy_handler(db, (int(*)(void*,int))sqliteBusyCallback, 11 | (void*)db); 12 | db->busyTimeout = ms; 13 | #ifdef SQLITE_ENABLE_SETLK_TIMEOUT 14 | -------------------------------------------------------------------------------- /embed/init.go: -------------------------------------------------------------------------------- 1 | // Package embed embeds SQLite into your application. 2 | // 3 | // Importing package embed initializes the [sqlite3.Binary] variable 4 | // with an appropriate build of SQLite: 5 | // 6 | // import _ "github.com/ncruces/go-sqlite3/embed" 7 | package embed 8 | 9 | import ( 10 | _ "embed" 11 | "unsafe" 12 | 13 | "github.com/ncruces/go-sqlite3" 14 | ) 15 | 16 | //go:embed sqlite3.wasm 17 | var binary string 18 | 19 | func init() { 20 | if sqlite3.Binary == nil { 21 | sqlite3.Binary = unsafe.Slice(unsafe.StringData(binary), len(binary)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gormlite/error_translator.go: -------------------------------------------------------------------------------- 1 | package gormlite 2 | 3 | import ( 4 | "errors" 5 | 6 | "gorm.io/gorm" 7 | 8 | "github.com/ncruces/go-sqlite3" 9 | ) 10 | 11 | // Translate it will translate the error to native gorm errors. 12 | func (_Dialector) Translate(err error) error { 13 | switch { 14 | case 15 | errors.Is(err, sqlite3.CONSTRAINT_UNIQUE), 16 | errors.Is(err, sqlite3.CONSTRAINT_PRIMARYKEY): 17 | return gorm.ErrDuplicatedKey 18 | case 19 | errors.Is(err, sqlite3.CONSTRAINT_FOREIGNKEY): 20 | return gorm.ErrForeignKeyViolated 21 | } 22 | return err 23 | } 24 | -------------------------------------------------------------------------------- /vfs/const_test.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func Test_ErrorCode_Error(t *testing.T) { 9 | tests := []struct { 10 | code _ErrorCode 11 | want string 12 | }{ 13 | {_OK, "sqlite3: not an error"}, 14 | {_ERROR, "sqlite3: SQL logic error"}, 15 | {math.MaxUint32, "sqlite3: unknown error"}, 16 | } 17 | for _, tt := range tests { 18 | t.Run(tt.want, func(t *testing.T) { 19 | if got := tt.code.Error(); got != tt.want { 20 | t.Errorf("_ErrorCode.Error() = %v, want %v", got, tt.want) 21 | } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/testdata/f2fs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | ROOT=../../ 6 | 7 | if mountpoint -q f2fs/; then 8 | sudo umount f2fs/ 9 | fi 10 | 11 | mkdir -p f2fs/ 12 | gunzip -c f2fs.img.gz > f2fs.img 13 | sudo mount -nv -o loop f2fs.img f2fs/ 14 | mkdir -p f2fs/tmp/ 15 | 16 | go test -c "$ROOT/tests" -coverpkg github.com/ncruces/go-sqlite3/... 17 | TMPDIR=$PWD/f2fs/tmp/ ./tests.test -test.v -test.short -test.coverprofile cover.out 18 | go tool cover -html cover.out 19 | 20 | sudo umount f2fs/ 21 | rm -r f2fs/ f2fs.img cover.out *.test -------------------------------------------------------------------------------- /sqlite3/pointer.c: -------------------------------------------------------------------------------- 1 | 2 | #include "include.h" 3 | #include "sqlite3.h" 4 | 5 | #define GO_POINTER_TYPE "github.com/ncruces/go-sqlite3.Pointer" 6 | 7 | int sqlite3_bind_pointer_go(sqlite3_stmt *stmt, int i, go_handle app) { 8 | return sqlite3_bind_pointer(stmt, i, app, GO_POINTER_TYPE, go_destroy); 9 | } 10 | 11 | void sqlite3_result_pointer_go(sqlite3_context *ctx, go_handle app) { 12 | sqlite3_result_pointer(ctx, app, GO_POINTER_TYPE, go_destroy); 13 | } 14 | 15 | go_handle sqlite3_value_pointer_go(sqlite3_value *val) { 16 | return sqlite3_value_pointer(val, GO_POINTER_TYPE); 17 | } -------------------------------------------------------------------------------- /litestream/README.md: -------------------------------------------------------------------------------- 1 | # Litestream lightweight read-replicas 2 | 3 | This package implements the **EXPERIMENTAL** `"litestream"` SQLite VFS 4 | that offers Litestream [lightweight read-replicas](https://fly.io/blog/litestream-revamped/#lightweight-read-replicas). 5 | 6 | See the [example](example_test.go) for how to use. 7 | 8 | Our `PRAGMA litestream_time` accepts: 9 | - Go [duration strings](https://pkg.go.dev/time#ParseDuration) 10 | - SQLite [time values](https://sqlite.org/lang_datefunc.html#time_values) 11 | - SQLite [time modifiers 1 through 13](https://sqlite.org/lang_datefunc.html#modifiers) 12 | -------------------------------------------------------------------------------- /internal/alloc/alloc_other.go: -------------------------------------------------------------------------------- 1 | //go:build !unix && !windows 2 | 3 | package alloc 4 | 5 | import "github.com/tetratelabs/wazero/experimental" 6 | 7 | func NewMemory(cap, max uint64) experimental.LinearMemory { 8 | return &sliceMemory{make([]byte, 0, cap)} 9 | } 10 | 11 | type sliceMemory struct { 12 | buf []byte 13 | } 14 | 15 | func (b *sliceMemory) Free() {} 16 | 17 | func (b *sliceMemory) Reallocate(size uint64) []byte { 18 | if cap := uint64(cap(b.buf)); size > cap { 19 | b.buf = append(b.buf[:cap], make([]byte, size-cap)...) 20 | } else { 21 | b.buf = b.buf[:size] 22 | } 23 | return b.buf 24 | } 25 | -------------------------------------------------------------------------------- /sqlite3/vfs_find.patch: -------------------------------------------------------------------------------- 1 | # Remove VFS registration. Go handles it. 2 | --- sqlite3.c.orig 3 | +++ sqlite3.c 4 | @@ -27176,7 +27176,7 @@ 5 | sqlite3_free(p); 6 | return sqlite3_os_init(); 7 | } 8 | - 9 | +#if 0 // Go handles VFS registration. 10 | /* 11 | ** The list of all registered VFS implementations. 12 | */ 13 | @@ -27273,7 +27273,7 @@ 14 | sqlite3_mutex_leave(mutex); 15 | return SQLITE_OK; 16 | } 17 | - 18 | +#endif 19 | /************** End of os.c **************************************************/ 20 | /************** Begin file fault.c *******************************************/ 21 | /* 22 | -------------------------------------------------------------------------------- /vfs/mvcc/README.md: -------------------------------------------------------------------------------- 1 | # Go `mvcc` SQLite VFS 2 | 3 | This package implements the **EXPERIMENTAL** `"mvcc"` in-memory SQLite VFS. 4 | 5 | It has some benefits over the [`"memdb"`](../memdb/README.md) VFS: 6 | - panics do not corrupt a shared database, 7 | - single-writer not blocked by readers, 8 | - readers never block, 9 | - instant snapshots. 10 | 11 | [`mvcc.TestDB`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/mvcc#TestDB) 12 | is the preferred way to setup an in-memory database for testing 13 | when you intend to leverage snapshots, 14 | e.g. to setup many independent copies of a database, 15 | such as one for each subtest. -------------------------------------------------------------------------------- /vfs/mvcc/mvcc_test.go: -------------------------------------------------------------------------------- 1 | package mvcc 2 | 3 | import ( 4 | _ "embed" 5 | "testing" 6 | 7 | "github.com/ncruces/go-sqlite3" 8 | _ "github.com/ncruces/go-sqlite3/embed" 9 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 10 | ) 11 | 12 | //go:embed testdata/wal.db 13 | var walDB string 14 | 15 | func Test_wal(t *testing.T) { 16 | t.Parallel() 17 | dsn := TestDB(t, NewSnapshot(walDB)) 18 | 19 | db, err := sqlite3.Open(dsn) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | defer db.Close() 24 | 25 | err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/testcfg/testcfg.go: -------------------------------------------------------------------------------- 1 | package testcfg 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/tetratelabs/wazero" 8 | 9 | "github.com/ncruces/go-sqlite3" 10 | ) 11 | 12 | // notest 13 | 14 | func init() { 15 | sqlite3.RuntimeConfig = wazero.NewRuntimeConfig().WithMemoryLimitPages(512) 16 | if os.Getenv("CI") != "" { 17 | path := filepath.Join(os.TempDir(), "wazero") 18 | if err := os.MkdirAll(path, 0777); err == nil { 19 | if cache, err := wazero.NewCompilationCacheWithDir(path); err == nil { 20 | sqlite3.RuntimeConfig = sqlite3.RuntimeConfig. 21 | WithCompilationCache(cache) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vfs/memdb/memdb_test.go: -------------------------------------------------------------------------------- 1 | package memdb 2 | 3 | import ( 4 | _ "embed" 5 | "testing" 6 | 7 | "github.com/ncruces/go-sqlite3" 8 | _ "github.com/ncruces/go-sqlite3/embed" 9 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 10 | ) 11 | 12 | //go:embed testdata/wal.db 13 | var walDB []byte 14 | 15 | func Test_wal(t *testing.T) { 16 | t.Parallel() 17 | 18 | Create("test.db", walDB) 19 | 20 | db, err := sqlite3.Open("file:/test.db?vfs=memdb") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | defer db.Close() 25 | 26 | err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /gormlite/tests.patch: -------------------------------------------------------------------------------- 1 | diff --git a/tests/.gitignore b/tests/.gitignore 2 | --- a/tests/.gitignore 3 | +++ b/tests/.gitignore 4 | @@ -1 +1 @@ 5 | -go.sum 6 | +* 7 | diff --git a/tests/tests_test.go b/tests/tests_test.go 8 | --- a/tests/tests_test.go 9 | +++ b/tests/tests_test.go 10 | @@ -8,9 +8,11 @@ import ( 11 | "path/filepath" 12 | "time" 13 | 14 | + _ "github.com/ncruces/go-sqlite3/embed" 15 | + sqlite "github.com/ncruces/go-sqlite3/gormlite" 16 | + 17 | "gorm.io/driver/gaussdb" 18 | "gorm.io/driver/mysql" 19 | "gorm.io/driver/postgres" 20 | - "gorm.io/driver/sqlite" 21 | "gorm.io/driver/sqlserver" 22 | "gorm.io/gorm" 23 | "gorm.io/gorm/logger" 24 | -------------------------------------------------------------------------------- /internal/dotlk/dotlk.go: -------------------------------------------------------------------------------- 1 | package dotlk 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "os" 7 | ) 8 | 9 | // LockShm creates a directory on disk to prevent SQLite 10 | // from using this path for a shared memory file. 11 | func LockShm(name string) error { 12 | err := os.Mkdir(name, 0777) 13 | if errors.Is(err, fs.ErrExist) { 14 | s, err := os.Lstat(name) 15 | if err == nil && s.IsDir() { 16 | return nil 17 | } 18 | } 19 | return err 20 | } 21 | 22 | // Unlock removes the lock or shared memory file. 23 | func Unlock(name string) error { 24 | err := os.Remove(name) 25 | if errors.Is(err, fs.ErrNotExist) { 26 | return nil 27 | } 28 | return err 29 | } 30 | -------------------------------------------------------------------------------- /tests/type_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | ) 8 | 9 | func TestDatatype_String(t *testing.T) { 10 | t.Parallel() 11 | 12 | tests := []struct { 13 | data sqlite3.Datatype 14 | want string 15 | }{ 16 | {sqlite3.INTEGER, "INTEGER"}, 17 | {sqlite3.FLOAT, "FLOAT"}, 18 | {sqlite3.TEXT, "TEXT"}, 19 | {sqlite3.BLOB, "BLOB"}, 20 | {sqlite3.NULL, "NULL"}, 21 | {10, "10"}, 22 | } 23 | for _, tt := range tests { 24 | t.Run(tt.want, func(t *testing.T) { 25 | if got := tt.data.String(); got != tt.want { 26 | t.Errorf("got %v, want %v", got, tt.want) 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ext/hash/sha3.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "crypto" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | "github.com/ncruces/go-sqlite3/internal/util" 8 | ) 9 | 10 | func sha3Func(ctx sqlite3.Context, arg ...sqlite3.Value) { 11 | size := 256 12 | if len(arg) > 1 { 13 | size = arg[1].Int() 14 | } 15 | 16 | switch size { 17 | case 224: 18 | hashFunc(ctx, arg[0], crypto.SHA3_224) 19 | case 256: 20 | hashFunc(ctx, arg[0], crypto.SHA3_256) 21 | case 384: 22 | hashFunc(ctx, arg[0], crypto.SHA3_384) 23 | case 512: 24 | hashFunc(ctx, arg[0], crypto.SHA3_512) 25 | default: 26 | ctx.ResultError(util.ErrorString("sha3: size must be 224, 256, 384, 512")) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gormlite/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | rm -rf gorm/ tests/ 7 | go work use -r . 8 | go test 9 | 10 | git clone --branch v1.31.1 --filter=blob:none https://github.com/go-gorm/gorm.git 11 | mv gorm/tests tests 12 | rm -rf gorm/ 13 | 14 | patch -p1 -N < tests.patch 15 | 16 | cd tests 17 | go mod edit \ 18 | -require github.com/ncruces/go-sqlite3/gormlite@v0.0.0 \ 19 | -replace github.com/ncruces/go-sqlite3/gormlite=../ \ 20 | -replace github.com/ncruces/go-sqlite3=../../ \ 21 | -droprequire gorm.io/driver/sqlite \ 22 | -dropreplace gorm.io/gorm 23 | go mod tidy && go work use . && go test 24 | 25 | cd .. 26 | rm -rf tests/ 27 | go work use -r . -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "github-actions" # See documentation for possible values 13 | directory: "/" # Location of package manifests 14 | schedule: 15 | interval: "daily" -------------------------------------------------------------------------------- /embed/bcw2/init.go: -------------------------------------------------------------------------------- 1 | // Package bcw2 embeds SQLite into your application. 2 | // 3 | // Importing package bcw2 initializes the [sqlite3.Binary] variable 4 | // with a build of SQLite that includes the [BEGIN CONCURRENT] and [Wal2] patches: 5 | // 6 | // import _ "github.com/ncruces/go-sqlite3/embed/bcw2" 7 | // 8 | // [BEGIN CONCURRENT]: https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md 9 | // [Wal2]: https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md 10 | package bcw2 11 | 12 | import ( 13 | _ "embed" 14 | "unsafe" 15 | 16 | "github.com/ncruces/go-sqlite3" 17 | ) 18 | 19 | //go:embed bcw2.wasm 20 | var binary string 21 | 22 | func init() { 23 | sqlite3.Binary = unsafe.Slice(unsafe.StringData(binary), len(binary)) 24 | } 25 | -------------------------------------------------------------------------------- /util/sql3util/parse_test.go: -------------------------------------------------------------------------------- 1 | package sql3util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3/util/sql3util" 7 | ) 8 | 9 | func TestParse(t *testing.T) { 10 | tab, err := sql3util.ParseTable(`CREATE TABLE child(x REFERENCES parent)`) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | if got := tab.Name; got != "child" { 16 | t.Errorf("got %s, want child", got) 17 | } 18 | if got := len(tab.Columns); got != 1 { 19 | t.Errorf("got %d, want 1", got) 20 | } 21 | 22 | col := tab.Columns[0] 23 | if got := col.Name; got != "x" { 24 | t.Errorf("got %s, want x", got) 25 | } 26 | 27 | fk := col.ForeignKeyClause 28 | if got := fk.Table; got != "parent" { 29 | t.Errorf("got %s, want parent", got) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ext/csv/types_test.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import "testing" 4 | 5 | func Test_getAffinity(t *testing.T) { 6 | tests := []struct { 7 | decl string 8 | want affinity 9 | }{ 10 | {"", blob}, 11 | {"INTEGER", integer}, 12 | {"TINYINT", integer}, 13 | {"TEXT", text}, 14 | {"CHAR", text}, 15 | {"CLOB", text}, 16 | {"BLOB", blob}, 17 | {"REAL", real}, 18 | {"FLOAT", real}, 19 | {"DOUBLE", real}, 20 | {"NUMERIC", numeric}, 21 | {"DECIMAL", numeric}, 22 | {"BOOLEAN", numeric}, 23 | {"DATETIME", numeric}, 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.decl, func(t *testing.T) { 27 | if got := getAffinity(tt.decl); got != tt.want { 28 | t.Errorf("getAffinity() = %v, want %v", got, tt.want) 29 | } 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ext/hash/blake2.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "crypto" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | "github.com/ncruces/go-sqlite3/internal/util" 8 | ) 9 | 10 | func blake2sFunc(ctx sqlite3.Context, arg ...sqlite3.Value) { 11 | hashFunc(ctx, arg[0], crypto.BLAKE2s_256) 12 | } 13 | 14 | func blake2bFunc(ctx sqlite3.Context, arg ...sqlite3.Value) { 15 | size := 512 16 | if len(arg) > 1 { 17 | size = arg[1].Int() 18 | } 19 | 20 | switch size { 21 | case 256: 22 | hashFunc(ctx, arg[0], crypto.BLAKE2b_256) 23 | case 384: 24 | hashFunc(ctx, arg[0], crypto.BLAKE2b_384) 25 | case 512: 26 | hashFunc(ctx, arg[0], crypto.BLAKE2b_512) 27 | default: 28 | ctx.ResultError(util.ErrorString("blake2b: size must be 256, 384, 512")) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ncruces/go-sqlite3 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/ncruces/julianday v1.0.0 7 | github.com/ncruces/sort v0.1.6 8 | github.com/ncruces/wbt v0.2.0 9 | github.com/tetratelabs/wazero v1.11.0 10 | golang.org/x/sys v0.39.0 11 | ) 12 | 13 | require ( 14 | github.com/dchest/siphash v1.2.3 // ext/bloom 15 | github.com/google/uuid v1.6.0 // ext/uuid 16 | github.com/psanford/httpreadat v0.1.0 // example 17 | golang.org/x/crypto v0.46.0 // vfs/adiantum vfs/xts 18 | golang.org/x/sync v0.19.0 // test 19 | golang.org/x/text v0.32.0 // ext/unicode 20 | lukechampine.com/adiantum v1.1.1 // vfs/adiantum 21 | ) 22 | 23 | retract ( 24 | v0.23.2 // tagged from the wrong branch 25 | v0.4.0 // tagged from the wrong branch 26 | ) 27 | -------------------------------------------------------------------------------- /registry.go: -------------------------------------------------------------------------------- 1 | package sqlite3 2 | 3 | import "sync" 4 | 5 | var ( 6 | // +checklocks:extRegistryMtx 7 | extRegistry []func(*Conn) error 8 | extRegistryMtx sync.RWMutex 9 | ) 10 | 11 | // AutoExtension causes the entryPoint function to be invoked 12 | // for each new database connection that is created. 13 | // 14 | // https://sqlite.org/c3ref/auto_extension.html 15 | func AutoExtension(entryPoint func(*Conn) error) { 16 | extRegistryMtx.Lock() 17 | extRegistry = append(extRegistry, entryPoint) 18 | extRegistryMtx.Unlock() 19 | } 20 | 21 | func initExtensions(c *Conn) error { 22 | extRegistryMtx.RLock() 23 | defer extRegistryMtx.RUnlock() 24 | for _, f := range extRegistry { 25 | if err := f(c); err != nil { 26 | return err 27 | } 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/repro.yml: -------------------------------------------------------------------------------- 1 | name: Reproducible build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: read 8 | id-token: write 9 | attestations: write 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | os: [macos-latest, ubuntu-latest, windows-latest] 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - uses: ilammy/msvc-dev-cmd@v1 20 | - uses: actions/checkout@v6 21 | 22 | - name: Build 23 | shell: bash 24 | run: .github/workflows/repro.sh 25 | 26 | - uses: actions/attest-build-provenance@v3 27 | if: matrix.os == 'ubuntu-latest' 28 | with: 29 | subject-path: | 30 | embed/sqlite3.wasm 31 | embed/bcw2/bcw2.wasm 32 | util/sql3util/wasm/sql3parse_table.wasm -------------------------------------------------------------------------------- /vfs/lock_other.go: -------------------------------------------------------------------------------- 1 | //go:build !(linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk) 2 | 3 | package vfs 4 | 5 | // SupportsFileLocking is false on platforms that do not support file locking. 6 | // To open a database file on those platforms, 7 | // you need to use the [nolock] or [immutable] URI parameters. 8 | // 9 | // [nolock]: https://sqlite.org/uri.html#urinolock 10 | // [immutable]: https://sqlite.org/uri.html#uriimmutable 11 | const SupportsFileLocking = false 12 | 13 | func (f *vfsFile) Lock(LockLevel) error { 14 | return _IOERR_LOCK 15 | } 16 | 17 | func (f *vfsFile) Unlock(LockLevel) error { 18 | return _IOERR_UNLOCK 19 | } 20 | 21 | func (f *vfsFile) CheckReservedLock() (bool, error) { 22 | return false, _IOERR_CHECKRESERVEDLOCK 23 | } 24 | -------------------------------------------------------------------------------- /gormlite/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod.go" 7 | curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod_test.go" 8 | curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod_parse_all_columns.go" 9 | curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod_parse_all_columns_test.go" 10 | curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/error_translator.go" 11 | curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/migrator.go" 12 | curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/sqlite.go" 13 | curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/sqlite_test.go" 14 | curl -#L "https://github.com/glebarez/sqlite/raw/v1.11.0/sqlite_error_translator_test.go" > error_translator_test.go -------------------------------------------------------------------------------- /vfs/adiantum/adiantum.go: -------------------------------------------------------------------------------- 1 | package adiantum 2 | 3 | import ( 4 | "crypto/rand" 5 | 6 | "golang.org/x/crypto/argon2" 7 | 8 | "lukechampine.com/adiantum" 9 | "lukechampine.com/adiantum/hbsh" 10 | ) 11 | 12 | // This variable can be replaced with -ldflags: 13 | // 14 | // go build -ldflags="-X github.com/ncruces/go-sqlite3/vfs/adiantum.pepper=adiantum" 15 | var pepper = "github.com/ncruces/go-sqlite3/vfs/adiantum" 16 | 17 | type adiantumCreator struct{} 18 | 19 | func (adiantumCreator) HBSH(key []byte) *hbsh.HBSH { 20 | if len(key) != 32 { 21 | return nil 22 | } 23 | return adiantum.New(key) 24 | } 25 | 26 | func (adiantumCreator) KDF(text string) []byte { 27 | if text == "" { 28 | key := make([]byte, 32) 29 | rand.Read(key) 30 | return key 31 | } 32 | return argon2.IDKey([]byte(text), []byte(pepper), 3, 64*1024, 4, 32) 33 | } 34 | -------------------------------------------------------------------------------- /vfs/xts/aes.go: -------------------------------------------------------------------------------- 1 | package xts 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/pbkdf2" 6 | "crypto/rand" 7 | "crypto/sha512" 8 | 9 | "golang.org/x/crypto/xts" 10 | ) 11 | 12 | // This variable can be replaced with -ldflags: 13 | // 14 | // go build -ldflags="-X github.com/ncruces/go-sqlite3/vfs/xts.pepper=xts" 15 | var pepper = "github.com/ncruces/go-sqlite3/vfs/xts" 16 | 17 | type aesCreator struct{} 18 | 19 | func (aesCreator) XTS(key []byte) *xts.Cipher { 20 | c, err := xts.NewCipher(aes.NewCipher, key) 21 | if err != nil { 22 | return nil 23 | } 24 | return c 25 | } 26 | 27 | func (aesCreator) KDF(text string) []byte { 28 | if text == "" { 29 | key := make([]byte, 32) 30 | rand.Read(key) 31 | return key 32 | } 33 | key, err := pbkdf2.Key(sha512.New, text, []byte(pepper), 10_000, 32) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return key 38 | } 39 | -------------------------------------------------------------------------------- /driver/savepoint.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | 7 | "github.com/ncruces/go-sqlite3" 8 | ) 9 | 10 | // Savepoint establishes a new transaction savepoint. 11 | // 12 | // https://sqlite.org/lang_savepoint.html 13 | func Savepoint(tx *sql.Tx) sqlite3.Savepoint { 14 | var ctx saveptCtx 15 | tx.ExecContext(&ctx, "") 16 | return ctx.Savepoint 17 | } 18 | 19 | // A saveptCtx is never canceled, has no values, and has no deadline. 20 | type saveptCtx struct{ sqlite3.Savepoint } 21 | 22 | func (*saveptCtx) Deadline() (deadline time.Time, ok bool) { 23 | // notest 24 | return 25 | } 26 | 27 | func (*saveptCtx) Done() <-chan struct{} { 28 | // notest 29 | return nil 30 | } 31 | 32 | func (*saveptCtx) Err() error { 33 | // notest 34 | return nil 35 | } 36 | 37 | func (*saveptCtx) Value(key any) any { 38 | // notest 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /ext/csv/arg.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/ncruces/go-sqlite3/util/sql3util" 8 | ) 9 | 10 | func uintArg(key, val string) (int, error) { 11 | i, err := strconv.ParseUint(val, 10, 15) 12 | if err != nil { 13 | return 0, fmt.Errorf("csv: invalid %q parameter: %s", key, val) 14 | } 15 | return int(i), nil 16 | } 17 | 18 | func boolArg(key, val string) (bool, error) { 19 | if val == "" { 20 | return true, nil 21 | } 22 | b, ok := sql3util.ParseBool(val) 23 | if ok { 24 | return b, nil 25 | } 26 | return false, fmt.Errorf("csv: invalid %q parameter: %s", key, val) 27 | } 28 | 29 | func runeArg(key, val string) (rune, error) { 30 | r, _, tail, err := strconv.UnquoteChar(sql3util.Unquote(val), 0) 31 | if tail != "" || err != nil { 32 | return 0, fmt.Errorf("csv: invalid %q parameter: %s", key, val) 33 | } 34 | return r, nil 35 | } 36 | -------------------------------------------------------------------------------- /sqlite3/text.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "sqlite3.h" 4 | 5 | int sqlite3_bind_text_go(sqlite3_stmt *stmt, int i, const char *zData, 6 | sqlite3_uint64 nData) { 7 | return sqlite3_bind_text64(stmt, i, zData, nData, &sqlite3_free, SQLITE_UTF8); 8 | } 9 | 10 | int sqlite3_bind_blob_go(sqlite3_stmt *stmt, int i, const char *zData, 11 | sqlite3_uint64 nData) { 12 | return sqlite3_bind_blob64(stmt, i, zData, nData, &sqlite3_free); 13 | } 14 | 15 | void sqlite3_result_text_go(sqlite3_context *ctx, const char *zData, 16 | sqlite3_uint64 nData) { 17 | sqlite3_result_text64(ctx, zData, nData, &sqlite3_free, SQLITE_UTF8); 18 | } 19 | 20 | void sqlite3_result_blob_go(sqlite3_context *ctx, const void *zData, 21 | sqlite3_uint64 nData) { 22 | sqlite3_result_blob64(ctx, zData, nData, &sqlite3_free); 23 | } -------------------------------------------------------------------------------- /vfs/os_std.go: -------------------------------------------------------------------------------- 1 | //go:build !unix 2 | 3 | package vfs 4 | 5 | import ( 6 | "io/fs" 7 | "os" 8 | ) 9 | 10 | const ( 11 | isUnix = false 12 | _O_NOFOLLOW = 0 13 | ) 14 | 15 | func osAccess(path string, flags AccessFlag) error { 16 | fi, err := os.Stat(path) 17 | if err != nil { 18 | return err 19 | } 20 | if flags == ACCESS_EXISTS { 21 | return nil 22 | } 23 | 24 | const ( 25 | S_IREAD = 0400 26 | S_IWRITE = 0200 27 | S_IEXEC = 0100 28 | ) 29 | 30 | var want fs.FileMode = S_IREAD 31 | if flags == ACCESS_READWRITE { 32 | want |= S_IWRITE 33 | } 34 | if fi.IsDir() { 35 | want |= S_IEXEC 36 | } 37 | if fi.Mode()&want != want { 38 | return fs.ErrPermission 39 | } 40 | return nil 41 | } 42 | 43 | func osSetMode(file *os.File, modeof string) error { 44 | fi, err := os.Stat(modeof) 45 | if err != nil { 46 | return err 47 | } 48 | file.Chmod(fi.Mode()) 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /ext/csv/schema.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/ncruces/go-sqlite3" 8 | ) 9 | 10 | func getSchema(header bool, columns int, row []string) string { 11 | var sep string 12 | var str strings.Builder 13 | str.WriteString("CREATE TABLE x(") 14 | 15 | if 0 <= columns && columns < len(row) { 16 | row = row[:columns] 17 | } 18 | for i, f := range row { 19 | str.WriteString(sep) 20 | if header && f != "" { 21 | str.WriteString(sqlite3.QuoteIdentifier(f)) 22 | } else { 23 | str.WriteString("c") 24 | str.WriteString(strconv.Itoa(i + 1)) 25 | } 26 | str.WriteString(" TEXT") 27 | sep = "," 28 | } 29 | for i := len(row); i < columns; i++ { 30 | str.WriteString(sep) 31 | str.WriteString("c") 32 | str.WriteString(strconv.Itoa(i + 1)) 33 | str.WriteString(" TEXT") 34 | sep = "," 35 | } 36 | str.WriteByte(')') 37 | 38 | return str.String() 39 | } 40 | -------------------------------------------------------------------------------- /ext/csv/schema_test.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import "testing" 4 | 5 | func Test_getSchema(t *testing.T) { 6 | t.Parallel() 7 | 8 | tests := []struct { 9 | header bool 10 | columns int 11 | row []string 12 | want string 13 | }{ 14 | {true, 2, nil, `CREATE TABLE x(c1 TEXT,c2 TEXT)`}, 15 | {false, 2, nil, `CREATE TABLE x(c1 TEXT,c2 TEXT)`}, 16 | {false, -1, []string{"abc", ""}, `CREATE TABLE x(c1 TEXT,c2 TEXT)`}, 17 | {true, 3, []string{"abc", ""}, `CREATE TABLE x("abc" TEXT,c2 TEXT,c3 TEXT)`}, 18 | {true, -1, []string{"abc", "def"}, `CREATE TABLE x("abc" TEXT,"def" TEXT)`}, 19 | {true, 1, []string{"abc", "def"}, `CREATE TABLE x("abc" TEXT)`}, 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.want, func(t *testing.T) { 23 | if got := getSchema(tt.header, tt.columns, tt.row); got != tt.want { 24 | t.Errorf("getSchema() = %v, want %v", got, tt.want) 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ext/stats/boolean.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import "github.com/ncruces/go-sqlite3" 4 | 5 | const ( 6 | every = iota 7 | some 8 | ) 9 | 10 | func newBoolean(kind int) sqlite3.AggregateConstructor { 11 | return func() sqlite3.AggregateFunction { return &boolean{kind: kind} } 12 | } 13 | 14 | type boolean struct { 15 | count int 16 | total int 17 | kind int 18 | } 19 | 20 | func (b *boolean) Value(ctx sqlite3.Context) { 21 | if b.kind == every { 22 | ctx.ResultBool(b.count == b.total) 23 | } else { 24 | ctx.ResultBool(b.count > 0) 25 | } 26 | } 27 | 28 | func (b *boolean) Step(ctx sqlite3.Context, arg ...sqlite3.Value) { 29 | a := arg[0] 30 | if a.Bool() { 31 | b.count++ 32 | } 33 | if a.Type() != sqlite3.NULL { 34 | b.total++ 35 | } 36 | } 37 | 38 | func (b *boolean) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) { 39 | a := arg[0] 40 | if a.Bool() { 41 | b.count-- 42 | } 43 | if a.Type() != sqlite3.NULL { 44 | b.total-- 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/vfs_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/ncruces/go-sqlite3" 8 | _ "github.com/ncruces/go-sqlite3/embed" 9 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 10 | "github.com/ncruces/go-sqlite3/vfs/memdb" 11 | "github.com/ncruces/go-sqlite3/vfs/readervfs" 12 | ) 13 | 14 | func TestMemoryVFS_Open_notfound(t *testing.T) { 15 | memdb.Delete("demo.db") 16 | 17 | _, err := sqlite3.Open("file:/demo.db?vfs=memdb&mode=ro") 18 | if err == nil { 19 | t.Error("want error") 20 | } 21 | if !errors.Is(err, sqlite3.CANTOPEN) { 22 | t.Errorf("got %v, want sqlite3.CANTOPEN", err) 23 | } 24 | } 25 | 26 | func TestReaderVFS_Open_notfound(t *testing.T) { 27 | readervfs.Delete("demo.db") 28 | 29 | _, err := sqlite3.Open("file:demo.db?vfs=reader") 30 | if err == nil { 31 | t.Error("want error") 32 | } 33 | if !errors.Is(err, sqlite3.CANTOPEN) { 34 | t.Errorf("got %v, want sqlite3.CANTOPEN", err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sqlite3/sqlite_cfg.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Platform Configuration 4 | 5 | #define SQLITE_OS_OTHER 1 6 | #define SQLITE_BYTEORDER 1234 7 | 8 | #define HAVE_INT8_T 1 9 | #define HAVE_INT16_T 1 10 | #define HAVE_INT32_T 1 11 | #define HAVE_INT64_T 1 12 | #define HAVE_INTPTR_T 1 13 | #define HAVE_UINT8_T 1 14 | #define HAVE_UINT16_T 1 15 | #define HAVE_UINT32_T 1 16 | #define HAVE_UINT64_T 1 17 | #define HAVE_UINTPTR_T 1 18 | #define HAVE_STDINT_H 1 19 | #define HAVE_INTTYPES_H 1 20 | 21 | #define HAVE_LOG2 1 22 | #define HAVE_LOG10 1 23 | #define HAVE_ISNAN 1 24 | 25 | #define HAVE_STRCHRNUL 1 26 | 27 | #define HAVE_USLEEP 1 28 | #define HAVE_NANOSLEEP 1 29 | 30 | #define HAVE_GMTIME_R 1 31 | #define HAVE_LOCALTIME_S 1 32 | 33 | #define HAVE_MALLOC_H 1 34 | #define HAVE_MALLOC_USABLE_SIZE 1 35 | 36 | // Implemented in hooks.c. 37 | static int sqliteBusyCallback(void *, int); 38 | 39 | // Implemented in vfs.c. 40 | int localtime_s(struct tm *const pTm, time_t const *const pTime); -------------------------------------------------------------------------------- /driver/time.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "bytes" 5 | "time" 6 | ) 7 | 8 | // Convert a string in [time.RFC3339Nano] format into a [time.Time] 9 | // if it roundtrips back to the same string. 10 | // This way times can be persisted to, and recovered from, the database, 11 | // but if a string is needed, [database/sql] will recover the same string. 12 | func maybeTime(text []byte) (_ time.Time, _ bool) { 13 | // Weed out (some) values that can't possibly be 14 | // [time.RFC3339Nano] timestamps. 15 | if len(text) < len("2006-01-02T15:04:05Z") { 16 | return 17 | } 18 | if len(text) > len(time.RFC3339Nano) { 19 | return 20 | } 21 | if text[4] != '-' || text[10] != 'T' || text[16] != ':' { 22 | return 23 | } 24 | 25 | // Slow path. 26 | var buf [len(time.RFC3339Nano)]byte 27 | date, err := time.Parse(time.RFC3339Nano, string(text)) 28 | if err == nil && bytes.Equal(text, date.AppendFormat(buf[:0], time.RFC3339Nano)) { 29 | return date, true 30 | } 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /sqlite3/libc/math.h: -------------------------------------------------------------------------------- 1 | #include_next // the system math.h 2 | 3 | #ifndef _WASM_SIMD128_MATH_H 4 | #define _WASM_SIMD128_MATH_H 5 | 6 | #include 7 | 8 | #ifdef __cplusplus 9 | extern "C" { 10 | #endif 11 | 12 | #ifdef __wasm_relaxed_simd__ 13 | 14 | // This header assumes "relaxed fused multiply-add" 15 | // is both faster and more precise. 16 | 17 | #define FP_FAST_FMA 1 18 | 19 | __attribute__((weak)) 20 | double fma(double x, double y, double z) { 21 | // If we get a software implementation from the host, 22 | // this is enough to short circuit it on the 2nd lane. 23 | const v128_t wx = wasm_f64x2_replace_lane(b, 0, x); 24 | const v128_t wy = wasm_f64x2_splat(y); 25 | const v128_t wz = wasm_f64x2_splat(z); 26 | const v128_t wr = wasm_f64x2_relaxed_madd(wx, wy, wz); 27 | return wasm_f64x2_extract_lane(wr, 0); 28 | } 29 | 30 | #endif // __wasm_relaxed_simd__ 31 | 32 | #ifdef __cplusplus 33 | } // extern "C" 34 | #endif 35 | 36 | #endif // _WASM_SIMD128_MATH_H -------------------------------------------------------------------------------- /ext/pivot/op_test.go: -------------------------------------------------------------------------------- 1 | package pivot 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | ) 8 | 9 | func Test_operator(t *testing.T) { 10 | tests := []struct { 11 | op sqlite3.IndexConstraintOp 12 | want string 13 | }{ 14 | {sqlite3.INDEX_CONSTRAINT_EQ, "="}, 15 | {sqlite3.INDEX_CONSTRAINT_LT, "<"}, 16 | {sqlite3.INDEX_CONSTRAINT_GT, ">"}, 17 | {sqlite3.INDEX_CONSTRAINT_LE, "<="}, 18 | {sqlite3.INDEX_CONSTRAINT_GE, ">="}, 19 | {sqlite3.INDEX_CONSTRAINT_NE, "<>"}, 20 | {sqlite3.INDEX_CONSTRAINT_IS, "IS"}, 21 | {sqlite3.INDEX_CONSTRAINT_ISNOT, "IS NOT"}, 22 | {sqlite3.INDEX_CONSTRAINT_REGEXP, "REGEXP"}, 23 | {sqlite3.INDEX_CONSTRAINT_MATCH, "MATCH"}, 24 | {sqlite3.INDEX_CONSTRAINT_GLOB, "GLOB"}, 25 | {sqlite3.INDEX_CONSTRAINT_LIKE, "LIKE"}, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.want, func(t *testing.T) { 29 | if got := operator(tt.op); got != tt.want { 30 | t.Errorf("operator() = %v, want %v", got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /embed/bcw2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/ncruces/go-sqlite3 v0.30.3 h1:X/CgWW9GzmIAkEPrifhKqf0cC15DuOVxAJaHFTTAURQ= 2 | github.com/ncruces/go-sqlite3 v0.30.3/go.mod h1:AxKu9sRxkludimFocbktlY6LiYSkxiI5gTA8r+os/Nw= 3 | github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= 4 | github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= 5 | github.com/ncruces/sort v0.1.6 h1:TrsJfGRH1AoWoaeB4/+gCohot9+cA6u/INaH5agIhNk= 6 | github.com/ncruces/sort v0.1.6/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk= 7 | github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= 8 | github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= 9 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 10 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 11 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 12 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 13 | -------------------------------------------------------------------------------- /util/osutil/osfs.go: -------------------------------------------------------------------------------- 1 | // Package osutil implements operating system utilities. 2 | package osutil 3 | 4 | import ( 5 | "io/fs" 6 | "os" 7 | ) 8 | 9 | // FS implements [fs.FS], [fs.StatFS], and [fs.ReadFileFS] 10 | // using package [os]. 11 | // 12 | // This filesystem does not respect [fs.ValidPath] rules, 13 | // and fails [testing/fstest.TestFS]! 14 | // 15 | // Still, it can be a useful tool to unify implementations 16 | // that can access either the [os] filesystem or an [fs.FS]. 17 | // It's OK to use this to open files, but you should avoid 18 | // opening directories, resolving paths, or walking the file system. 19 | type FS struct{} 20 | 21 | // Open implements [fs.FS]. 22 | func (FS) Open(name string) (fs.File, error) { 23 | return os.OpenFile(name, os.O_RDONLY, 0) 24 | } 25 | 26 | // Stat implements [fs.StatFS]. 27 | func (FS) Stat(name string) (fs.FileInfo, error) { 28 | return os.Stat(name) 29 | } 30 | 31 | // ReadFile implements [fs.ReadFileFS]. 32 | func (FS) ReadFile(name string) ([]byte, error) { 33 | return os.ReadFile(name) 34 | } 35 | -------------------------------------------------------------------------------- /vfs/os_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build unix && !sqlite3_flock 2 | 3 | package vfs 4 | 5 | import ( 6 | "crypto/rand" 7 | "io" 8 | "os" 9 | "runtime" 10 | "testing" 11 | 12 | "golang.org/x/sys/unix" 13 | ) 14 | 15 | func Test_osAllocate(t *testing.T) { 16 | if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { 17 | t.Skip() 18 | } 19 | 20 | f, err := os.CreateTemp(t.TempDir(), "file") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | _, err = io.CopyN(f, rand.Reader, 1024*1024) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | n, err := f.Seek(0, io.SeekEnd) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | if n != 1024*1024 { 35 | t.Fatalf("got %d, want %d", n, 1024*1024) 36 | } 37 | 38 | err = osAllocate(f, 16*1024*1024) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | var stat unix.Stat_t 44 | err = unix.Stat(f.Name(), &stat) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | if stat.Blocks*512 != 16*1024*1024 { 50 | t.Fatalf("got %d, want %d", stat.Blocks*512, 16*1024*1024) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /vfs/os_f2fs_linux.go: -------------------------------------------------------------------------------- 1 | //go:build amd64 || arm64 || riscv64 2 | 3 | package vfs 4 | 5 | import ( 6 | "os" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | const ( 12 | // https://godbolt.org/z/1PcK5vea3 13 | _F2FS_IOC_START_ATOMIC_WRITE = 62721 14 | _F2FS_IOC_COMMIT_ATOMIC_WRITE = 62722 15 | _F2FS_IOC_ABORT_ATOMIC_WRITE = 62725 16 | _F2FS_IOC_GET_FEATURES = 2147808524 // -2147158772 17 | _F2FS_FEATURE_ATOMIC_WRITE = 4 18 | ) 19 | 20 | // notest 21 | 22 | func osBatchAtomic(file *os.File) bool { 23 | flags, err := unix.IoctlGetInt(int(file.Fd()), _F2FS_IOC_GET_FEATURES) 24 | return err == nil && flags&_F2FS_FEATURE_ATOMIC_WRITE != 0 25 | } 26 | 27 | func (f *vfsFile) BeginAtomicWrite() error { 28 | return unix.IoctlSetInt(int(f.Fd()), _F2FS_IOC_START_ATOMIC_WRITE, 0) 29 | } 30 | 31 | func (f *vfsFile) CommitAtomicWrite() error { 32 | return unix.IoctlSetInt(int(f.Fd()), _F2FS_IOC_COMMIT_ATOMIC_WRITE, 0) 33 | } 34 | 35 | func (f *vfsFile) RollbackAtomicWrite() error { 36 | return unix.IoctlSetInt(int(f.Fd()), _F2FS_IOC_ABORT_ATOMIC_WRITE, 0) 37 | } 38 | -------------------------------------------------------------------------------- /vfs/memdb/example_test.go: -------------------------------------------------------------------------------- 1 | package memdb_test 2 | 3 | import ( 4 | "database/sql" 5 | _ "embed" 6 | "fmt" 7 | "log" 8 | 9 | _ "github.com/ncruces/go-sqlite3/driver" 10 | _ "github.com/ncruces/go-sqlite3/embed" 11 | "github.com/ncruces/go-sqlite3/vfs/memdb" 12 | ) 13 | 14 | //go:embed testdata/test.db 15 | var testDB []byte 16 | 17 | func Example() { 18 | memdb.Create("test.db", testDB) 19 | 20 | db, err := sql.Open("sqlite3", "file:/test.db?vfs=memdb") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | defer db.Close() 25 | 26 | _, err = db.Exec(`INSERT INTO users (id, name) VALUES (3, 'rust')`) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | rows, err := db.Query(`SELECT id, name FROM users`) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | defer rows.Close() 36 | 37 | for rows.Next() { 38 | var id, name string 39 | err = rows.Scan(&id, &name) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | fmt.Printf("%s %s\n", id, name) 44 | } 45 | // Output: 46 | // 0 go 47 | // 1 zig 48 | // 2 whatever 49 | // 3 rust 50 | } 51 | -------------------------------------------------------------------------------- /vfs/mvcc/example_test.go: -------------------------------------------------------------------------------- 1 | package mvcc_test 2 | 3 | import ( 4 | "database/sql" 5 | _ "embed" 6 | "fmt" 7 | "log" 8 | 9 | _ "github.com/ncruces/go-sqlite3/driver" 10 | _ "github.com/ncruces/go-sqlite3/embed" 11 | "github.com/ncruces/go-sqlite3/vfs/mvcc" 12 | ) 13 | 14 | //go:embed testdata/test.db 15 | var testDB string 16 | 17 | func Example() { 18 | mvcc.Create("test.db", mvcc.NewSnapshot(testDB)) 19 | 20 | db, err := sql.Open("sqlite3", "file:/test.db?vfs=mvcc") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | defer db.Close() 25 | 26 | _, err = db.Exec(`INSERT INTO users (id, name) VALUES (3, 'rust')`) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | rows, err := db.Query(`SELECT id, name FROM users`) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | defer rows.Close() 36 | 37 | for rows.Next() { 38 | var id, name string 39 | err = rows.Scan(&id, &name) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | fmt.Printf("%s %s\n", id, name) 44 | } 45 | // Output: 46 | // 0 go 47 | // 1 zig 48 | // 2 whatever 49 | // 3 rust 50 | } 51 | -------------------------------------------------------------------------------- /vfs/shm_other.go: -------------------------------------------------------------------------------- 1 | //go:build !(((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le || loong64)) || sqlite3_flock || sqlite3_dotlk) 2 | 3 | package vfs 4 | 5 | // SupportsSharedMemory is false on platforms that do not support shared memory. 6 | // To use [WAL without shared-memory], you need to set [EXCLUSIVE locking mode]. 7 | // 8 | // [WAL without shared-memory]: https://sqlite.org/wal.html#noshm 9 | // [EXCLUSIVE locking mode]: https://sqlite.org/pragma.html#pragma_locking_mode 10 | const SupportsSharedMemory = false 11 | 12 | // NewSharedMemory returns a shared-memory WAL-index 13 | // backed by a file with the given path. 14 | // It will return nil if shared-memory is not supported, 15 | // or not appropriate for the given flags. 16 | // Only [OPEN_MAIN_DB] databases may need a WAL-index. 17 | // You must ensure all concurrent accesses to a database 18 | // use shared-memory instances created with the same path. 19 | func NewSharedMemory(path string, flags OpenFlag) SharedMemory { 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/util/module.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tetratelabs/wazero/experimental" 7 | 8 | "github.com/ncruces/go-sqlite3/internal/alloc" 9 | ) 10 | 11 | type ConnKey struct{} 12 | 13 | type moduleKey struct{} 14 | type moduleState struct { 15 | sysError error 16 | mmapState 17 | handleState 18 | } 19 | 20 | func NewContext(ctx context.Context) context.Context { 21 | state := new(moduleState) 22 | ctx = experimental.WithMemoryAllocator(ctx, experimental.MemoryAllocatorFunc(alloc.NewMemory)) 23 | ctx = experimental.WithCloseNotifier(ctx, state) 24 | ctx = context.WithValue(ctx, moduleKey{}, state) 25 | return ctx 26 | } 27 | 28 | func GetSystemError(ctx context.Context) error { 29 | // Test needed to simplify testing. 30 | s, ok := ctx.Value(moduleKey{}).(*moduleState) 31 | if ok { 32 | return s.sysError 33 | } 34 | return nil 35 | } 36 | 37 | func SetSystemError(ctx context.Context, err error) { 38 | // Test needed to simplify testing. 39 | s, ok := ctx.Value(moduleKey{}).(*moduleState) 40 | if ok { 41 | s.sysError = err 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sqlite3/main.c: -------------------------------------------------------------------------------- 1 | // Amalgamation 2 | #include "sqlite3.c" 3 | // Extensions 4 | #include "ext/anycollseq.c" 5 | #include "ext/base64.c" 6 | #include "ext/decimal.c" 7 | #include "ext/ieee754.c" 8 | #include "ext/regexp.c" 9 | #include "ext/series.c" 10 | #include "ext/spellfix.c" 11 | #include "ext/uint.c" 12 | // Bindings 13 | #include "func.c" 14 | #include "hooks.c" 15 | #include "pointer.c" 16 | #include "stmt.c" 17 | #include "text.c" 18 | #include "time.c" 19 | #include "vfs.c" 20 | #include "vtab.c" 21 | 22 | __attribute__((constructor)) void init() { 23 | sqlite3_initialize(); 24 | sqlite3_auto_extension((void (*)(void))sqlite3_base_init); 25 | sqlite3_auto_extension((void (*)(void))sqlite3_decimal_init); 26 | sqlite3_auto_extension((void (*)(void))sqlite3_ieee_init); 27 | sqlite3_auto_extension((void (*)(void))sqlite3_regexp_init); 28 | sqlite3_auto_extension((void (*)(void))sqlite3_series_init); 29 | sqlite3_auto_extension((void (*)(void))sqlite3_spellfix_init); 30 | sqlite3_auto_extension((void (*)(void))sqlite3_uint_init); 31 | sqlite3_auto_extension((void (*)(void))sqlite3_time_init); 32 | } -------------------------------------------------------------------------------- /litestream/example_test.go: -------------------------------------------------------------------------------- 1 | package litestream_test 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/benbjohnson/litestream/s3" 8 | 9 | "github.com/ncruces/go-sqlite3/driver" 10 | _ "github.com/ncruces/go-sqlite3/embed" 11 | "github.com/ncruces/go-sqlite3/litestream" 12 | ) 13 | 14 | func ExampleNewReplica() { 15 | client := s3.NewReplicaClient() 16 | client.Bucket = "test-bucket" 17 | client.Path = "fruits.db" 18 | 19 | litestream.NewReplica("fruits.db", client, litestream.ReplicaOptions{ 20 | PollInterval: 5 * time.Second, 21 | }) 22 | 23 | db, err := driver.Open("file:fruits.db?vfs=litestream") 24 | if err != nil { 25 | log.Fatalln(err) 26 | } 27 | defer db.Close() 28 | 29 | for { 30 | time.Sleep(time.Second) 31 | rows, err := db.Query("SELECT * FROM fruits") 32 | if err != nil { 33 | log.Fatalln(err) 34 | } 35 | 36 | for rows.Next() { 37 | var name, color string 38 | err := rows.Scan(&name, &color) 39 | if err != nil { 40 | log.Fatalln(err) 41 | } 42 | log.Println(name, color) 43 | } 44 | 45 | log.Println("===") 46 | rows.Close() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package sqlite3_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/ncruces/go-sqlite3" 8 | _ "github.com/ncruces/go-sqlite3/embed" 9 | ) 10 | 11 | const memory = ":memory:" 12 | 13 | func Example() { 14 | db, err := sqlite3.Open(memory) 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | 19 | err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | err = db.Exec(`INSERT INTO users (id, name) VALUES (0, 'go'), (1, 'zig'), (2, 'whatever')`) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | stmt, _, err := db.Prepare(`SELECT id, name FROM users`) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | defer stmt.Close() 34 | 35 | for stmt.Step() { 36 | fmt.Println(stmt.ColumnInt(0), stmt.ColumnText(1)) 37 | } 38 | if err := stmt.Err(); err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | err = stmt.Close() 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | err = db.Close() 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | // Output: 52 | // 0 go 53 | // 1 zig 54 | // 2 whatever 55 | } 56 | -------------------------------------------------------------------------------- /internal/dotlk/dotlk_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package dotlk 4 | 5 | import ( 6 | "errors" 7 | "io/fs" 8 | "os" 9 | "strconv" 10 | 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | // TryLock returns nil if it acquired the lock, 15 | // fs.ErrExist if another process has the lock. 16 | func TryLock(name string) error { 17 | for retry := true; retry; retry = false { 18 | f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) 19 | if err == nil { 20 | f.WriteString(strconv.Itoa(os.Getpid())) 21 | f.Close() 22 | return nil 23 | } 24 | if !errors.Is(err, fs.ErrExist) { 25 | return err 26 | } 27 | if !removeStale(name) { 28 | break 29 | } 30 | } 31 | return fs.ErrExist 32 | } 33 | 34 | func removeStale(name string) bool { 35 | buf, err := os.ReadFile(name) 36 | if err != nil { 37 | return errors.Is(err, fs.ErrNotExist) 38 | } 39 | 40 | pid, err := strconv.Atoi(string(buf)) 41 | if err != nil { 42 | return false 43 | } 44 | if unix.Kill(pid, 0) == nil { 45 | return false 46 | } 47 | 48 | err = os.Remove(name) 49 | return err == nil || errors.Is(err, fs.ErrNotExist) 50 | } 51 | -------------------------------------------------------------------------------- /vfs/tests/speedtest1/wasm/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | ROOT=../../../../ 7 | BINARYEN="$ROOT/tools/binaryen/bin" 8 | WASI_SDK="$ROOT/tools/wasi-sdk/bin" 9 | 10 | "$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -g0 -O2 \ 11 | -o speedtest1.wasm main.c \ 12 | -I"$ROOT/sqlite3/libc" -I"$ROOT/sqlite3" \ 13 | -mmutable-globals -mnontrapping-fptoint \ 14 | -msimd128 -mbulk-memory -msign-ext \ 15 | -mreference-types -mmultivalue \ 16 | -mno-extended-const \ 17 | -fno-stack-protector \ 18 | -Wl,--stack-first \ 19 | -Wl,--import-undefined \ 20 | -D_HAVE_SQLITE_CONFIG_H -DSQLITE_USE_URI \ 21 | -DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \ 22 | $(awk '{print "-Wl,--export="$0}' exports.txt) 23 | 24 | "$BINARYEN/wasm-opt" -g speedtest1.wasm -o speedtest1.tmp \ 25 | --low-memory-unused --gufa --generate-global-effects --converge -O3 \ 26 | --enable-mutable-globals --enable-nontrapping-float-to-int \ 27 | --enable-simd --enable-bulk-memory --enable-sign-ext \ 28 | --enable-reference-types --enable-multivalue \ 29 | --strip --strip-producers 30 | mv speedtest1.tmp speedtest1.wasm -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nuno Cruces 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /embed/bcw2/README.md: -------------------------------------------------------------------------------- 1 | # Embeddable Wasm build of SQLite 2 | 3 | This folder includes an alternative embeddable Wasm build of SQLite, 4 | which includes the experimental 5 | [`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md) and 6 | [Wal2](https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md) patches. 7 | 8 | It also enables the optional 9 | [`UPDATE … ORDER BY … LIMIT`](https://sqlite.org/lang_update.html#optional_limit_and_order_by_clauses) and 10 | [`DELETE … ORDER BY … LIMIT`](https://sqlite.org/lang_delete.html#optional_limit_and_order_by_clauses) clauses, 11 | and the [`WITHIN GROUP ORDER BY`](https://sqlite.org/compile.html#enable_ordered_set_aggregates) aggregate syntax. 12 | 13 | > [!IMPORTANT] 14 | > This package is experimental. 15 | > It is built from the `bedrock` branch of SQLite, 16 | > since that is _currently_ the most stable, maintained branch to include these features. 17 | 18 | > [!CAUTION] 19 | > The Wal2 journaling mode creates databases that other versions of SQLite cannot access. 20 | 21 | The build is easily reproducible, and verifiable, using 22 | [Artifact Attestations](https://github.com/ncruces/go-sqlite3/attestations). -------------------------------------------------------------------------------- /internal/util/json.go: -------------------------------------------------------------------------------- 1 | //go:build !goexperiment.jsonv2 2 | 3 | package util 4 | 5 | import ( 6 | "encoding/json" 7 | "math" 8 | "strconv" 9 | "time" 10 | "unsafe" 11 | ) 12 | 13 | type JSON struct{ Value any } 14 | 15 | func (j JSON) Scan(value any) error { 16 | var buf []byte 17 | 18 | switch v := value.(type) { 19 | case []byte: 20 | buf = v 21 | case string: 22 | buf = unsafe.Slice(unsafe.StringData(v), len(v)) 23 | case int64: 24 | buf = strconv.AppendInt(nil, v, 10) 25 | case float64: 26 | buf = AppendNumber(nil, v) 27 | case time.Time: 28 | buf = append(buf, '"') 29 | buf = v.AppendFormat(buf, time.RFC3339Nano) 30 | buf = append(buf, '"') 31 | case nil: 32 | buf = []byte("null") 33 | default: 34 | panic(AssertErr()) 35 | } 36 | 37 | return json.Unmarshal(buf, j.Value) 38 | } 39 | 40 | func AppendNumber(dst []byte, f float64) []byte { 41 | switch { 42 | case math.IsNaN(f): 43 | dst = append(dst, "null"...) 44 | case math.IsInf(f, 1): 45 | dst = append(dst, "9.0e999"...) 46 | case math.IsInf(f, -1): 47 | dst = append(dst, "-9.0e999"...) 48 | default: 49 | return strconv.AppendFloat(dst, f, 'g', -1, 64) 50 | } 51 | return dst 52 | } 53 | -------------------------------------------------------------------------------- /vfs/tests/mptest/testdata/config01.test: -------------------------------------------------------------------------------- 1 | /* 2 | ** Configure five tasks in different ways, then run tests. 3 | */ 4 | --if vfsname() GLOB 'unix' 5 | PRAGMA page_size=8192; 6 | --task 1 7 | PRAGMA journal_mode=PERSIST; 8 | PRAGMA mmap_size=0; 9 | --end 10 | --task 2 11 | PRAGMA journal_mode=TRUNCATE; 12 | PRAGMA mmap_size=28672; 13 | --end 14 | --task 3 15 | PRAGMA journal_mode=MEMORY; 16 | --end 17 | --task 4 18 | PRAGMA journal_mode=OFF; 19 | --end 20 | --task 4 21 | PRAGMA mmap_size(268435456); 22 | --end 23 | --source multiwrite01.test 24 | --wait all 25 | PRAGMA page_size=16384; 26 | VACUUM; 27 | CREATE TABLE pgsz(taskid, sz INTEGER); 28 | --task 1 29 | INSERT INTO pgsz VALUES(1, eval('PRAGMA page_size')); 30 | --end 31 | --task 2 32 | INSERT INTO pgsz VALUES(2, eval('PRAGMA page_size')); 33 | --end 34 | --task 3 35 | INSERT INTO pgsz VALUES(3, eval('PRAGMA page_size')); 36 | --end 37 | --task 4 38 | INSERT INTO pgsz VALUES(4, eval('PRAGMA page_size')); 39 | --end 40 | --task 5 41 | INSERT INTO pgsz VALUES(5, eval('PRAGMA page_size')); 42 | --end 43 | --source multiwrite01.test 44 | --wait all 45 | SELECT sz FROM pgsz; 46 | --match 16384 16384 16384 16384 16384 47 | -------------------------------------------------------------------------------- /internal/util/json_v2.go: -------------------------------------------------------------------------------- 1 | //go:build goexperiment.jsonv2 2 | 3 | package util 4 | 5 | import ( 6 | "encoding/json/v2" 7 | "math" 8 | "strconv" 9 | "time" 10 | "unsafe" 11 | ) 12 | 13 | type JSON struct{ Value any } 14 | 15 | func (j JSON) Scan(value any) error { 16 | var buf []byte 17 | 18 | switch v := value.(type) { 19 | case []byte: 20 | buf = v 21 | case string: 22 | buf = unsafe.Slice(unsafe.StringData(v), len(v)) 23 | case int64: 24 | buf = strconv.AppendInt(nil, v, 10) 25 | case float64: 26 | buf = AppendNumber(nil, v) 27 | case time.Time: 28 | buf = append(buf, '"') 29 | buf = v.AppendFormat(buf, time.RFC3339Nano) 30 | buf = append(buf, '"') 31 | case nil: 32 | buf = []byte("null") 33 | default: 34 | panic(AssertErr()) 35 | } 36 | 37 | return json.Unmarshal(buf, j.Value) 38 | } 39 | 40 | func AppendNumber(dst []byte, f float64) []byte { 41 | switch { 42 | case math.IsNaN(f): 43 | dst = append(dst, "null"...) 44 | case math.IsInf(f, 1): 45 | dst = append(dst, "9.0e999"...) 46 | case math.IsInf(f, -1): 47 | dst = append(dst, "-9.0e999"...) 48 | default: 49 | return strconv.AppendFloat(dst, f, 'g', -1, 64) 50 | } 51 | return dst 52 | } 53 | -------------------------------------------------------------------------------- /gormlite/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nuno Cruces 4 | Copyright (c) 2023 Jinzhu 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /util/sql3util/const.go: -------------------------------------------------------------------------------- 1 | package sql3util 2 | 3 | const ( 4 | _NONE = iota 5 | _MEMORY 6 | _SYNTAX 7 | _UNSUPPORTEDSQL 8 | ) 9 | 10 | type ConflictClause uint32 11 | 12 | const ( 13 | CONFLICT_NONE ConflictClause = iota 14 | CONFLICT_ROLLBACK 15 | CONFLICT_ABORT 16 | CONFLICT_FAIL 17 | CONFLICT_IGNORE 18 | CONFLICT_REPLACE 19 | ) 20 | 21 | type OrderClause uint32 22 | 23 | const ( 24 | ORDER_NONE OrderClause = iota 25 | ORDER_ASC 26 | ORDER_DESC 27 | ) 28 | 29 | type FKAction uint32 30 | 31 | const ( 32 | FKACTION_NONE FKAction = iota 33 | FKACTION_SETNULL 34 | FKACTION_SETDEFAULT 35 | FKACTION_CASCADE 36 | FKACTION_RESTRICT 37 | FKACTION_NOACTION 38 | ) 39 | 40 | type FKDefType uint32 41 | 42 | const ( 43 | DEFTYPE_NONE FKDefType = iota 44 | DEFTYPE_DEFERRABLE 45 | DEFTYPE_DEFERRABLE_INITIALLY_DEFERRED 46 | DEFTYPE_DEFERRABLE_INITIALLY_IMMEDIATE 47 | DEFTYPE_NOTDEFERRABLE 48 | DEFTYPE_NOTDEFERRABLE_INITIALLY_DEFERRED 49 | DEFTYPE_NOTDEFERRABLE_INITIALLY_IMMEDIATE 50 | ) 51 | 52 | type StatementType uint32 53 | 54 | const ( 55 | CREATE_UNKNOWN StatementType = iota 56 | CREATE_TABLE 57 | ALTER_RENAME_TABLE 58 | ALTER_RENAME_COLUMN 59 | ALTER_ADD_COLUMN 60 | ALTER_DROP_COLUMN 61 | ) 62 | -------------------------------------------------------------------------------- /vfs/readervfs/api.go: -------------------------------------------------------------------------------- 1 | // Package readervfs implements an SQLite VFS for immutable databases. 2 | // 3 | // The "reader" [vfs.VFS] permits accessing any [io.ReaderAt] 4 | // as an immutable SQLite database. 5 | // 6 | // Importing package readervfs registers the VFS: 7 | // 8 | // import _ "github.com/ncruces/go-sqlite3/vfs/readervfs" 9 | package readervfs 10 | 11 | import ( 12 | "sync" 13 | 14 | "github.com/ncruces/go-sqlite3/util/ioutil" 15 | "github.com/ncruces/go-sqlite3/vfs" 16 | ) 17 | 18 | func init() { 19 | vfs.Register("reader", readerVFS{}) 20 | } 21 | 22 | var ( 23 | readerMtx sync.RWMutex 24 | // +checklocks:readerMtx 25 | readerDBs = map[string]ioutil.SizeReaderAt{} 26 | ) 27 | 28 | // Create creates an immutable database from reader. 29 | // The caller should ensure that data from reader does not mutate, 30 | // otherwise SQLite might return incorrect query results and/or [sqlite3.CORRUPT] errors. 31 | func Create(name string, reader ioutil.SizeReaderAt) { 32 | readerMtx.Lock() 33 | readerDBs[name] = reader 34 | readerMtx.Unlock() 35 | } 36 | 37 | // Delete deletes a shared memory database. 38 | func Delete(name string) { 39 | readerMtx.Lock() 40 | delete(readerDBs, name) 41 | readerMtx.Unlock() 42 | } 43 | -------------------------------------------------------------------------------- /util/sql3util/wasm/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | ROOT=../../../ 7 | BINARYEN="$ROOT/tools/binaryen/bin" 8 | WASI_SDK="$ROOT/tools/wasi-sdk/bin" 9 | 10 | trap 'rm -f sql3parse_table.tmp' EXIT 11 | 12 | "$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -g0 -Oz \ 13 | -Wall -Wextra -Wno-unused-parameter -Wno-unused-function \ 14 | -o sql3parse_table.wasm main.c \ 15 | -I"$ROOT/sqlite3/libc" -I"$ROOT/sqlite3" \ 16 | -mexec-model=reactor \ 17 | -mmutable-globals -mnontrapping-fptoint \ 18 | -msimd128 -mbulk-memory -msign-ext \ 19 | -mreference-types -mmultivalue \ 20 | -mno-extended-const \ 21 | -fno-stack-protector \ 22 | -Wl,--stack-first \ 23 | -Wl,--import-undefined \ 24 | -Wl,--export=sql3parse_table 25 | 26 | "$BINARYEN/wasm-ctor-eval" -c _initialize sql3parse_table.wasm -o sql3parse_table.tmp 27 | "$BINARYEN/wasm-opt" sql3parse_table.tmp -o sql3parse_table.wasm \ 28 | --low-memory-unused --gufa --generate-global-effects --converge -Oz \ 29 | --enable-mutable-globals --enable-nontrapping-float-to-int \ 30 | --enable-simd --enable-bulk-memory --enable-sign-ext \ 31 | --enable-reference-types --enable-multivalue \ 32 | --strip --strip-debug --strip-producers -------------------------------------------------------------------------------- /sqlite3/time.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "sqlite3.h" 5 | 6 | static int time_collation(void *pArg, int nKey1, const void *pKey1, int nKey2, 7 | const void *pKey2) { 8 | UNUSED_PARAMETER(pArg); 9 | 10 | // Remove a Z suffix if one key is no longer than the other. 11 | // A Z suffix collates before any character but after the empty string. 12 | // This avoids making different keys equal. 13 | const int nK1 = nKey1; 14 | const int nK2 = nKey2; 15 | const char *pK1 = (const char *)pKey1; 16 | const char *pK2 = (const char *)pKey2; 17 | if (nK1 && nK1 <= nK2 && pK1[nK1 - 1] == 'Z') { 18 | nKey1--; 19 | } 20 | if (nK2 && nK2 <= nK1 && pK2[nK2 - 1] == 'Z') { 21 | nKey2--; 22 | } 23 | 24 | int n = nKey1 < nKey2 ? nKey1 : nKey2; 25 | int rc = memcmp(pKey1, pKey2, n); 26 | if (rc == 0) { 27 | rc = nKey1 - nKey2; 28 | } 29 | return rc; 30 | } 31 | 32 | int sqlite3_time_init(sqlite3 *db, char **pzErrMsg, 33 | const sqlite3_api_routines *pApi) { 34 | UNUSED_PARAMETER2(pzErrMsg, pApi); 35 | sqlite3_create_collation_v2(db, "time", SQLITE_UTF8, /*arg=*/NULL, 36 | time_collation, /*destroy=*/NULL); 37 | return SQLITE_OK; 38 | } -------------------------------------------------------------------------------- /vfs/shm.go: -------------------------------------------------------------------------------- 1 | //go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le || loong64)) || sqlite3_flock || sqlite3_dotlk 2 | 3 | package vfs 4 | 5 | // SupportsSharedMemory is false on platforms that do not support shared memory. 6 | // To use [WAL without shared-memory], you need to set [EXCLUSIVE locking mode]. 7 | // 8 | // [WAL without shared-memory]: https://sqlite.org/wal.html#noshm 9 | // [EXCLUSIVE locking mode]: https://sqlite.org/pragma.html#pragma_locking_mode 10 | const SupportsSharedMemory = true 11 | 12 | func (f *vfsFile) SharedMemory() SharedMemory { return f.shm } 13 | 14 | // NewSharedMemory returns a shared-memory WAL-index 15 | // backed by a file with the given path. 16 | // It will return nil if shared-memory is not supported, 17 | // or not appropriate for the given flags. 18 | // Only [OPEN_MAIN_DB] databases may need a WAL-index. 19 | // You must ensure all concurrent accesses to a database 20 | // use shared-memory instances created with the same path. 21 | func NewSharedMemory(path string, flags OpenFlag) SharedMemory { 22 | if flags&OPEN_MAIN_DB == 0 || flags&(OPEN_DELETEONCLOSE|OPEN_MEMORY) != 0 { 23 | return nil 24 | } 25 | return &vfsShm{path: path} 26 | } 27 | -------------------------------------------------------------------------------- /vfs/registry.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import "sync" 4 | 5 | var ( 6 | // +checklocks:vfsRegistryMtx 7 | vfsRegistry map[string]VFS 8 | vfsRegistryMtx sync.RWMutex 9 | ) 10 | 11 | // Find returns a VFS given its name. 12 | // If there is no match, nil is returned. 13 | // If name is empty or "os", the default VFS is returned. 14 | // 15 | // https://sqlite.org/c3ref/vfs_find.html 16 | func Find(name string) VFS { 17 | if name == "" || name == "os" { 18 | return vfsOS{} 19 | } 20 | return find(name) 21 | } 22 | 23 | func find(name string) VFS { 24 | vfsRegistryMtx.RLock() 25 | defer vfsRegistryMtx.RUnlock() 26 | return vfsRegistry[name] 27 | } 28 | 29 | // Register registers a VFS. 30 | // Empty and "os" are reserved names. 31 | // 32 | // https://sqlite.org/c3ref/vfs_find.html 33 | func Register(name string, vfs VFS) { 34 | if name == "" || name == "os" { 35 | return 36 | } 37 | vfsRegistryMtx.Lock() 38 | if vfsRegistry == nil { 39 | vfsRegistry = map[string]VFS{} 40 | } 41 | vfsRegistry[name] = vfs 42 | vfsRegistryMtx.Unlock() 43 | } 44 | 45 | // Unregister unregisters a VFS. 46 | // 47 | // https://sqlite.org/c3ref/vfs_find.html 48 | func Unregister(name string) { 49 | vfsRegistryMtx.Lock() 50 | delete(vfsRegistry, name) 51 | vfsRegistryMtx.Unlock() 52 | } 53 | -------------------------------------------------------------------------------- /util/ioutil/size.go: -------------------------------------------------------------------------------- 1 | package ioutil 2 | 3 | import ( 4 | "io" 5 | "io/fs" 6 | 7 | "github.com/ncruces/go-sqlite3" 8 | ) 9 | 10 | // A SizeReaderAt is a ReaderAt with a Size method. 11 | // Use [NewSizeReaderAt] to adapt different Size interfaces. 12 | type SizeReaderAt interface { 13 | Size() (int64, error) 14 | io.ReaderAt 15 | } 16 | 17 | // NewSizeReaderAt returns a SizeReaderAt given an io.ReaderAt 18 | // that implements one of: 19 | // - Size() (int64, error) 20 | // - Size() int64 21 | // - Len() int 22 | // - Stat() (fs.FileInfo, error) 23 | // - Seek(offset int64, whence int) (int64, error) 24 | func NewSizeReaderAt(r io.ReaderAt) SizeReaderAt { 25 | return sizer{r} 26 | } 27 | 28 | type sizer struct{ io.ReaderAt } 29 | 30 | func (s sizer) Size() (int64, error) { 31 | switch s := s.ReaderAt.(type) { 32 | case interface{ Size() (int64, error) }: 33 | return s.Size() 34 | case interface{ Size() int64 }: 35 | return s.Size(), nil 36 | case interface{ Len() int }: 37 | return int64(s.Len()), nil 38 | case interface{ Stat() (fs.FileInfo, error) }: 39 | fi, err := s.Stat() 40 | if err != nil { 41 | return 0, err 42 | } 43 | return fi.Size(), nil 44 | case io.Seeker: 45 | return s.Seek(0, io.SeekEnd) 46 | } 47 | return 0, sqlite3.IOERR_SEEK 48 | } 49 | -------------------------------------------------------------------------------- /func_seq_test.go: -------------------------------------------------------------------------------- 1 | package sqlite3_test 2 | 3 | import ( 4 | "fmt" 5 | "iter" 6 | "log" 7 | 8 | "github.com/ncruces/go-sqlite3" 9 | _ "github.com/ncruces/go-sqlite3/embed" 10 | ) 11 | 12 | func ExampleConn_CreateAggregateFunction() { 13 | db, err := sqlite3.Open(":memory:") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | defer db.Close() 18 | 19 | err = db.Exec(`CREATE TABLE test (col)`) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | err = db.Exec(`INSERT INTO test VALUES (1), (2), (3)`) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | err = db.CreateAggregateFunction("seq_avg", 1, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS, 30 | func(ctx *sqlite3.Context, seq iter.Seq[[]sqlite3.Value]) { 31 | count := 0 32 | total := 0.0 33 | for arg := range seq { 34 | total += arg[0].Float() 35 | count++ 36 | } 37 | ctx.ResultFloat(total / float64(count)) 38 | }) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | stmt, _, err := db.Prepare(`SELECT seq_avg(col) FROM test`) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | defer stmt.Close() 48 | 49 | for stmt.Step() { 50 | fmt.Println(stmt.ColumnFloat(0)) 51 | } 52 | if err := stmt.Err(); err != nil { 53 | log.Fatal(err) 54 | } 55 | // Output: 56 | // 2 57 | } 58 | -------------------------------------------------------------------------------- /ext/csv/types.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/ncruces/go-sqlite3/util/sql3util" 7 | ) 8 | 9 | type affinity byte 10 | 11 | const ( 12 | blob affinity = 0 13 | text affinity = 1 14 | numeric affinity = 2 15 | integer affinity = 3 16 | real affinity = 4 17 | ) 18 | 19 | func getColumnAffinities(schema string) ([]affinity, error) { 20 | tab, err := sql3util.ParseTable(schema) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | columns := tab.Columns 26 | types := make([]affinity, len(columns)) 27 | for i, col := range columns { 28 | types[i] = getAffinity(col.Type) 29 | } 30 | return types, nil 31 | } 32 | 33 | func getAffinity(declType string) affinity { 34 | // https://sqlite.org/datatype3.html#determination_of_column_affinity 35 | if declType == "" { 36 | return blob 37 | } 38 | name := strings.ToUpper(declType) 39 | if strings.Contains(name, "INT") { 40 | return integer 41 | } 42 | if strings.Contains(name, "CHAR") || strings.Contains(name, "CLOB") || strings.Contains(name, "TEXT") { 43 | return text 44 | } 45 | if strings.Contains(name, "BLOB") { 46 | return blob 47 | } 48 | if strings.Contains(name, "REAL") || strings.Contains(name, "FLOA") || strings.Contains(name, "DOUB") { 49 | return real 50 | } 51 | return numeric 52 | } 53 | -------------------------------------------------------------------------------- /ext/hash/sha2.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "crypto" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | "github.com/ncruces/go-sqlite3/internal/util" 8 | ) 9 | 10 | func sha224Func(ctx sqlite3.Context, arg ...sqlite3.Value) { 11 | hashFunc(ctx, arg[0], crypto.SHA224) 12 | } 13 | 14 | func sha384Func(ctx sqlite3.Context, arg ...sqlite3.Value) { 15 | hashFunc(ctx, arg[0], crypto.SHA384) 16 | } 17 | 18 | func sha256Func(ctx sqlite3.Context, arg ...sqlite3.Value) { 19 | size := 256 20 | if len(arg) > 1 { 21 | size = arg[1].Int() 22 | } 23 | 24 | switch size { 25 | case 224: 26 | hashFunc(ctx, arg[0], crypto.SHA224) 27 | case 256: 28 | hashFunc(ctx, arg[0], crypto.SHA256) 29 | default: 30 | ctx.ResultError(util.ErrorString("sha256: size must be 224, 256")) 31 | } 32 | } 33 | 34 | func sha512Func(ctx sqlite3.Context, arg ...sqlite3.Value) { 35 | size := 512 36 | if len(arg) > 1 { 37 | size = arg[1].Int() 38 | } 39 | 40 | switch size { 41 | case 224: 42 | hashFunc(ctx, arg[0], crypto.SHA512_224) 43 | case 256: 44 | hashFunc(ctx, arg[0], crypto.SHA512_256) 45 | case 384: 46 | hashFunc(ctx, arg[0], crypto.SHA384) 47 | case 512: 48 | hashFunc(ctx, arg[0], crypto.SHA512) 49 | default: 50 | ctx.ResultError(util.ErrorString("sha512: size must be 224, 256, 384, 512")) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /gormlite/error_translator_test.go: -------------------------------------------------------------------------------- 1 | package gormlite 2 | 3 | import ( 4 | "testing" 5 | 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/logger" 8 | 9 | "github.com/ncruces/go-sqlite3/vfs/memdb" 10 | ) 11 | 12 | func TestErrorTranslator(t *testing.T) { 13 | // This is the example object for testing the unique constraint error 14 | type Article struct { 15 | ArticleNumber string `gorm:"unique"` 16 | } 17 | 18 | db, err := gorm.Open(Open(memdb.TestDB(t)), &gorm.Config{ 19 | Logger: logger.Default.LogMode(logger.Silent), 20 | TranslateError: true}) 21 | 22 | if err != nil { 23 | t.Errorf("Expected Open to succeed; got error: %v", err) 24 | } 25 | if db == nil { 26 | t.Errorf("Expected db to be non-nil.") 27 | } 28 | 29 | err = db.AutoMigrate(&Article{}) 30 | if err != nil { 31 | t.Errorf("Expected to migrate database models to succeed: %v", err) 32 | } 33 | 34 | err = db.Create(&Article{ArticleNumber: "A00000XX"}).Error 35 | if err != nil { 36 | t.Errorf("Expected first create to succeed: %v", err) 37 | } 38 | 39 | err = db.Create(&Article{ArticleNumber: "A00000XX"}).Error 40 | if err == nil { 41 | t.Errorf("Expected second create to fail.") 42 | } 43 | 44 | if err != gorm.ErrDuplicatedKey { 45 | t.Errorf("Expected error from second create to be gorm.ErrDuplicatedKey: %v", err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /vfs/tests/mptest/wasm/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | ROOT=../../../../ 7 | BINARYEN="$ROOT/tools/binaryen/bin" 8 | WASI_SDK="$ROOT/tools/wasi-sdk/bin" 9 | 10 | "$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -g0 -O2 \ 11 | -o mptest.wasm main.c \ 12 | -I"$ROOT/sqlite3/libc" -I"$ROOT/sqlite3" \ 13 | -mmutable-globals -mnontrapping-fptoint \ 14 | -msimd128 -mbulk-memory -msign-ext \ 15 | -mreference-types -mmultivalue \ 16 | -mno-extended-const \ 17 | -fno-stack-protector \ 18 | -Wl,--stack-first \ 19 | -Wl,--import-undefined \ 20 | -D_HAVE_SQLITE_CONFIG_H -DSQLITE_USE_URI \ 21 | -DSQLITE_DEFAULT_SYNCHRONOUS=0 \ 22 | -DSQLITE_DEFAULT_LOCKING_MODE=0 \ 23 | -DSQLITE_NO_SYNC -DSQLITE_THREADSAFE=0 \ 24 | -DSQLITE_OMIT_LOAD_EXTENSION -DHAVE_USLEEP \ 25 | -DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \ 26 | -D_WASI_EMULATED_GETPID -lwasi-emulated-getpid \ 27 | $(awk '{print "-Wl,--export="$0}' exports.txt) 28 | 29 | "$BINARYEN/wasm-opt" -g mptest.wasm -o mptest.tmp \ 30 | --low-memory-unused --gufa --generate-global-effects --converge -O3 \ 31 | --enable-mutable-globals --enable-nontrapping-float-to-int \ 32 | --enable-simd --enable-bulk-memory --enable-sign-ext \ 33 | --enable-reference-types --enable-multivalue \ 34 | --strip --strip-producers 35 | mv mptest.tmp mptest.wasm -------------------------------------------------------------------------------- /embed/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | ROOT=../ 7 | BINARYEN="$ROOT/tools/binaryen/bin" 8 | WASI_SDK="$ROOT/tools/wasi-sdk/bin" 9 | 10 | trap 'rm -f sqlite3.tmp' EXIT 11 | 12 | "$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -g0 -O2 \ 13 | -Wall -Wextra -Wno-unused-parameter -Wno-unused-function \ 14 | -o sqlite3.wasm "$ROOT/sqlite3/main.c" \ 15 | -I"$ROOT/sqlite3/libc" -I"$ROOT/sqlite3" \ 16 | -mexec-model=reactor \ 17 | -mmutable-globals -mnontrapping-fptoint \ 18 | -msimd128 -mbulk-memory -msign-ext \ 19 | -mreference-types -mmultivalue \ 20 | -mno-extended-const \ 21 | -fno-stack-protector \ 22 | -Wl,--stack-first \ 23 | -Wl,--import-undefined \ 24 | -Wl,--initial-memory=327680 \ 25 | -D_HAVE_SQLITE_CONFIG_H \ 26 | -DSQLITE_EXPERIMENTAL_PRAGMA_20251114 \ 27 | -DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \ 28 | $(awk '{print "-Wl,--export="$0}' exports.txt) 29 | 30 | "$BINARYEN/wasm-ctor-eval" -g -c _initialize sqlite3.wasm -o sqlite3.tmp 31 | "$BINARYEN/wasm-opt" -g sqlite3.tmp -o sqlite3.wasm \ 32 | --low-memory-unused --gufa --generate-global-effects --converge -O3 \ 33 | --enable-mutable-globals --enable-nontrapping-float-to-int \ 34 | --enable-simd --enable-bulk-memory --enable-sign-ext \ 35 | --enable-reference-types --enable-multivalue \ 36 | --strip --strip-producers -------------------------------------------------------------------------------- /embed/bcw2/bcw2_test.go: -------------------------------------------------------------------------------- 1 | package bcw2 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/ncruces/go-sqlite3/driver" 8 | "github.com/ncruces/go-sqlite3/ext/stats" 9 | "github.com/ncruces/go-sqlite3/vfs" 10 | ) 11 | 12 | func Test_bcw2(t *testing.T) { 13 | if !vfs.SupportsSharedMemory { 14 | t.Skip("skipping without shared memory") 15 | } 16 | 17 | tmp := filepath.ToSlash(filepath.Join(t.TempDir(), "test.db")) 18 | 19 | db, err := driver.Open("file:"+tmp+"?_pragma=journal_mode(wal2)&_txlock=concurrent", stats.Register) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | defer db.Close() 24 | 25 | tx, err := db.Begin() 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | defer tx.Rollback() 30 | 31 | _, err = tx.Exec(`CREATE TABLE test (col)`) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | _, err = tx.Exec(`DELETE FROM test LIMIT 1`) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | _, err = tx.Exec(`SELECT median() WITHIN GROUP (ORDER BY col) FROM test`) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | err = tx.Commit() 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | var version string 52 | err = db.QueryRow(`SELECT sqlite_version()`).Scan(&version) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if version != "3.52.0" { 57 | t.Error(version) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /gormlite/go.sum: -------------------------------------------------------------------------------- 1 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 2 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 3 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 4 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 5 | github.com/ncruces/go-sqlite3 v0.30.3 h1:X/CgWW9GzmIAkEPrifhKqf0cC15DuOVxAJaHFTTAURQ= 6 | github.com/ncruces/go-sqlite3 v0.30.3/go.mod h1:AxKu9sRxkludimFocbktlY6LiYSkxiI5gTA8r+os/Nw= 7 | github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= 8 | github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= 9 | github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= 10 | github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= 11 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 12 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 13 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 14 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 15 | gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= 16 | gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= 17 | -------------------------------------------------------------------------------- /sqlite3/tools.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | ROOT=../ 7 | 8 | if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then 9 | WASI_SDK="x86_64-windows" 10 | BINARYEN="x86_64-windows" 11 | elif [[ "$OSTYPE" == "linux"* ]]; then 12 | if [[ "$(uname -m)" == "x86_64" ]]; then 13 | WASI_SDK="x86_64-linux" 14 | BINARYEN="x86_64-linux" 15 | else 16 | WASI_SDK="arm64-linux" 17 | BINARYEN="aarch64-linux" 18 | fi 19 | elif [[ "$OSTYPE" == "darwin"* ]]; then 20 | if [[ "$(uname -m)" == "x86_64" ]]; then 21 | WASI_SDK="x86_64-macos" 22 | BINARYEN="x86_64-macos" 23 | else 24 | WASI_SDK="arm64-macos" 25 | BINARYEN="arm64-macos" 26 | fi 27 | fi 28 | 29 | WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-29/wasi-sdk-29.0-$WASI_SDK.tar.gz" 30 | BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_125/binaryen-version_125-$BINARYEN.tar.gz" 31 | 32 | # Download tools 33 | mkdir -p "$ROOT/tools" 34 | [ -d "$ROOT/tools/wasi-sdk" ] || curl -#L "$WASI_SDK" | tar xzC "$ROOT/tools" & 35 | [ -d "$ROOT/tools/binaryen" ] || curl -#L "$BINARYEN" | tar xzC "$ROOT/tools" & 36 | wait 37 | 38 | [ -d "$ROOT/tools/wasi-sdk" ] || mv "$ROOT/tools/wasi-sdk"* "$ROOT/tools/wasi-sdk" 39 | [ -d "$ROOT/tools/binaryen" ] || mv "$ROOT/tools/binaryen"* "$ROOT/tools/binaryen" -------------------------------------------------------------------------------- /util/fsutil/mode_test.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "io/fs" 5 | "testing" 6 | ) 7 | 8 | func TestFileModeFromUnix(t *testing.T) { 9 | tests := []struct { 10 | mode fs.FileMode 11 | want fs.FileMode 12 | }{ 13 | {0010754, 0754 | fs.ModeNamedPipe}, 14 | {0020754, 0754 | fs.ModeCharDevice | fs.ModeDevice}, 15 | {0040754, 0754 | fs.ModeDir}, 16 | {0060754, 0754 | fs.ModeDevice}, 17 | {0100754, 0754}, 18 | {0120754, 0754 | fs.ModeSymlink}, 19 | {0140754, 0754 | fs.ModeSocket}, 20 | {0170754, 0754 | fs.ModeIrregular}, 21 | } 22 | for _, tt := range tests { 23 | t.Run(tt.mode.String(), func(t *testing.T) { 24 | if got := FileModeFromUnix(tt.mode); got != tt.want { 25 | t.Errorf("fixMode() = %o, want %o", got, tt.want) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func FuzzParseFileMode(f *testing.F) { 32 | f.Add("---------") 33 | f.Add("rwxrwxrwx") 34 | f.Add("----------") 35 | f.Add("-rwxrwxrwx") 36 | f.Add("b") 37 | f.Add("b---------") 38 | f.Add("drwxrwxrwx") 39 | f.Add("dalTLDpSugct?") 40 | f.Add("dalTLDpSugct?---------") 41 | f.Add("dalTLDpSugct?rwxrwxrwx") 42 | f.Add("dalTLDpSugct?----------") 43 | 44 | f.Fuzz(func(t *testing.T, str string) { 45 | mode, err := ParseFileMode(str) 46 | if err != nil { 47 | return 48 | } 49 | got := mode.String() 50 | if got != str { 51 | t.Errorf("was %q, got %q (%o)", str, got, mode) 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /sqlite3/sqlite_opt.h: -------------------------------------------------------------------------------- 1 | // Recommended Options 2 | 3 | #define SQLITE_DQS 0 4 | #define SQLITE_THREADSAFE 0 5 | #define SQLITE_DEFAULT_WAL_SYNCHRONOUS 1 6 | #define SQLITE_LIKE_DOESNT_MATCH_BLOBS 7 | #define SQLITE_STRICT_SUBTYPE 1 8 | #define SQLITE_OMIT_DEPRECATED 9 | #define SQLITE_OMIT_SHARED_CACHE 10 | #define SQLITE_OMIT_AUTOINIT 11 | 12 | // We need these: 13 | // #define SQLITE_DEFAULT_MEMSTATUS 0 14 | // #define SQLITE_MAX_EXPR_DEPTH 0 15 | // #define SQLITE_USE_ALLOCA 16 | // #define SQLITE_OMIT_DECLTYPE 17 | // #define SQLITE_OMIT_PROGRESS_CALLBACK 18 | 19 | // Other Options 20 | 21 | #define SQLITE_ALLOW_URI_AUTHORITY 22 | #define SQLITE_TRUSTED_SCHEMA 0 23 | #define SQLITE_DEFAULT_FOREIGN_KEYS 1 24 | #define SQLITE_ENABLE_API_ARMOR 25 | #define SQLITE_ENABLE_ATOMIC_WRITE 26 | #define SQLITE_ENABLE_BATCH_ATOMIC_WRITE 27 | #define SQLITE_ENABLE_COLUMN_METADATA 28 | #define SQLITE_ENABLE_SETLK_TIMEOUT 2 29 | #define SQLITE_ENABLE_STAT4 1 30 | 31 | // We have our own memdb VFS. 32 | // To avoid interactions between the two, 33 | // omit sqlite3_serialize/sqlite3_deserialize, 34 | // which we also don't wrap. 35 | #define SQLITE_OMIT_DESERIALIZE 36 | 37 | // Amalgamated Extensions 38 | 39 | #define SQLITE_ENABLE_MATH_FUNCTIONS 1 40 | #define SQLITE_ENABLE_JSON1 1 41 | #define SQLITE_ENABLE_FTS5 1 42 | #define SQLITE_ENABLE_RTREE 1 43 | #define SQLITE_ENABLE_GEOPOLY 1 44 | 45 | #define SQLITE_SOUNDEX 46 | #define SQLITE_UNTESTABLE -------------------------------------------------------------------------------- /internal/util/mmap_windows.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "unsafe" 7 | 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | type MappedRegion struct { 12 | windows.Handle 13 | Data []byte 14 | addr uintptr 15 | } 16 | 17 | func MapRegion(f *os.File, offset int64, size int32) (*MappedRegion, error) { 18 | maxSize := offset + int64(size) 19 | h, err := windows.CreateFileMapping( 20 | windows.Handle(f.Fd()), nil, windows.PAGE_READWRITE, 21 | uint32(maxSize>>32), uint32(maxSize), nil) 22 | if h == 0 { 23 | return nil, err 24 | } 25 | 26 | const allocationGranularity = 64 * 1024 27 | align := offset % allocationGranularity 28 | offset -= align 29 | 30 | a, err := windows.MapViewOfFile(h, windows.FILE_MAP_WRITE, 31 | uint32(offset>>32), uint32(offset), uintptr(size)+uintptr(align)) 32 | if a == 0 { 33 | windows.CloseHandle(h) 34 | return nil, err 35 | } 36 | 37 | ret := &MappedRegion{Handle: h, addr: a} 38 | // SliceHeader, although deprecated, avoids a go vet warning. 39 | sh := (*reflect.SliceHeader)(unsafe.Pointer(&ret.Data)) 40 | sh.Data = a + uintptr(align) 41 | sh.Len = int(size) 42 | sh.Cap = int(size) 43 | return ret, nil 44 | } 45 | 46 | func (r *MappedRegion) Unmap() error { 47 | if r.Data == nil { 48 | return nil 49 | } 50 | err := windows.UnmapViewOfFile(r.addr) 51 | if err != nil { 52 | return err 53 | } 54 | r.Data = nil 55 | return windows.CloseHandle(r.Handle) 56 | } 57 | -------------------------------------------------------------------------------- /util/ioutil/seek.go: -------------------------------------------------------------------------------- 1 | package ioutil 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | // SeekingReaderAt implements [io.ReaderAt] 9 | // through an underlying [io.ReadSeeker]. 10 | type SeekingReaderAt struct { 11 | // +checklocks:l 12 | r io.ReadSeeker 13 | l sync.Mutex 14 | } 15 | 16 | // NewSeekingReaderAt creates a new SeekingReaderAt. 17 | // The SeekingReaderAt takes ownership of r 18 | // and will modify its seek offset, 19 | // so callers should not use r after this call. 20 | func NewSeekingReaderAt(r io.ReadSeeker) *SeekingReaderAt { 21 | return &SeekingReaderAt{r: r} 22 | } 23 | 24 | // ReadAt implements [io.ReaderAt]. 25 | func (s *SeekingReaderAt) ReadAt(p []byte, off int64) (n int, _ error) { 26 | s.l.Lock() 27 | defer s.l.Unlock() 28 | 29 | _, err := s.r.Seek(off, io.SeekStart) 30 | if err != nil { 31 | return 0, err 32 | } 33 | 34 | for len(p) > 0 { 35 | i, err := s.r.Read(p) 36 | p = p[i:] 37 | n += i 38 | if err != nil { 39 | return n, err 40 | } 41 | } 42 | return n, nil 43 | } 44 | 45 | // Size implements [SizeReaderAt]. 46 | func (s *SeekingReaderAt) Size() (int64, error) { 47 | s.l.Lock() 48 | defer s.l.Unlock() 49 | return s.r.Seek(0, io.SeekEnd) 50 | } 51 | 52 | // Close implements [io.Closer]. 53 | func (s *SeekingReaderAt) Close() error { 54 | s.l.Lock() 55 | defer s.l.Unlock() 56 | if c, ok := s.r.(io.Closer); ok { 57 | s.r = nil 58 | return c.Close() 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /vfs/registry_test.go: -------------------------------------------------------------------------------- 1 | package vfs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | _ "github.com/ncruces/go-sqlite3/embed" 8 | "github.com/ncruces/go-sqlite3/vfs" 9 | ) 10 | 11 | type testVFS struct { 12 | *testing.T 13 | } 14 | 15 | func (t testVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) { 16 | t.Log("Open", name, flags) 17 | t.SkipNow() 18 | return nil, flags, nil 19 | } 20 | 21 | func (t testVFS) Delete(name string, syncDir bool) error { 22 | t.Log("Delete", name, syncDir) 23 | return nil 24 | } 25 | 26 | func (t testVFS) Access(name string, flags vfs.AccessFlag) (bool, error) { 27 | t.Log("Access", name, flags) 28 | return true, nil 29 | } 30 | 31 | func (t testVFS) FullPathname(name string) (string, error) { 32 | t.Log("FullPathname", name) 33 | return name, nil 34 | } 35 | 36 | func TestRegister(t *testing.T) { 37 | vfs.Register("foo", testVFS{t}) 38 | defer vfs.Unregister("foo") 39 | 40 | conn, err := sqlite3.Open("file:file.db?vfs=foo") 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | defer conn.Close() 45 | 46 | t.Error("want skip") 47 | } 48 | 49 | func TestRegister_os(t *testing.T) { 50 | os := vfs.Find("os") 51 | if os == nil { 52 | t.Fail() 53 | } 54 | 55 | vfs.Register("os", testVFS{t}) 56 | if vfs.Find("os") != os { 57 | t.Fail() 58 | } 59 | 60 | vfs.Unregister("os") 61 | if vfs.Find("os") != os { 62 | t.Fail() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /driver/util.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "database/sql/driver" 4 | 5 | func namedValues(args []driver.Value) []driver.NamedValue { 6 | named := make([]driver.NamedValue, len(args)) 7 | for i, v := range args { 8 | named[i] = driver.NamedValue{ 9 | Ordinal: i + 1, 10 | Value: v, 11 | } 12 | } 13 | return named 14 | } 15 | 16 | func notWhitespace(sql string) bool { 17 | const ( 18 | code = iota 19 | slash 20 | minus 21 | ccomment 22 | sqlcomment 23 | endcomment 24 | ) 25 | 26 | state := code 27 | for _, b := range ([]byte)(sql) { 28 | if b == 0 { 29 | break 30 | } 31 | 32 | switch state { 33 | case code: 34 | switch b { 35 | case '/': 36 | state = slash 37 | case '-': 38 | state = minus 39 | case ' ', ';', '\t', '\n', '\v', '\f', '\r': 40 | continue 41 | default: 42 | return true 43 | } 44 | case slash: 45 | if b != '*' { 46 | return true 47 | } 48 | state = ccomment 49 | case minus: 50 | if b != '-' { 51 | return true 52 | } 53 | state = sqlcomment 54 | case ccomment: 55 | if b == '*' { 56 | state = endcomment 57 | } 58 | case sqlcomment: 59 | if b == '\n' { 60 | state = code 61 | } 62 | case endcomment: 63 | switch b { 64 | case '/': 65 | state = code 66 | case '*': 67 | state = endcomment 68 | default: 69 | state = ccomment 70 | } 71 | } 72 | } 73 | return state == slash || state == minus 74 | } 75 | -------------------------------------------------------------------------------- /gormlite/ddlmod_parse_all_columns_test.go: -------------------------------------------------------------------------------- 1 | package gormlite 2 | 3 | import "testing" 4 | 5 | func TestParseAllColumns(t *testing.T) { 6 | tc := []struct { 7 | name string 8 | input string 9 | expected []string 10 | }{ 11 | { 12 | name: "Simple case", 13 | input: "PRIMARY KEY (column1, column2)", 14 | expected: []string{"column1", "column2"}, 15 | }, 16 | { 17 | name: "Quoted column name", 18 | input: "PRIMARY KEY (`column,xxx`, \"column 2\", \"column)3\", 'column''4', \"column\"\"5\")", 19 | expected: []string{"column,xxx", "column 2", "column)3", "column'4", "column\"5"}, 20 | }, 21 | { 22 | name: "Japanese column name", 23 | input: "PRIMARY KEY (カラム1, `カラム2`)", 24 | expected: []string{"カラム1", "カラム2"}, 25 | }, 26 | { 27 | name: "Column name quoted with []", 28 | input: "PRIMARY KEY ([column1], [column2])", 29 | expected: []string{"column1", "column2"}, 30 | }, 31 | } 32 | for _, tt := range tc { 33 | t.Run(tt.name, func(t *testing.T) { 34 | cols, err := parseAllColumns(tt.input) 35 | if err != nil { 36 | t.Errorf("Failed to parse columns: %s", err) 37 | } 38 | if len(cols) != len(tt.expected) { 39 | t.Errorf("Expected %d columns, got %d", len(tt.expected), len(cols)) 40 | } 41 | for i, col := range cols { 42 | if col != tt.expected[i] { 43 | t.Errorf("Expected %s, got %s", tt.expected[i], col) 44 | } 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /vfs/shm_memlk.go: -------------------------------------------------------------------------------- 1 | //go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le || loong64)) || sqlite3_flock || sqlite3_dotlk 2 | 3 | package vfs 4 | 5 | import "github.com/ncruces/go-sqlite3/internal/util" 6 | 7 | // +checklocks:s.Mutex 8 | func (s *vfsShm) shmMemLock(offset, n int32, flags _ShmFlag) error { 9 | switch { 10 | case flags&_SHM_UNLOCK != 0: 11 | for i := offset; i < offset+n; i++ { 12 | if s.lock[i] { 13 | if s.vfsShmParent.lock[i] <= 0 { 14 | s.vfsShmParent.lock[i] = 0 15 | } else { 16 | s.vfsShmParent.lock[i]-- 17 | } 18 | s.lock[i] = false 19 | } 20 | } 21 | case flags&_SHM_SHARED != 0: 22 | for i := offset; i < offset+n; i++ { 23 | if !s.lock[i] && 24 | s.vfsShmParent.lock[i]+1 <= 0 { 25 | return _BUSY 26 | } 27 | } 28 | for i := offset; i < offset+n; i++ { 29 | if !s.lock[i] { 30 | s.vfsShmParent.lock[i]++ 31 | s.lock[i] = true 32 | } 33 | } 34 | case flags&_SHM_EXCLUSIVE != 0: 35 | for i := offset; i < offset+n; i++ { 36 | if s.lock[i] { 37 | // SQLite never requests an exclusive lock that it already holds. 38 | panic(util.AssertErr()) 39 | } 40 | if s.vfsShmParent.lock[i] != 0 { 41 | return _BUSY 42 | } 43 | } 44 | for i := offset; i < offset+n; i++ { 45 | s.vfsShmParent.lock[i] = -1 46 | s.lock[i] = true 47 | } 48 | default: 49 | panic(util.AssertErr()) 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /vfs/adiantum/example_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk 2 | 3 | package adiantum_test 4 | 5 | import ( 6 | "crypto/rand" 7 | "log" 8 | "os" 9 | 10 | "golang.org/x/crypto/argon2" 11 | 12 | "lukechampine.com/adiantum/hbsh" 13 | "lukechampine.com/adiantum/hpolyc" 14 | 15 | "github.com/ncruces/go-sqlite3" 16 | "github.com/ncruces/go-sqlite3/vfs" 17 | "github.com/ncruces/go-sqlite3/vfs/adiantum" 18 | ) 19 | 20 | func ExampleRegister_hpolyc() { 21 | vfs.Register("hpolyc", adiantum.Wrap(vfs.Find(""), hpolycCreator{})) 22 | 23 | db, err := sqlite3.Open("file:demo.db?vfs=hpolyc" + 24 | "&textkey=correct+horse+battery+staple") 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | defer os.Remove("./demo.db") 29 | defer db.Close() 30 | // Output: 31 | } 32 | 33 | type hpolycCreator struct{} 34 | 35 | // HBSH creates an HBSH cipher given a key. 36 | func (hpolycCreator) HBSH(key []byte) *hbsh.HBSH { 37 | if len(key) != 32 { 38 | // Key is not appropriate, return nil. 39 | return nil 40 | } 41 | return hpolyc.New(key) 42 | } 43 | 44 | // KDF gets a key from a secret. 45 | func (hpolycCreator) KDF(secret string) []byte { 46 | if secret == "" { 47 | // No secret is given, generate a random key. 48 | key := make([]byte, 32) 49 | rand.Read(key) 50 | return key 51 | } 52 | // Hash the secret with a KDF. 53 | return argon2.IDKey([]byte(secret), []byte("hpolyc"), 3, 64*1024, 4, 32) 54 | } 55 | -------------------------------------------------------------------------------- /vfs/tests/mptest/testdata/crash02.subtest: -------------------------------------------------------------------------------- 1 | /* 2 | ** This script is called from crash01.test and config02.test and perhaps other 3 | ** script. After the database file has been set up, make a big rollback 4 | ** journal in client 1, then crash client 1. 5 | ** Then in the other clients, do an integrity check. 6 | */ 7 | --task 1 leave-hot-journal 8 | --sleep 5 9 | --finish 10 | PRAGMA cache_size=10; 11 | BEGIN; 12 | UPDATE t1 SET b=randomblob(20000); 13 | UPDATE t2 SET b=randomblob(20000); 14 | UPDATE t3 SET b=randomblob(20000); 15 | UPDATE t4 SET b=randomblob(20000); 16 | UPDATE t5 SET b=randomblob(20000); 17 | UPDATE t1 SET b=NULL; 18 | UPDATE t2 SET b=NULL; 19 | UPDATE t3 SET b=NULL; 20 | UPDATE t4 SET b=NULL; 21 | UPDATE t5 SET b=NULL; 22 | --print Task one crashing an incomplete transaction 23 | --exit 1 24 | --end 25 | --task 2 integrity_check-2 26 | SELECT count(*) FROM t1; 27 | --match 64 28 | --sleep 100 29 | PRAGMA integrity_check(10); 30 | --match ok 31 | --end 32 | --task 3 integrity_check-3 33 | SELECT count(*) FROM t1; 34 | --match 64 35 | --sleep 100 36 | PRAGMA integrity_check(10); 37 | --match ok 38 | --end 39 | --task 4 integrity_check-4 40 | SELECT count(*) FROM t1; 41 | --match 64 42 | --sleep 100 43 | PRAGMA integrity_check(10); 44 | --match ok 45 | --end 46 | --task 5 integrity_check-5 47 | SELECT count(*) FROM t1; 48 | --match 64 49 | --sleep 100 50 | PRAGMA integrity_check(10); 51 | --match ok 52 | --end 53 | --wait all 54 | -------------------------------------------------------------------------------- /internal/util/handle.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | type handleState struct { 9 | handles []any 10 | holes int 11 | } 12 | 13 | func (s *handleState) CloseNotify(ctx context.Context, exitCode uint32) { 14 | for _, h := range s.handles { 15 | if c, ok := h.(io.Closer); ok { 16 | c.Close() 17 | } 18 | } 19 | s.handles = nil 20 | s.holes = 0 21 | } 22 | 23 | func GetHandle(ctx context.Context, id Ptr_t) any { 24 | if id == 0 { 25 | return nil 26 | } 27 | s := ctx.Value(moduleKey{}).(*moduleState) 28 | return s.handles[^id] 29 | } 30 | 31 | func DelHandle(ctx context.Context, id Ptr_t) error { 32 | if id == 0 { 33 | return nil 34 | } 35 | s := ctx.Value(moduleKey{}).(*moduleState) 36 | a := s.handles[^id] 37 | s.handles[^id] = nil 38 | if l := Ptr_t(len(s.handles)); l == ^id { 39 | s.handles = s.handles[:l-1] 40 | } else { 41 | s.holes++ 42 | } 43 | if c, ok := a.(io.Closer); ok { 44 | return c.Close() 45 | } 46 | return nil 47 | } 48 | 49 | func AddHandle(ctx context.Context, a any) Ptr_t { 50 | if a == nil { 51 | panic(NilErr) 52 | } 53 | 54 | s := ctx.Value(moduleKey{}).(*moduleState) 55 | 56 | // Find an empty slot. 57 | if s.holes > cap(s.handles)-len(s.handles) { 58 | for id, h := range s.handles { 59 | if h == nil { 60 | s.holes-- 61 | s.handles[id] = a 62 | return ^Ptr_t(id) 63 | } 64 | } 65 | } 66 | 67 | // Add a new slot. 68 | s.handles = append(s.handles, a) 69 | return -Ptr_t(len(s.handles)) 70 | } 71 | -------------------------------------------------------------------------------- /ext/stats/TODO.md: -------------------------------------------------------------------------------- 1 | # ANSI SQL Aggregate Functions 2 | 3 | https://oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html 4 | 5 | ## Built in aggregates 6 | 7 | - [x] `COUNT(*)` 8 | - [x] `COUNT(expression)` 9 | - [x] `SUM(expression)` 10 | - [x] `AVG(expression)` 11 | - [x] `MIN(expression)` 12 | - [x] `MAX(expression)` 13 | 14 | https://sqlite.org/lang_aggfunc.html 15 | 16 | ## Statistical aggregates 17 | 18 | - [x] `STDDEV_POP(expression)` 19 | - [x] `STDDEV_SAMP(expression)` 20 | - [x] `VAR_POP(expression)` 21 | - [x] `VAR_SAMP(expression)` 22 | - [x] `COVAR_POP(dependent, independent)` 23 | - [x] `COVAR_SAMP(dependent, independent)` 24 | - [x] `CORR(dependent, independent)` 25 | 26 | ## Linear regression aggregates 27 | 28 | - [X] `REGR_AVGX(dependent, independent)` 29 | - [X] `REGR_AVGY(dependent, independent)` 30 | - [X] `REGR_SXX(dependent, independent)` 31 | - [X] `REGR_SYY(dependent, independent)` 32 | - [X] `REGR_SXY(dependent, independent)` 33 | - [X] `REGR_COUNT(dependent, independent)` 34 | - [X] `REGR_SLOPE(dependent, independent)` 35 | - [X] `REGR_INTERCEPT(dependent, independent)` 36 | - [X] `REGR_R2(dependent, independent)` 37 | 38 | ## Set aggregates 39 | 40 | - [X] `CUME_DIST() OVER window` 41 | - [X] `RANK() OVER window` 42 | - [X] `DENSE_RANK() OVER window` 43 | - [X] `PERCENT_RANK() OVER window` 44 | 45 | https://sqlite.org/windowfunctions.html#builtins 46 | 47 | ## Boolean aggregates 48 | 49 | - [X] `EVERY(boolean)` 50 | - [X] `SOME(boolean)` 51 | 52 | ## Additional aggregates 53 | 54 | - [X] `MEDIAN(expression)` 55 | - [X] `PERCENTILE_CONT(expression, fraction)` 56 | - [X] `PERCENTILE_DISC(expression, fraction)` -------------------------------------------------------------------------------- /sqlite3/libc/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | ROOT=../../ 7 | BINARYEN="$ROOT/tools/binaryen/bin" 8 | WASI_SDK="$ROOT/tools/wasi-sdk/bin" 9 | SRCS="${1:-libc.c}" 10 | "../tools.sh" 11 | 12 | trap 'rm -f libc.c libc.tmp' EXIT 13 | cat << EOF > libc.c 14 | #include 15 | #include 16 | EOF 17 | 18 | "$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -g0 -O2 \ 19 | -Wall -Wextra -Wno-unused-parameter -Wno-unused-function \ 20 | -o libc.wasm -I. "$SRCS" \ 21 | -mexec-model=reactor \ 22 | -mmutable-globals -mnontrapping-fptoint \ 23 | -msimd128 -mbulk-memory -msign-ext \ 24 | -mreference-types -mmultivalue \ 25 | -mno-extended-const \ 26 | -fno-stack-protector \ 27 | -Wl,-z,stack-size=4096 \ 28 | -Wl,--stack-first \ 29 | -Wl,--import-undefined \ 30 | -Wl,--initial-memory=16777216 \ 31 | -Wl,--export=memchr \ 32 | -Wl,--export=memcmp \ 33 | -Wl,--export=memcpy \ 34 | -Wl,--export=memmove \ 35 | -Wl,--export=memrchr \ 36 | -Wl,--export=memset \ 37 | -Wl,--export=strchr \ 38 | -Wl,--export=strchrnul \ 39 | -Wl,--export=strcspn \ 40 | -Wl,--export=strlen \ 41 | -Wl,--export=strrchr \ 42 | -Wl,--export=strspn \ 43 | -Wl,--export=qsort 44 | 45 | "$BINARYEN/wasm-ctor-eval" -g -c _initialize libc.wasm -o libc.tmp 46 | "$BINARYEN/wasm-opt" -g libc.tmp -o libc.wasm \ 47 | --low-memory-unused --generate-global-effects --converge -O3 \ 48 | --enable-mutable-globals --enable-nontrapping-float-to-int \ 49 | --enable-simd --enable-bulk-memory --enable-sign-ext \ 50 | --enable-reference-types --enable-multivalue \ 51 | --strip --strip-debug --strip-producers 52 | 53 | "$BINARYEN/wasm-dis" -o libc.wat libc.wasm -------------------------------------------------------------------------------- /util/ioutil/size_test.go: -------------------------------------------------------------------------------- 1 | package ioutil 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestNewSizeReaderAt(t *testing.T) { 12 | f, err := os.Create(filepath.Join(t.TempDir(), "abc.txt")) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | defer f.Close() 17 | 18 | n, err := NewSizeReaderAt(f).Size() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | if n != 0 { 23 | t.Errorf("got %d", n) 24 | } 25 | 26 | reader := strings.NewReader("abc") 27 | 28 | n, err = NewSizeReaderAt(reader).Size() 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | if n != 3 { 33 | t.Errorf("got %d", n) 34 | } 35 | 36 | n, err = NewSizeReaderAt(readlener{reader, reader.Len()}).Size() 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | if n != 3 { 41 | t.Errorf("got %d", n) 42 | } 43 | 44 | n, err = NewSizeReaderAt(readsizer{reader, reader.Size()}).Size() 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | if n != 3 { 49 | t.Errorf("got %d", n) 50 | } 51 | 52 | n, err = NewSizeReaderAt(readseeker{reader, reader}).Size() 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if n != 3 { 57 | t.Errorf("got %d", n) 58 | } 59 | 60 | _, err = NewSizeReaderAt(readerat{reader}).Size() 61 | if err == nil { 62 | t.Error("want error") 63 | } 64 | } 65 | 66 | type readlener struct { 67 | io.ReaderAt 68 | len int 69 | } 70 | 71 | func (l readlener) Len() int { return l.len } 72 | 73 | type readsizer struct { 74 | io.ReaderAt 75 | size int64 76 | } 77 | 78 | func (l readsizer) Size() (int64, error) { return l.size, nil } 79 | 80 | type readseeker struct { 81 | io.ReaderAt 82 | io.Seeker 83 | } 84 | 85 | type readerat struct { 86 | io.ReaderAt 87 | } 88 | -------------------------------------------------------------------------------- /tests/ext_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | _ "github.com/ncruces/go-sqlite3/embed" 8 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 9 | ) 10 | 11 | func Test_base64(t *testing.T) { 12 | t.Parallel() 13 | 14 | db, err := sqlite3.Open(":memory:") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | defer db.Close() 19 | 20 | // base64 21 | stmt, _, err := db.Prepare(`SELECT base64('TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu')`) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | defer stmt.Close() 26 | 27 | if !stmt.Step() { 28 | t.Fatal("expected one row") 29 | } 30 | if got := stmt.ColumnText(0); got != "Many hands make light work." { 31 | t.Errorf("got %q", got) 32 | } 33 | } 34 | 35 | func Test_decimal(t *testing.T) { 36 | t.Parallel() 37 | 38 | db, err := sqlite3.Open(":memory:") 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | defer db.Close() 43 | 44 | stmt, _, err := db.Prepare(`SELECT decimal_add(decimal('0.1'), decimal('0.2')) = decimal('0.3')`) 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | defer stmt.Close() 49 | 50 | if !stmt.Step() { 51 | t.Fatal("expected one row") 52 | } 53 | if !stmt.ColumnBool(0) { 54 | t.Error("want true") 55 | } 56 | } 57 | 58 | func Test_uint(t *testing.T) { 59 | t.Parallel() 60 | 61 | db, err := sqlite3.Open(":memory:") 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | defer db.Close() 66 | 67 | stmt, _, err := db.Prepare(`SELECT 'z2' < 'z11' COLLATE UINT`) 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | defer stmt.Close() 72 | 73 | if !stmt.Step() { 74 | t.Fatal("expected one row") 75 | } 76 | if !stmt.ColumnBool(0) { 77 | t.Error("want true") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/endian_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "log" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/ncruces/go-sqlite3" 11 | ) 12 | 13 | func Test_endianness(t *testing.T) { 14 | big := binary.BigEndian.AppendUint64(nil, 0x1234567890ABCDEF) 15 | little := binary.LittleEndian.AppendUint64(nil, 0x1234567890ABCDEF) 16 | native := binary.NativeEndian.AppendUint64(nil, 0x1234567890ABCDEF) 17 | switch { 18 | case bytes.Equal(big, native): 19 | t.Log("Platform is big endian") 20 | case bytes.Equal(little, native): 21 | t.Log("Platform is little endian") 22 | default: 23 | t.Fatal("Platform is middle endian") 24 | } 25 | 26 | db, err := sqlite3.Open(":memory:") 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | defer db.Close() 31 | 32 | err = db.Exec(`CREATE TABLE test (col)`) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | const value int64 = -9223372036854775808 38 | { 39 | stmt, _, err := db.Prepare(`INSERT INTO test VALUES (?)`) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | defer stmt.Close() 44 | 45 | err = stmt.BindInt64(1, value) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | err = stmt.Exec() 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | } 55 | { 56 | stmt, _, err := db.Prepare(`SELECT * FROM test`) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | defer stmt.Close() 61 | 62 | if !stmt.Step() { 63 | t.Fatal(stmt.Err()) 64 | } else { 65 | if got := stmt.ColumnInt64(0); got != value { 66 | t.Errorf("got %d, want %d", got, value) 67 | } 68 | if got := stmt.ColumnText(0); got != strconv.FormatInt(value, 10) { 69 | t.Errorf("got %s, want %d", got, value) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /sqlite3/download.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | cd -P -- "$(dirname -- "$0")" 5 | 6 | curl -#OL "https://sqlite.org/2025/sqlite-autoconf-3510100.tar.gz" 7 | 8 | # Verify download. 9 | if hash=$(openssl dgst -sha3-256 sqlite-autoconf-*.tar.gz); then 10 | if ! [[ $hash =~ 9b2b1e73f577def1d5b75c5541555a7f42e6e073ad19f7a9118478389c9bbd9b ]]; then 11 | echo $hash 12 | exit 1 13 | fi 14 | fi 2> /dev/null 15 | 16 | tar xzf sqlite-autoconf-*.tar.gz 17 | 18 | # To test a snapshot instead: 19 | # curl -# https://sqlite.org/snapshot/sqlite-snapshot-202410081727.tar.gz | tar xz 20 | 21 | mv sqlite-*/sqlite3.c . 22 | mv sqlite-*/sqlite3.h . 23 | mv sqlite-*/sqlite3ext.h . 24 | rm -r sqlite-* 25 | 26 | GITHUB_TAG="https://github.com/sqlite/sqlite/raw/version-3.51.1" 27 | 28 | mkdir -p ext/ 29 | cd ext/ 30 | curl -#OL "$GITHUB_TAG/ext/misc/anycollseq.c" 31 | curl -#OL "$GITHUB_TAG/ext/misc/base64.c" 32 | curl -#OL "$GITHUB_TAG/ext/misc/decimal.c" 33 | curl -#OL "$GITHUB_TAG/ext/misc/ieee754.c" 34 | curl -#OL "$GITHUB_TAG/ext/misc/regexp.c" 35 | curl -#OL "$GITHUB_TAG/ext/misc/series.c" 36 | curl -#OL "$GITHUB_TAG/ext/misc/spellfix.c" 37 | curl -#OL "$GITHUB_TAG/ext/misc/uint.c" 38 | cd ~- 39 | 40 | cd ../vfs/tests/mptest/testdata/ 41 | curl -#OL "$GITHUB_TAG/mptest/config01.test" 42 | curl -#OL "$GITHUB_TAG/mptest/config02.test" 43 | curl -#OL "$GITHUB_TAG/mptest/crash01.test" 44 | curl -#OL "$GITHUB_TAG/mptest/crash02.subtest" 45 | curl -#OL "$GITHUB_TAG/mptest/multiwrite01.test" 46 | cd ~- 47 | 48 | cd ../vfs/tests/mptest/wasm/ 49 | curl -#OL "$GITHUB_TAG/mptest/mptest.c" 50 | cd ~- 51 | 52 | cd ../vfs/tests/speedtest1/wasm/ 53 | curl -#OL "$GITHUB_TAG/test/speedtest1.c" 54 | cd ~- 55 | 56 | cat *.patch | patch -p0 --no-backup-if-mismatch 57 | -------------------------------------------------------------------------------- /tests/cksm_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | _ "embed" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/ncruces/go-sqlite3" 9 | "github.com/ncruces/go-sqlite3/driver" 10 | _ "github.com/ncruces/go-sqlite3/embed" 11 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 12 | "github.com/ncruces/go-sqlite3/util/ioutil" 13 | "github.com/ncruces/go-sqlite3/vfs/memdb" 14 | "github.com/ncruces/go-sqlite3/vfs/readervfs" 15 | ) 16 | 17 | //go:embed testdata/cksm.db 18 | var cksmDB string 19 | 20 | func Test_fileformat(t *testing.T) { 21 | t.Parallel() 22 | 23 | readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(cksmDB))) 24 | 25 | db, err := driver.Open("file:test.db?vfs=reader") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | defer db.Close() 30 | 31 | var enabled bool 32 | err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | if !enabled { 37 | t.Error("want true") 38 | } 39 | 40 | db.SetMaxIdleConns(0) // Clears the page cache. 41 | 42 | _, err = db.Exec(`PRAGMA integrity_check`) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | } 47 | 48 | func Test_enable(t *testing.T) { 49 | t.Parallel() 50 | 51 | db, err := driver.Open(memdb.TestDB(t), 52 | func(db *sqlite3.Conn) error { 53 | return db.EnableChecksums("main") 54 | }) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | defer db.Close() 59 | 60 | var enabled bool 61 | err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | if !enabled { 66 | t.Error("want true") 67 | } 68 | 69 | db.SetMaxIdleConns(0) // Clears the page cache. 70 | 71 | _, err = db.Exec(`PRAGMA integrity_check`) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /litestream/cache.go: -------------------------------------------------------------------------------- 1 | package litestream 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/benbjohnson/litestream" 9 | "github.com/superfly/ltx" 10 | ) 11 | 12 | type pageCache struct { 13 | pages map[uint32]cachedPage // +checklocks:mtx 14 | size int 15 | mtx sync.Mutex 16 | } 17 | 18 | type cachedPage struct { 19 | data []byte 20 | txid ltx.TXID 21 | } 22 | 23 | func (c *pageCache) getOrFetch(ctx context.Context, client ReplicaClient, pgno uint32, elem ltx.PageIndexElem) ([]byte, error) { 24 | if c.size > 0 { 25 | c.mtx.Lock() 26 | page := c.pages[pgno] 27 | c.mtx.Unlock() 28 | 29 | if page.txid == elem.MaxTXID { 30 | return page.data, nil 31 | } 32 | } 33 | 34 | h, data, err := litestream.FetchPage(ctx, client, elem.Level, elem.MinTXID, elem.MaxTXID, elem.Offset, elem.Size) 35 | if err != nil { 36 | return nil, fmt.Errorf("fetch page: %w", err) 37 | } 38 | if pgno != h.Pgno { 39 | return nil, fmt.Errorf("fetch page: want %d, got %d", pgno, h.Pgno) 40 | } 41 | 42 | if c.size > 0 { 43 | c.mtx.Lock() 44 | if c.pages != nil { 45 | c.evict(len(data)) 46 | } else { 47 | c.pages = map[uint32]cachedPage{} 48 | } 49 | c.pages[pgno] = cachedPage{data, elem.MaxTXID} 50 | c.mtx.Unlock() 51 | } 52 | return data, nil 53 | } 54 | 55 | // +checklocks:c.mtx 56 | func (c *pageCache) evict(pageSize int) { 57 | // Evict random keys until we're under the maximum size. 58 | // SQLite has its own page cache, which it will use for each connection. 59 | // Since this is a second layer of shared cache, 60 | // random eviction is probably good enough. 61 | if pageSize*len(c.pages) < c.size { 62 | return 63 | } 64 | for key := range c.pages { 65 | delete(c.pages, key) 66 | if pageSize*len(c.pages) < c.size { 67 | return 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /driver/savepoint_test.go: -------------------------------------------------------------------------------- 1 | package driver_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/ncruces/go-sqlite3/driver" 8 | _ "github.com/ncruces/go-sqlite3/embed" 9 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 10 | _ "github.com/ncruces/go-sqlite3/vfs/memdb" 11 | ) 12 | 13 | func ExampleSavepoint() { 14 | db, err := driver.Open("file:/svpt.db?vfs=memdb") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | defer db.Close() 19 | 20 | _, err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | err = func() error { 26 | tx, err := db.Begin() 27 | if err != nil { 28 | return err 29 | } 30 | defer tx.Rollback() 31 | 32 | stmt, err := tx.Prepare(`INSERT INTO users (id, name) VALUES (?, ?)`) 33 | if err != nil { 34 | return err 35 | } 36 | defer stmt.Close() 37 | 38 | _, err = stmt.Exec(0, "go") 39 | if err != nil { 40 | return err 41 | } 42 | 43 | _, err = stmt.Exec(1, "zig") 44 | if err != nil { 45 | return err 46 | } 47 | 48 | savept := driver.Savepoint(tx) 49 | 50 | _, err = stmt.Exec(2, "whatever") 51 | if err != nil { 52 | return err 53 | } 54 | 55 | err = savept.Rollback() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | _, err = stmt.Exec(3, "rust") 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return tx.Commit() 66 | }() 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | rows, err := db.Query(`SELECT id, name FROM users`) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | defer rows.Close() 76 | 77 | for rows.Next() { 78 | var id, name string 79 | err = rows.Scan(&id, &name) 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | fmt.Printf("%s %s\n", id, name) 84 | } 85 | // Output: 86 | // 0 go 87 | // 1 zig 88 | // 3 rust 89 | } 90 | -------------------------------------------------------------------------------- /internal/util/math_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func Test_abs(t *testing.T) { 9 | tests := []struct { 10 | arg int 11 | want int 12 | }{ 13 | {0, 0}, 14 | {1, 1}, 15 | {-1, 1}, 16 | {math.MaxInt, math.MaxInt}, 17 | {math.MinInt, math.MinInt}, 18 | } 19 | for _, tt := range tests { 20 | t.Run("", func(t *testing.T) { 21 | if got := abs(tt.arg); got != tt.want { 22 | t.Errorf("abs(%d) = %d, want %d", tt.arg, got, tt.want) 23 | } 24 | }) 25 | } 26 | } 27 | 28 | func Test_GCD(t *testing.T) { 29 | tests := []struct { 30 | arg1 int 31 | arg2 int 32 | want int 33 | }{ 34 | {0, 0, 0}, 35 | {0, 1, 1}, 36 | {1, 0, 1}, 37 | {1, 1, 1}, 38 | {2, 3, 1}, 39 | {42, 56, 14}, 40 | {48, -18, 6}, 41 | {1e9, 1e9, 1e9}, 42 | {1e9, -1e9, 1e9}, 43 | {-1e9, -1e9, 1e9}, 44 | {math.MaxInt, math.MaxInt, math.MaxInt}, 45 | {math.MinInt, math.MinInt, math.MinInt}, 46 | } 47 | for _, tt := range tests { 48 | t.Run("", func(t *testing.T) { 49 | if got := GCD(tt.arg1, tt.arg2); got != tt.want { 50 | t.Errorf("gcd(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func Test_LCM(t *testing.T) { 57 | tests := []struct { 58 | arg1 int 59 | arg2 int 60 | want int 61 | }{ 62 | {0, 0, 0}, 63 | {0, 1, 0}, 64 | {1, 0, 0}, 65 | {1, 1, 1}, 66 | {2, 3, 6}, 67 | {42, 56, 168}, 68 | {48, -18, 144}, 69 | {1e9, 1e9, 1e9}, 70 | {1e9, -1e9, 1e9}, 71 | {-1e9, -1e9, 1e9}, 72 | {math.MaxInt, math.MaxInt, math.MaxInt}, 73 | {math.MinInt, math.MinInt, math.MinInt}, 74 | } 75 | for _, tt := range tests { 76 | t.Run("", func(t *testing.T) { 77 | if got := LCM(tt.arg1, tt.arg2); got != tt.want { 78 | t.Errorf("lcm(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /vfs/os_ofd.go: -------------------------------------------------------------------------------- 1 | //go:build (linux || darwin) && !(sqlite3_flock || sqlite3_dotlk) 2 | 3 | package vfs 4 | 5 | import ( 6 | "os" 7 | "time" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func osGetSharedLock(file *os.File) error { 13 | // Test the PENDING lock before acquiring a new SHARED lock. 14 | if lock, _ := osTestLock(file, _PENDING_BYTE, 1, _IOERR); lock == unix.F_WRLCK { 15 | return _BUSY 16 | } 17 | // Acquire the SHARED lock. 18 | return osReadLock(file, _SHARED_FIRST, _SHARED_SIZE, 0) 19 | } 20 | 21 | func osGetReservedLock(file *os.File) error { 22 | // Acquire the RESERVED lock. 23 | return osWriteLock(file, _RESERVED_BYTE, 1, 0) 24 | } 25 | 26 | func osGetExclusiveLock(file *os.File, state *LockLevel) error { 27 | if *state == LOCK_RESERVED { 28 | // A PENDING lock is needed before acquiring an EXCLUSIVE lock. 29 | if err := osWriteLock(file, _PENDING_BYTE, 1, -1); err != nil { 30 | return err 31 | } 32 | *state = LOCK_PENDING 33 | } 34 | // Acquire the EXCLUSIVE lock. 35 | return osWriteLock(file, _SHARED_FIRST, _SHARED_SIZE, time.Millisecond) 36 | } 37 | 38 | func osDowngradeLock(file *os.File, state LockLevel) error { 39 | if state >= LOCK_EXCLUSIVE { 40 | // Downgrade to a SHARED lock. 41 | if err := osReadLock(file, _SHARED_FIRST, _SHARED_SIZE, 0); err != nil { 42 | // notest // this should never happen 43 | return _IOERR_RDLOCK 44 | } 45 | } 46 | // Release the PENDING and RESERVED locks. 47 | return osUnlock(file, _PENDING_BYTE, 2) 48 | } 49 | 50 | func osReleaseLock(file *os.File, _ LockLevel) error { 51 | // Release all locks. 52 | return osUnlock(file, 0, 0) 53 | } 54 | 55 | func osCheckReservedLock(file *os.File) (bool, error) { 56 | // Test the RESERVED lock. 57 | lock, err := osTestLock(file, _RESERVED_BYTE, 1, _IOERR_CHECKRESERVEDLOCK) 58 | return lock == unix.F_WRLCK, err 59 | } 60 | -------------------------------------------------------------------------------- /ext/stats/boolean_test.go: -------------------------------------------------------------------------------- 1 | package stats_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3" 7 | _ "github.com/ncruces/go-sqlite3/embed" 8 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 9 | ) 10 | 11 | func TestRegister_boolean(t *testing.T) { 12 | t.Parallel() 13 | 14 | db, err := sqlite3.Open(":memory:") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | defer db.Close() 19 | 20 | err = db.Exec(`CREATE TABLE data (x)`) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | err = db.Exec(`INSERT INTO data (x) VALUES (4), (7.0), (13), (NULL), (16), (3.14)`) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | stmt, _, err := db.Prepare(` 31 | SELECT 32 | every(x > 0), 33 | every(x > 10), 34 | some(x > 10), 35 | some(x > 20) 36 | FROM data`) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | if !stmt.Step() { 41 | t.Fatal(stmt.Err()) 42 | } else { 43 | if got := stmt.ColumnBool(0); got != true { 44 | t.Errorf("got %v, want true", got) 45 | } 46 | if got := stmt.ColumnBool(1); got != false { 47 | t.Errorf("got %v, want false", got) 48 | } 49 | if got := stmt.ColumnBool(2); got != true { 50 | t.Errorf("got %v, want true", got) 51 | } 52 | if got := stmt.ColumnBool(3); got != false { 53 | t.Errorf("got %v, want false", got) 54 | } 55 | } 56 | stmt.Close() 57 | 58 | stmt, _, err = db.Prepare(`SELECT every(x > 10) OVER (ROWS 1 PRECEDING) FROM data`) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | want := [...]bool{false, false, false, true, true, false} 64 | for i := 0; stmt.Step(); i++ { 65 | if got := stmt.ColumnBool(0); got != want[i] { 66 | t.Errorf("got %v, want %v", got, want[i]) 67 | } 68 | if got := stmt.ColumnType(0); got != sqlite3.INTEGER { 69 | t.Errorf("got %v, want INTEGER", got) 70 | } 71 | } 72 | stmt.Close() 73 | } 74 | -------------------------------------------------------------------------------- /litestream/time.go: -------------------------------------------------------------------------------- 1 | package litestream 2 | 3 | import ( 4 | "math" 5 | "strings" 6 | "time" 7 | 8 | "github.com/ncruces/go-sqlite3/util/sql3util" 9 | ) 10 | 11 | func parseTimeDelta(s string) (years, months, days int, duration time.Duration, ok bool) { 12 | duration, err := time.ParseDuration(s) 13 | if err == nil { 14 | return 0, 0, 0, duration, true 15 | } 16 | 17 | if strings.EqualFold(s, "now") { 18 | return 0, 0, 0, 0, true 19 | } 20 | 21 | ss := strings.TrimSuffix(strings.ToLower(s), "s") 22 | switch { 23 | case strings.HasSuffix(ss, " year"): 24 | years, duration, ok = parseDateUnit(ss, " year", 365*86400) 25 | 26 | case strings.HasSuffix(ss, " month"): 27 | months, duration, ok = parseDateUnit(ss, " month", 30*86400) 28 | 29 | case strings.HasSuffix(ss, " day"): 30 | months, duration, ok = parseDateUnit(ss, " day", 86400) 31 | 32 | case strings.HasSuffix(ss, " hour"): 33 | duration, ok = parseTimeUnit(ss, " hour", time.Hour) 34 | 35 | case strings.HasSuffix(ss, " minute"): 36 | duration, ok = parseTimeUnit(ss, " minute", time.Minute) 37 | 38 | case strings.HasSuffix(ss, " second"): 39 | duration, ok = parseTimeUnit(ss, " second", time.Second) 40 | 41 | default: 42 | return sql3util.ParseTimeShift(s) 43 | } 44 | return 45 | } 46 | 47 | func parseDateUnit(s, unit string, seconds float64) (int, time.Duration, bool) { 48 | f, ok := sql3util.ParseFloat(s[:len(s)-len(unit)]) 49 | if !ok { 50 | return 0, 0, false 51 | } 52 | 53 | i, f := math.Modf(f) 54 | if math.MinInt <= i && i <= math.MaxInt { 55 | return int(i), time.Duration(f * seconds * float64(time.Second)), true 56 | } 57 | return 0, 0, false 58 | } 59 | 60 | func parseTimeUnit(s, unit string, scale time.Duration) (time.Duration, bool) { 61 | f, ok := sql3util.ParseFloat(s[:len(s)-len(unit)]) 62 | return time.Duration(f * float64(scale)), ok 63 | } 64 | -------------------------------------------------------------------------------- /ext/fileio/fileio_test.go: -------------------------------------------------------------------------------- 1 | package fileio_test 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "io/fs" 7 | "os" 8 | "testing" 9 | 10 | "github.com/ncruces/go-sqlite3" 11 | "github.com/ncruces/go-sqlite3/driver" 12 | _ "github.com/ncruces/go-sqlite3/embed" 13 | "github.com/ncruces/go-sqlite3/ext/fileio" 14 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 15 | "github.com/ncruces/go-sqlite3/vfs/memdb" 16 | ) 17 | 18 | func Test_lsmode(t *testing.T) { 19 | t.Parallel() 20 | dsn := memdb.TestDB(t) 21 | 22 | db, err := driver.Open(dsn, fileio.Register) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | defer db.Close() 27 | 28 | d, err := os.Getwd() 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | s, err := os.Stat(d) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | var mode string 39 | err = db.QueryRow(`SELECT lsmode(?)`, s.Mode()).Scan(&mode) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | if len(mode) != 10 || mode[0] != 'd' { 45 | t.Errorf("got %s", mode) 46 | } else { 47 | t.Logf("got %s", mode) 48 | } 49 | } 50 | 51 | func Test_readfile(t *testing.T) { 52 | t.Parallel() 53 | 54 | for _, fsys := range []fs.FS{nil, os.DirFS(".")} { 55 | t.Run("", func(t *testing.T) { 56 | dsn := memdb.TestDB(t) 57 | 58 | db, err := driver.Open(dsn, func(c *sqlite3.Conn) error { 59 | fileio.RegisterFS(c, fsys) 60 | return nil 61 | }) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | defer db.Close() 66 | 67 | rows, err := db.Query(`SELECT readfile('fileio_test.go')`) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | if rows.Next() { 73 | var data sql.RawBytes 74 | rows.Scan(&data) 75 | 76 | if !bytes.HasPrefix(data, []byte("package fileio_test")) { 77 | t.Errorf("got %s", data[:min(64, len(data))]) 78 | } 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ext/zorder/zorder.go: -------------------------------------------------------------------------------- 1 | // Package zorder provides functions for z-order transformations. 2 | // 3 | // https://sqlite.org/src/doc/tip/ext/misc/zorder.c 4 | package zorder 5 | 6 | import ( 7 | "errors" 8 | 9 | "github.com/ncruces/go-sqlite3" 10 | "github.com/ncruces/go-sqlite3/internal/util" 11 | ) 12 | 13 | // Register registers the zorder and unzorder SQL functions. 14 | func Register(db *sqlite3.Conn) error { 15 | const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS 16 | return errors.Join( 17 | db.CreateFunction("zorder", -1, flags, zorder), 18 | db.CreateFunction("unzorder", 3, flags, unzorder)) 19 | } 20 | 21 | func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) { 22 | var x [24]int64 23 | if n := len(arg); n < 2 || n > 24 { 24 | ctx.ResultError(util.ErrorString("zorder: needs between 2 and 24 dimensions")) 25 | return 26 | } 27 | for i := range arg { 28 | x[i] = arg[i].Int64() 29 | } 30 | 31 | var z int64 32 | for i := range 63 { 33 | j := i % len(arg) 34 | z |= (x[j] & 1) << i 35 | x[j] >>= 1 36 | } 37 | 38 | for i := range arg { 39 | if x[i] != 0 { 40 | ctx.ResultError(util.ErrorString("zorder: argument out of range")) 41 | return 42 | } 43 | } 44 | ctx.ResultInt64(z) 45 | } 46 | 47 | func unzorder(ctx sqlite3.Context, arg ...sqlite3.Value) { 48 | i := arg[2].Int64() 49 | n := arg[1].Int64() 50 | z := arg[0].Int64() 51 | 52 | if n < 2 || n > 24 { 53 | ctx.ResultError(util.ErrorString("unzorder: needs between 2 and 24 dimensions")) 54 | return 55 | } 56 | if i < 0 || i >= n { 57 | ctx.ResultError(util.ErrorString("unzorder: index out of range")) 58 | return 59 | } 60 | if z < 0 { 61 | ctx.ResultError(util.ErrorString("unzorder: argument out of range")) 62 | return 63 | } 64 | 65 | var k int 66 | var x int64 67 | for j := i; j < 63; j += n { 68 | x |= ((z >> j) & 1) << k 69 | k++ 70 | } 71 | ctx.ResultInt64(x) 72 | } 73 | -------------------------------------------------------------------------------- /sqlite3/stmt.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "sqlite3.h" 4 | 5 | int sqlite3_exec_go(sqlite3_stmt *stmt) { 6 | while (sqlite3_step(stmt) == SQLITE_ROW); 7 | return sqlite3_reset(stmt); 8 | } 9 | 10 | union sqlite3_data { 11 | sqlite3_int64 i; 12 | double d; 13 | struct { 14 | const void *ptr; 15 | int len; 16 | }; 17 | }; 18 | 19 | int sqlite3_columns_go(sqlite3_stmt *stmt, int nCol, char *aType, 20 | union sqlite3_data *aData) { 21 | if (nCol != sqlite3_column_count(stmt)) { 22 | return SQLITE_MISUSE; 23 | } 24 | bool check = false; 25 | for (int i = 0; i < nCol; ++i) { 26 | const void *ptr = NULL; 27 | switch (aType[i] = sqlite3_column_type(stmt, i)) { 28 | default: // SQLITE_NULL 29 | aData[i] = (union sqlite3_data){}; 30 | continue; 31 | case SQLITE_INTEGER: 32 | aData[i].i = sqlite3_column_int64(stmt, i); 33 | continue; 34 | case SQLITE_FLOAT: 35 | aData[i].d = sqlite3_column_double(stmt, i); 36 | continue; 37 | case SQLITE_TEXT: 38 | ptr = sqlite3_column_text(stmt, i); 39 | break; 40 | case SQLITE_BLOB: 41 | ptr = sqlite3_column_blob(stmt, i); 42 | break; 43 | } 44 | aData[i].ptr = ptr; 45 | aData[i].len = sqlite3_column_bytes(stmt, i); 46 | if (ptr == NULL) check = true; 47 | } 48 | if (check && SQLITE_NOMEM == sqlite3_errcode(sqlite3_db_handle(stmt))) { 49 | return SQLITE_NOMEM; 50 | } 51 | return SQLITE_OK; 52 | } 53 | 54 | static_assert(offsetof(union sqlite3_data, i) == 0, "Unexpected offset"); 55 | static_assert(offsetof(union sqlite3_data, d) == 0, "Unexpected offset"); 56 | static_assert(offsetof(union sqlite3_data, ptr) == 0, "Unexpected offset"); 57 | static_assert(offsetof(union sqlite3_data, len) == 4, "Unexpected offset"); 58 | static_assert(sizeof(union sqlite3_data) == 8, "Unexpected size"); -------------------------------------------------------------------------------- /ext/stats/welford_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func Test_welford(t *testing.T) { 9 | t.Parallel() 10 | 11 | var s1, s2 welford 12 | s1.enqueue(1) 13 | s1.dequeue(1) 14 | 15 | s1.enqueue(4) 16 | s1.enqueue(7) 17 | s1.enqueue(13) 18 | s1.enqueue(16) 19 | if got := s1.mean(); got != 10 { 20 | t.Errorf("got %v, want 10", got) 21 | } 22 | if got := s1.var_samp(); got != 30 { 23 | t.Errorf("got %v, want 30", got) 24 | } 25 | if got := s1.var_pop(); got != 22.5 { 26 | t.Errorf("got %v, want 22.5", got) 27 | } 28 | if got := s1.stddev_samp(); got != math.Sqrt(30) { 29 | t.Errorf("got %v, want √30", got) 30 | } 31 | if got := s1.stddev_pop(); got != math.Sqrt(22.5) { 32 | t.Errorf("got %v, want √22.5", got) 33 | } 34 | 35 | s1.dequeue(4) 36 | s2.enqueue(7) 37 | s2.enqueue(13) 38 | s2.enqueue(16) 39 | if s1.var_pop() != s2.var_pop() { 40 | t.Errorf("got %v, want %v", s1, s2) 41 | } 42 | } 43 | 44 | func Test_covar(t *testing.T) { 45 | t.Parallel() 46 | 47 | var c1, c2 welford2 48 | c1.enqueue(1, 1) 49 | c1.dequeue(1, 1) 50 | 51 | c1.enqueue(3, 70) 52 | c1.enqueue(5, 80) 53 | c1.enqueue(2, 60) 54 | c1.enqueue(7, 90) 55 | c1.enqueue(4, 75) 56 | 57 | if got := c1.covar_samp(); got != 21.25 { 58 | t.Errorf("got %v, want 21.25", got) 59 | } 60 | if got := c1.covar_pop(); got != 17 { 61 | t.Errorf("got %v, want 17", got) 62 | } 63 | 64 | c1.dequeue(3, 70) 65 | c2.enqueue(5, 80) 66 | c2.enqueue(2, 60) 67 | c2.enqueue(7, 90) 68 | c2.enqueue(4, 75) 69 | if c1.covar_pop() != c2.covar_pop() { 70 | t.Errorf("got %v, want %v", c1.covar_pop(), c2.covar_pop()) 71 | } 72 | } 73 | 74 | func Test_correlation(t *testing.T) { 75 | t.Parallel() 76 | 77 | var c welford2 78 | c.enqueue(1, 3) 79 | c.enqueue(2, 2) 80 | c.enqueue(3, 1) 81 | 82 | if got := c.correlation(); got != -1 { 83 | t.Errorf("got %v, want -1", got) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /sqlite3/libc/stdlib.h: -------------------------------------------------------------------------------- 1 | #include_next // the system stdlib.h 2 | 3 | #ifndef _WASM_SIMD128_STDLIB_H 4 | #define _WASM_SIMD128_STDLIB_H 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | // Shellsort with Gonnet & Baeza-Yates gap sequence. 11 | // Simple, no recursion, doesn't use the C stack. 12 | // Clang auto-vectorizes the inner loop. 13 | 14 | __attribute__((weak)) 15 | void qsort(void *base, size_t nel, size_t width, 16 | int (*comp)(const void *, const void *)) { 17 | // If nel is zero, we're required to do nothing. 18 | // If it's one, the array is already sorted. 19 | size_t wnel = width * nel; 20 | size_t gap = nel; 21 | while (gap > 1) { 22 | // Use 64-bit unsigned arithmetic to avoid intermediate overflow. 23 | // Absent overflow, gap will be strictly less than its previous value. 24 | // Once it is one or zero, set it to one: do a final pass, and stop. 25 | gap = (5ull * gap - 1) / 11; 26 | if (gap == 0) gap = 1; 27 | 28 | // It'd be undefined behavior for wnel to overflow a size_t; 29 | // or if width is zero: the base pointer would be invalid. 30 | // Since gap is stricly less than nel, we can assume 31 | // wgap is strictly less than wnel. 32 | size_t wgap = width * gap; 33 | __builtin_assume(wgap < wnel); 34 | for (size_t i = wgap; i < wnel; i += width) { 35 | // Even without overflow flags, the overflow builtin helps the compiler. 36 | for (size_t j = i; !__builtin_sub_overflow(j, wgap, &j);) { 37 | char *a = j + (char *)base; 38 | char *b = a + wgap; 39 | if (comp(a, b) <= 0) break; 40 | 41 | // This well known loop is automatically vectorized. 42 | size_t s = width; 43 | do { 44 | char tmp = *a; 45 | *a++ = *b; 46 | *b++ = tmp; 47 | } while (--s); 48 | } 49 | } 50 | } 51 | } 52 | 53 | #ifdef __cplusplus 54 | } // extern "C" 55 | #endif 56 | 57 | #endif // _WASM_SIMD128_STDLIB_H -------------------------------------------------------------------------------- /driver/util_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "database/sql/driver" 5 | "slices" 6 | "testing" 7 | 8 | _ "github.com/ncruces/go-sqlite3/embed" 9 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 10 | ) 11 | 12 | func Test_namedValues(t *testing.T) { 13 | want := []driver.NamedValue{ 14 | {Ordinal: 1, Value: true}, 15 | {Ordinal: 2, Value: false}, 16 | } 17 | got := namedValues([]driver.Value{true, false}) 18 | if !slices.Equal(got, want) { 19 | t.Errorf("got %v, want %v", got, want) 20 | } 21 | } 22 | 23 | func Fuzz_notWhitespace(f *testing.F) { 24 | f.Add("") 25 | f.Add(" ") 26 | f.Add(";") 27 | f.Add("0") 28 | f.Add("-") 29 | f.Add("-0") 30 | f.Add("--") 31 | f.Add("--0") 32 | f.Add("--\n") 33 | f.Add("--0\n") 34 | f.Add("/0") 35 | f.Add("/*") 36 | f.Add("/*/") 37 | f.Add("/**") 38 | f.Add("/*0") 39 | f.Add("/**/") 40 | f.Add("/***/") 41 | f.Add("/**0/") 42 | f.Add("\v") 43 | f.Add(" \v") 44 | f.Add("\xf0") 45 | f.Add("\000") 46 | 47 | db, err := Open(":memory:") 48 | if err != nil { 49 | f.Fatal(err) 50 | } 51 | defer db.Close() 52 | 53 | f.Fuzz(func(t *testing.T, str string) { 54 | if len(str) > 128 { 55 | t.SkipNow() 56 | } 57 | 58 | c, err := db.Conn(t.Context()) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | defer c.Close() 63 | 64 | c.Raw(func(driverConn any) error { 65 | conn := driverConn.(*conn).Conn 66 | stmt, tail, err := conn.Prepare(str) 67 | stmt.Close() 68 | 69 | // It's hard to be bug for bug compatible with SQLite. 70 | // We settle for somewhat less: 71 | // - if SQLite reports whitespace, we must too 72 | // - if we report whitespace, SQLite must not parse a statement 73 | if notWhitespace(str) { 74 | if stmt == nil && tail == "" && err == nil { 75 | t.Errorf("was whitespace: %q", str) 76 | } 77 | } else { 78 | if stmt != nil { 79 | t.Errorf("was not whitespace: %q (%v)", str, err) 80 | } 81 | } 82 | return nil 83 | }) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /tests/json_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "encoding/json" 5 | "math" 6 | "testing" 7 | "time" 8 | 9 | "github.com/ncruces/go-sqlite3" 10 | "github.com/ncruces/go-sqlite3/driver" 11 | _ "github.com/ncruces/go-sqlite3/embed" 12 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 13 | "github.com/ncruces/go-sqlite3/vfs/memdb" 14 | "github.com/ncruces/julianday" 15 | ) 16 | 17 | func TestJSON(t *testing.T) { 18 | t.Parallel() 19 | dsn := memdb.TestDB(t) 20 | 21 | db, err := driver.Open(dsn) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | defer db.Close() 26 | 27 | conn, err := db.Conn(t.Context()) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | defer conn.Close() 32 | 33 | _, err = conn.ExecContext(t.Context(), `CREATE TABLE test (col)`) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | reference := time.Date(2013, 10, 7, 4, 23, 19, 120_000_000, time.FixedZone("", -4*3600)) 39 | 40 | _, err = conn.ExecContext(t.Context(), 41 | `INSERT INTO test (col) VALUES (?), (?), (?), (?)`, 42 | nil, 1, math.Pi, reference, 43 | ) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | _, err = conn.ExecContext(t.Context(), 49 | `INSERT INTO test (col) VALUES (?), (?), (?), (?)`, 50 | sqlite3.JSON(math.Pi), sqlite3.JSON(false), 51 | julianday.Format(reference), sqlite3.JSON([]string{})) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | rows, err := conn.QueryContext(t.Context(), "SELECT * FROM test") 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | defer rows.Close() 61 | 62 | want := []string{ 63 | "null", "1", "3.141592653589793", 64 | `"2013-10-07T04:23:19.12-04:00"`, 65 | "3.141592653589793", "false", 66 | "2456572.849526851851852", "[]", 67 | } 68 | for rows.Next() { 69 | var got json.RawMessage 70 | err = rows.Scan(sqlite3.JSON(&got)) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | if string(got) != want[0] { 75 | t.Errorf("got %q, want %q", got, want[0]) 76 | } 77 | want = want[1:] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /func_win_test.go: -------------------------------------------------------------------------------- 1 | package sqlite3_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "unicode" 7 | 8 | "github.com/ncruces/go-sqlite3" 9 | _ "github.com/ncruces/go-sqlite3/embed" 10 | ) 11 | 12 | func ExampleConn_CreateWindowFunction() { 13 | db, err := sqlite3.Open(":memory:") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | defer db.Close() 18 | 19 | err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | err = db.Exec(`INSERT INTO words (word) VALUES ('côte'), ('cote'), ('coter'), ('coté'), ('cotée'), ('côté')`) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | err = db.CreateWindowFunction("count_ascii", 1, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS, newASCIICounter) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | stmt, _, err := db.Prepare(`SELECT count_ascii(word) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM words`) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | defer stmt.Close() 39 | 40 | for stmt.Step() { 41 | fmt.Println(stmt.ColumnInt(0)) 42 | } 43 | if err := stmt.Err(); err != nil { 44 | log.Fatal(err) 45 | } 46 | // Output: 47 | // 1 48 | // 2 49 | // 2 50 | // 1 51 | // 0 52 | // 0 53 | } 54 | 55 | type countASCII struct{ result int } 56 | 57 | func newASCIICounter() sqlite3.AggregateFunction { 58 | return &countASCII{} 59 | } 60 | 61 | func (f *countASCII) Value(ctx sqlite3.Context) { 62 | ctx.ResultInt(f.result) 63 | } 64 | 65 | func (f *countASCII) Step(ctx sqlite3.Context, arg ...sqlite3.Value) { 66 | if f.isASCII(arg[0]) { 67 | f.result++ 68 | } 69 | } 70 | 71 | func (f *countASCII) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) { 72 | if f.isASCII(arg[0]) { 73 | f.result-- 74 | } 75 | } 76 | 77 | func (f *countASCII) isASCII(arg sqlite3.Value) bool { 78 | if arg.Type() != sqlite3.TEXT { 79 | return false 80 | } 81 | for _, c := range arg.RawText() { 82 | if c > unicode.MaxASCII { 83 | return false 84 | } 85 | } 86 | return true 87 | } 88 | -------------------------------------------------------------------------------- /ext/fileio/fileio.go: -------------------------------------------------------------------------------- 1 | // Package fileio provides SQL functions to read, write and list files. 2 | // 3 | // https://sqlite.org/src/doc/tip/ext/misc/fileio.c 4 | package fileio 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io/fs" 10 | "os" 11 | 12 | "github.com/ncruces/go-sqlite3" 13 | ) 14 | 15 | // Register registers SQL functions readfile, writefile, lsmode, 16 | // and the table-valued function fsdir. 17 | func Register(db *sqlite3.Conn) error { 18 | return RegisterFS(db, nil) 19 | } 20 | 21 | // RegisterFS registers SQL functions readfile, lsmode, 22 | // and the table-valued function fsdir; 23 | // fsys will be used to read files and list directories. 24 | func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error { 25 | var err error 26 | if fsys == nil { 27 | err = db.CreateFunction("writefile", -1, sqlite3.DIRECTONLY, writefile) 28 | } 29 | return errors.Join(err, 30 | db.CreateFunction("readfile", 1, sqlite3.DIRECTONLY, readfile(fsys)), 31 | db.CreateFunction("lsmode", 1, sqlite3.DETERMINISTIC, lsmode), 32 | sqlite3.CreateModule(db, "fsdir", nil, func(db *sqlite3.Conn, _, _, _ string, _ ...string) (fsdir, error) { 33 | err := db.DeclareVTab(`CREATE TABLE x(name TEXT,mode INT,mtime TIMESTAMP,data BLOB,path HIDDEN,dir HIDDEN)`) 34 | if err == nil { 35 | err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY) 36 | } 37 | return fsdir{fsys}, err 38 | })) 39 | } 40 | 41 | func lsmode(ctx sqlite3.Context, arg ...sqlite3.Value) { 42 | ctx.ResultText(fs.FileMode(arg[0].Int()).String()) 43 | } 44 | 45 | func readfile(fsys fs.FS) sqlite3.ScalarFunction { 46 | return func(ctx sqlite3.Context, arg ...sqlite3.Value) { 47 | var err error 48 | var data []byte 49 | 50 | if fsys != nil { 51 | data, err = fs.ReadFile(fsys, arg[0].Text()) 52 | } else { 53 | data, err = os.ReadFile(arg[0].Text()) 54 | } 55 | 56 | switch { 57 | case err == nil: 58 | ctx.ResultBlob(data) 59 | case !errors.Is(err, fs.ErrNotExist): 60 | ctx.ResultError(fmt.Errorf("readfile: %w", err)) // notest 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= 2 | github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= 6 | github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= 7 | github.com/ncruces/sort v0.1.6 h1:TrsJfGRH1AoWoaeB4/+gCohot9+cA6u/INaH5agIhNk= 8 | github.com/ncruces/sort v0.1.6/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk= 9 | github.com/ncruces/wbt v0.2.0 h1:Q9zlKOBSZc7Yy/R2cGa35g6RKUUE3BjNIW3tfGC4F04= 10 | github.com/ncruces/wbt v0.2.0/go.mod h1:DtF92amvMxH69EmBFUSFWRDAlo6hOEfoNQnClxj9C/c= 11 | github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE= 12 | github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE= 13 | github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= 14 | github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= 15 | golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= 16 | golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 17 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 18 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 19 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 20 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 21 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 22 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 23 | lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA= 24 | lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw= 25 | -------------------------------------------------------------------------------- /ext/fileio/fsdir_test.go: -------------------------------------------------------------------------------- 1 | package fileio_test 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "io/fs" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/ncruces/go-sqlite3" 12 | "github.com/ncruces/go-sqlite3/driver" 13 | _ "github.com/ncruces/go-sqlite3/embed" 14 | "github.com/ncruces/go-sqlite3/ext/fileio" 15 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 16 | "github.com/ncruces/go-sqlite3/vfs/memdb" 17 | ) 18 | 19 | func Test_fsdir(t *testing.T) { 20 | t.Parallel() 21 | 22 | for _, fsys := range []fs.FS{nil, os.DirFS(".")} { 23 | t.Run("", func(t *testing.T) { 24 | dsn := memdb.TestDB(t) 25 | 26 | db, err := driver.Open(dsn, func(c *sqlite3.Conn) error { 27 | fileio.RegisterFS(c, fsys) 28 | return nil 29 | }) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | defer db.Close() 34 | 35 | rows, err := db.Query(`SELECT * FROM fsdir('.', '.')`) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | for rows.Next() { 41 | var name string 42 | var mode fs.FileMode 43 | var mtime time.Time 44 | var data sql.RawBytes 45 | err := rows.Scan(&name, &mode, sqlite3.TimeFormatUnixFrac.Scanner(&mtime), &data) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if mode.Perm() == 0 { 50 | t.Errorf("got: %v", mode) 51 | } 52 | if mtime.Before(time.Unix(0, 0)) { 53 | t.Errorf("got: %v", mtime) 54 | } 55 | if name == "fsdir_test.go" { 56 | if !bytes.HasPrefix(data, []byte("package fileio_test")) { 57 | t.Errorf("got: %s", data[:min(64, len(data))]) 58 | } 59 | } 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func Test_fsdir_errors(t *testing.T) { 66 | t.Parallel() 67 | 68 | db, err := sqlite3.Open(":memory:") 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | defer db.Close() 73 | 74 | err = fileio.Register(db) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | err = db.Exec(`SELECT name FROM fsdir()`) 80 | if err == nil { 81 | t.Fatal("want error") 82 | } else { 83 | t.Log(err) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /litestream/api.go: -------------------------------------------------------------------------------- 1 | // Package litestream implements a Litestream lightweight read-replica VFS. 2 | package litestream 3 | 4 | import ( 5 | "log/slog" 6 | "sync" 7 | "time" 8 | 9 | "github.com/benbjohnson/litestream" 10 | 11 | "github.com/ncruces/go-sqlite3/vfs" 12 | ) 13 | 14 | const ( 15 | // The default poll interval. 16 | DefaultPollInterval = 1 * time.Second 17 | 18 | // The default cache size: 10 MiB. 19 | DefaultCacheSize = 10 * 1024 * 1024 20 | ) 21 | 22 | func init() { 23 | vfs.Register("litestream", liteVFS{}) 24 | } 25 | 26 | var ( 27 | liteMtx sync.RWMutex 28 | // +checklocks:liteMtx 29 | liteDBs = map[string]*liteDB{} 30 | ) 31 | 32 | // ReplicaOptions represents options for [NewReplica]. 33 | type ReplicaOptions struct { 34 | // Where to log error messages. May be nil. 35 | Logger *slog.Logger 36 | 37 | // Replica poll interval. 38 | // Should be less than the compaction interval 39 | // used by the replica at MinLevel+1. 40 | PollInterval time.Duration 41 | 42 | // CacheSize is the maximum size of the page cache in bytes. 43 | // Zero means DefaultCacheSize, negative disables caching. 44 | CacheSize int 45 | } 46 | 47 | // NewReplica creates a read-replica from a Litestream client. 48 | func NewReplica(name string, client ReplicaClient, options ReplicaOptions) { 49 | if options.Logger != nil { 50 | options.Logger = options.Logger.With("name", name) 51 | } else { 52 | options.Logger = slog.New(slog.DiscardHandler) 53 | } 54 | if options.PollInterval <= 0 { 55 | options.PollInterval = DefaultPollInterval 56 | } 57 | if options.CacheSize == 0 { 58 | options.CacheSize = DefaultCacheSize 59 | } 60 | 61 | liteMtx.Lock() 62 | liteDBs[name] = &liteDB{ 63 | client: client, 64 | opts: options, 65 | cache: pageCache{size: options.CacheSize}, 66 | } 67 | liteMtx.Unlock() 68 | } 69 | 70 | // RemoveReplica removes a replica by name. 71 | func RemoveReplica(name string) { 72 | liteMtx.Lock() 73 | delete(liteDBs, name) 74 | liteMtx.Unlock() 75 | } 76 | 77 | type ReplicaClient = litestream.ReplicaClient 78 | -------------------------------------------------------------------------------- /vfs/os_linux.go: -------------------------------------------------------------------------------- 1 | //go:build !sqlite3_flock 2 | 3 | package vfs 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "time" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | func osSync(file *os.File, _ OpenFlag, _ SyncFlag) error { 14 | // SQLite trusts Linux's fdatasync for all fsync's. 15 | for { 16 | err := unix.Fdatasync(int(file.Fd())) 17 | if err != unix.EINTR { 18 | return err 19 | } 20 | } 21 | } 22 | 23 | func osAllocate(file *os.File, size int64) error { 24 | if size == 0 { 25 | return nil 26 | } 27 | for { 28 | err := unix.Fallocate(int(file.Fd()), 0, 0, size) 29 | if err == unix.EOPNOTSUPP { 30 | break 31 | } 32 | if err != unix.EINTR { 33 | return err 34 | } 35 | } 36 | off, err := file.Seek(0, io.SeekEnd) 37 | if err != nil { 38 | return err 39 | } 40 | if size <= off { 41 | return nil 42 | } 43 | return file.Truncate(size) 44 | 45 | } 46 | 47 | func osReadLock(file *os.File, start, len int64, timeout time.Duration) error { 48 | return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK) 49 | } 50 | 51 | func osWriteLock(file *os.File, start, len int64, timeout time.Duration) error { 52 | return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK) 53 | } 54 | 55 | func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, def _ErrorCode) error { 56 | lock := unix.Flock_t{ 57 | Type: typ, 58 | Start: start, 59 | Len: len, 60 | } 61 | var err error 62 | switch { 63 | default: 64 | err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock) 65 | case timeout < 0: 66 | err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLKW, &lock) 67 | } 68 | return osLockErrorCode(err, def) 69 | } 70 | 71 | func osUnlock(file *os.File, start, len int64) error { 72 | lock := unix.Flock_t{ 73 | Type: unix.F_UNLCK, 74 | Start: start, 75 | Len: len, 76 | } 77 | for { 78 | err := unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock) 79 | if err == nil { 80 | return nil 81 | } 82 | if err != unix.EINTR { 83 | return sysError{err, _IOERR_UNLOCK} 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ext/ipaddr/ipaddr_test.go: -------------------------------------------------------------------------------- 1 | package ipaddr_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ncruces/go-sqlite3/driver" 7 | _ "github.com/ncruces/go-sqlite3/embed" 8 | "github.com/ncruces/go-sqlite3/ext/ipaddr" 9 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 10 | "github.com/ncruces/go-sqlite3/vfs/memdb" 11 | ) 12 | 13 | func TestRegister(t *testing.T) { 14 | t.Parallel() 15 | dsn := memdb.TestDB(t) 16 | 17 | db, err := driver.Open(dsn, ipaddr.Register) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | defer db.Close() 22 | 23 | var got string 24 | 25 | err = db.QueryRow(`SELECT ipfamily('::1')`).Scan(&got) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | if got != "6" { 30 | t.Fatalf("got %s", got) 31 | } 32 | 33 | err = db.QueryRow(`SELECT ipfamily('[::1]:80')`).Scan(&got) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | if got != "6" { 38 | t.Fatalf("got %s", got) 39 | } 40 | 41 | err = db.QueryRow(`SELECT ipfamily('192.168.1.5/24')`).Scan(&got) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | if got != "4" { 46 | t.Fatalf("got %s", got) 47 | } 48 | 49 | err = db.QueryRow(`SELECT iphost('192.168.1.5/24')`).Scan(&got) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | if got != "192.168.1.5" { 54 | t.Fatalf("got %s", got) 55 | } 56 | 57 | err = db.QueryRow(`SELECT ipmasklen('192.168.1.5/24')`).Scan(&got) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | if got != "24" { 62 | t.Fatalf("got %s", got) 63 | } 64 | 65 | err = db.QueryRow(`SELECT ipnetwork('192.168.1.5/24')`).Scan(&got) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | if got != "192.168.1.0/24" { 70 | t.Fatalf("got %s", got) 71 | } 72 | 73 | err = db.QueryRow(`SELECT ipcontains('192.168.1.0/24', '192.168.1.5')`).Scan(&got) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | if got != "1" { 78 | t.Fatalf("got %s", got) 79 | } 80 | 81 | err = db.QueryRow(`SELECT ipoverlaps('192.168.1.0/24', '192.168.1.5/32')`).Scan(&got) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | if got != "1" { 86 | t.Fatalf("got %s", got) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/alloc/alloc_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package alloc 4 | 5 | import ( 6 | "math" 7 | 8 | "golang.org/x/sys/unix" 9 | 10 | "github.com/tetratelabs/wazero/experimental" 11 | ) 12 | 13 | func NewMemory(cap, max uint64) experimental.LinearMemory { 14 | // Round up to the page size. 15 | rnd := uint64(unix.Getpagesize() - 1) 16 | res := (max + rnd) &^ rnd 17 | 18 | if res > math.MaxInt { 19 | // This ensures int(res) overflows to a negative value, 20 | // and unix.Mmap returns EINVAL. 21 | res = math.MaxUint64 22 | } 23 | 24 | com := res 25 | prot := unix.PROT_READ | unix.PROT_WRITE 26 | if cap < max { // Commit memory only if cap=max. 27 | com = 0 28 | prot = unix.PROT_NONE 29 | } 30 | 31 | // Reserve res bytes of address space, to ensure we won't need to move it. 32 | // A protected, private, anonymous mapping should not commit memory. 33 | b, err := unix.Mmap(-1, 0, int(res), prot, unix.MAP_PRIVATE|unix.MAP_ANON) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return &mmappedMemory{buf: b[:com]} 38 | } 39 | 40 | // The slice covers the entire mmapped memory: 41 | // - len(buf) is the already committed memory, 42 | // - cap(buf) is the reserved address space. 43 | type mmappedMemory struct { 44 | buf []byte 45 | } 46 | 47 | func (m *mmappedMemory) Reallocate(size uint64) []byte { 48 | com := uint64(len(m.buf)) 49 | res := uint64(cap(m.buf)) 50 | if com < size && size <= res { 51 | // Grow geometrically, round up to the page size. 52 | rnd := uint64(unix.Getpagesize() - 1) 53 | new := com + com>>3 54 | new = min(max(size, new), res) 55 | new = (new + rnd) &^ rnd 56 | 57 | // Commit additional memory up to new bytes. 58 | err := unix.Mprotect(m.buf[com:new], unix.PROT_READ|unix.PROT_WRITE) 59 | if err != nil { 60 | return nil 61 | } 62 | 63 | m.buf = m.buf[:new] // Update committed memory. 64 | } 65 | // Limit returned capacity because bytes beyond 66 | // len(m.buf) have not yet been committed. 67 | return m.buf[:size:len(m.buf)] 68 | } 69 | 70 | func (m *mmappedMemory) Free() { 71 | err := unix.Munmap(m.buf[:cap(m.buf)]) 72 | if err != nil { 73 | panic(err) 74 | } 75 | m.buf = nil 76 | } 77 | -------------------------------------------------------------------------------- /tests/quote_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "math" 7 | "testing" 8 | "time" 9 | 10 | "github.com/ncruces/go-sqlite3" 11 | ) 12 | 13 | func TestQuote(t *testing.T) { 14 | tests := []struct { 15 | val any 16 | want string 17 | }{ 18 | {`abc`, "'abc'"}, 19 | {`a"bc`, "'a\"bc'"}, 20 | {`a'bc`, "'a''bc'"}, 21 | {"\x07bc", "'\abc'"}, 22 | {"\x1c\n", "'\x1c\n'"}, 23 | {"\xB0\x00\x0B", "'\xB0'"}, 24 | {[]byte("\xB0\x00\x0B"), "x'B0000B'"}, 25 | 26 | {0, "0"}, 27 | {true, "1"}, 28 | {false, "0"}, 29 | {nil, "NULL"}, 30 | {math.NaN(), "NULL"}, 31 | {math.Inf(1), "9.0e999"}, 32 | {math.Inf(-1), "-9.0e999"}, 33 | {math.Pi, "3.141592653589793"}, 34 | {int64(math.MaxInt64), "9223372036854775807"}, 35 | {time.Unix(0, 0).UTC(), "'1970-01-01T00:00:00Z'"}, 36 | {sqlite3.ZeroBlob(4), "x'00000000'"}, 37 | {int8(0), "0"}, 38 | {uint(0), "0"}, 39 | {float32(0), "0"}, 40 | {(*string)(nil), "NULL"}, 41 | {json.Number("0"), "'0'"}, 42 | {&sql.RawBytes{'0'}, "x'30'"}, 43 | {t, ""}, // panic 44 | } 45 | 46 | for _, tt := range tests { 47 | t.Run(tt.want, func(t *testing.T) { 48 | defer func() { 49 | if r := recover(); r != nil && tt.want != "" { 50 | t.Errorf("Quote(%q) = %v", tt.val, r) 51 | } 52 | }() 53 | 54 | if got := sqlite3.Quote(tt.val); got != tt.want { 55 | t.Errorf("Quote(%v) = %q, want %q", tt.val, got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestQuoteIdentifier(t *testing.T) { 62 | tests := []struct { 63 | id string 64 | want string 65 | }{ 66 | {`abc`, `"abc"`}, 67 | {`a"bc`, `"a""bc"`}, 68 | {`a'bc`, `"a'bc"`}, 69 | {"\x07bc", "\"\abc\""}, 70 | {"\x1c\n", "\"\x1c\n\""}, 71 | {"\xB0\x00\x0B", ""}, // panic 72 | } 73 | 74 | for _, tt := range tests { 75 | t.Run(tt.want, func(t *testing.T) { 76 | defer func() { 77 | if r := recover(); r != nil && tt.want != "" { 78 | t.Errorf("QuoteIdentifier(%q) = %v", tt.id, r) 79 | } 80 | }() 81 | 82 | if got := sqlite3.QuoteIdentifier(tt.id); got != tt.want { 83 | t.Errorf("QuoteIdentifier(%v) = %q, want %q", tt.id, got, tt.want) 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /driver/json_test.go: -------------------------------------------------------------------------------- 1 | package driver_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/ncruces/go-sqlite3" 8 | "github.com/ncruces/go-sqlite3/driver" 9 | _ "github.com/ncruces/go-sqlite3/embed" 10 | _ "github.com/ncruces/go-sqlite3/vfs/memdb" 11 | ) 12 | 13 | func Example_json() { 14 | db, err := driver.Open("file:/json.db?vfs=memdb") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | defer db.Close() 19 | 20 | _, err = db.Exec(` 21 | CREATE TABLE orders ( 22 | cart_id INTEGER PRIMARY KEY, 23 | user_id INTEGER NOT NULL, 24 | cart BLOB -- stored as JSONB 25 | ) STRICT; 26 | `) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | type CartItem struct { 32 | ItemID string `json:"id"` 33 | Name string `json:"name"` 34 | Quantity int `json:"quantity,omitempty"` 35 | Price int `json:"price,omitempty"` 36 | } 37 | 38 | type Cart struct { 39 | Items []CartItem `json:"items"` 40 | } 41 | 42 | // convert to JSONB on insertion 43 | _, err = db.Exec(`INSERT INTO orders (user_id, cart) VALUES (?, jsonb(?))`, 123, sqlite3.JSON(Cart{ 44 | []CartItem{ 45 | {ItemID: "111", Name: "T-shirt", Quantity: 1, Price: 250}, 46 | {ItemID: "222", Name: "Trousers", Quantity: 1, Price: 600}, 47 | }, 48 | })) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | var total string 54 | err = db.QueryRow(` 55 | SELECT total(json_each.value -> 'price') 56 | FROM orders, json_each(cart -> 'items') 57 | WHERE cart_id = last_insert_rowid() 58 | `).Scan(&total) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | fmt.Println("total:", total) 64 | 65 | var cart Cart 66 | err = db.QueryRow(` 67 | SELECT json(cart) -- convert to JSON on retrieval 68 | FROM orders 69 | WHERE cart_id = last_insert_rowid() 70 | `).Scan(sqlite3.JSON(&cart)) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | for _, item := range cart.Items { 76 | fmt.Printf("id: %s, name: %s, quantity: %d, price: %d\n", 77 | item.ItemID, item.Name, item.Quantity, item.Price) 78 | } 79 | 80 | // Output: 81 | // total: 850 82 | // id: 111, name: T-shirt, quantity: 1, price: 250 83 | // id: 222, name: Trousers, quantity: 1, price: 600 84 | } 85 | -------------------------------------------------------------------------------- /embed/README.md: -------------------------------------------------------------------------------- 1 | # Embeddable Wasm build of SQLite 2 | 3 | This folder includes an embeddable Wasm build of SQLite 3.51.1 for use with 4 | [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3). 5 | 6 | The following optional features are compiled in: 7 | - [math functions](https://sqlite.org/lang_mathfunc.html) 8 | - [FTS5](https://sqlite.org/fts5.html) 9 | - [JSON](https://sqlite.org/json1.html) 10 | - [R*Tree](https://sqlite.org/rtree.html) 11 | - [GeoPoly](https://sqlite.org/geopoly.html) 12 | - [Spellfix1](https://sqlite.org/spellfix1.html) 13 | - [soundex](https://sqlite.org/lang_corefunc.html#soundex) 14 | - [stat4](https://sqlite.org/compile.html#enable_stat4) 15 | - [base64](https://github.com/sqlite/sqlite/blob/master/ext/misc/base64.c) 16 | - [decimal](https://github.com/sqlite/sqlite/blob/master/ext/misc/decimal.c) 17 | - [ieee754](https://github.com/sqlite/sqlite/blob/master/ext/misc/ieee754.c) 18 | - [regexp](https://github.com/sqlite/sqlite/blob/master/ext/misc/regexp.c) 19 | - [series](https://github.com/sqlite/sqlite/blob/master/ext/misc/series.c) 20 | - [uint](https://github.com/sqlite/sqlite/blob/master/ext/misc/uint.c) 21 | - [time](../sqlite3/time.c) 22 | 23 | See the [configuration options](../sqlite3/sqlite_opt.h), 24 | and [patches](../sqlite3) applied. 25 | 26 | Built using [`wasi-sdk`](https://github.com/WebAssembly/wasi-sdk), 27 | and [`binaryen`](https://github.com/WebAssembly/binaryen). 28 | 29 | The build is easily reproducible, and verifiable, using 30 | [Artifact Attestations](https://github.com/ncruces/go-sqlite3/attestations). 31 | 32 | ### Customizing the build 33 | 34 | You can use your own custom build of SQLite. 35 | 36 | Examples of custom builds of SQLite are: 37 | - [`github.com/ncruces/go-sqlite3/embed/bcw2`](https://github.com/ncruces/go-sqlite3/tree/main/embed/bcw2) 38 | built from a branch supporting [`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md) 39 | and [Wal2](https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md). 40 | - [`github.com/asg017/sqlite-vec-go-bindings/ncruces`](https://github.com/asg017/sqlite-vec-go-bindings) 41 | which includes the [`sqlite-vec`](https://github.com/asg017/sqlite-vec) vector search extension. -------------------------------------------------------------------------------- /ext/fileio/write_test.go: -------------------------------------------------------------------------------- 1 | package fileio 2 | 3 | import ( 4 | "database/sql" 5 | "io/fs" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/ncruces/go-sqlite3/driver" 11 | _ "github.com/ncruces/go-sqlite3/embed" 12 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 13 | "github.com/ncruces/go-sqlite3/vfs/memdb" 14 | ) 15 | 16 | func Test_writefile(t *testing.T) { 17 | t.Parallel() 18 | dsn := memdb.TestDB(t) 19 | 20 | db, err := driver.Open(dsn, Register) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | defer db.Close() 25 | 26 | dir := t.TempDir() 27 | link := filepath.Join(dir, "link") 28 | file := filepath.Join(dir, "test.txt") 29 | nest := filepath.Join(dir, "tmp", "test.txt") 30 | sock := filepath.Join(dir, "sock") 31 | twosday := time.Date(2022, 2, 22, 22, 22, 22, 0, time.UTC) 32 | 33 | _, err = db.Exec(`SELECT writefile(?, 'Hello world!')`, file) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | _, err = db.Exec(`SELECT writefile(?, ?, ?)`, link, "test.txt", fs.ModeSymlink) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | _, err = db.Exec(`SELECT writefile(?, ?, ?, ?)`, dir, nil, 0040700, twosday.Unix()) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | rows, err := db.Query(`SELECT * FROM fsdir('.', ?)`, dir) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | for rows.Next() { 54 | var name string 55 | var mode fs.FileMode 56 | var mtime time.Time 57 | var data sql.NullString 58 | err := rows.Scan(&name, &mode, &mtime, &data) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | if mode.IsDir() && !mtime.Equal(twosday) { 63 | t.Errorf("got: %v", mtime) 64 | } 65 | if mode.IsRegular() && data.String != "Hello world!" { 66 | t.Errorf("got: %v", data) 67 | } 68 | if mode&fs.ModeSymlink != 0 && data.String != "test.txt" { 69 | t.Errorf("got: %v", data) 70 | } 71 | } 72 | 73 | _, err = db.Exec(`SELECT writefile(?, 'Hello world!')`, nest) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | _, err = db.Exec(`SELECT writefile(?, ?, ?)`, sock, nil, fs.ModeSocket) 79 | if err == nil { 80 | t.Fatal("want error") 81 | } else { 82 | t.Log(err) 83 | } 84 | 85 | _, err = db.Exec(`SELECT writefile()`) 86 | if err == nil { 87 | t.Fatal("want error") 88 | } else { 89 | t.Log(err) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /util/fsutil/mode.go: -------------------------------------------------------------------------------- 1 | // Package fsutil implements filesystem utilities. 2 | package fsutil 3 | 4 | import ( 5 | "io/fs" 6 | 7 | "github.com/ncruces/go-sqlite3" 8 | "github.com/ncruces/go-sqlite3/internal/util" 9 | ) 10 | 11 | // ParseFileMode parses a file mode as returned by 12 | // [fs.FileMode.String]. 13 | func ParseFileMode(str string) (fs.FileMode, error) { 14 | var mode fs.FileMode 15 | err := util.ErrorString("invalid mode: " + str) 16 | 17 | if len(str) < 10 { 18 | return 0, err 19 | } 20 | 21 | for i, c := range []byte("dalTLDpSugct?") { 22 | if str[0] == c { 23 | if len(str) < 10 { 24 | return 0, err 25 | } 26 | mode |= 1 << uint(32-1-i) 27 | str = str[1:] 28 | } 29 | } 30 | 31 | if mode == 0 { 32 | if str[0] != '-' { 33 | return 0, err 34 | } 35 | str = str[1:] 36 | } 37 | if len(str) != 9 { 38 | return 0, err 39 | } 40 | 41 | for i, c := range []byte("rwxrwxrwx") { 42 | if str[i] == c { 43 | mode |= 1 << uint(9-1-i) 44 | } 45 | if str[i] != '-' { 46 | return 0, err 47 | } 48 | } 49 | 50 | return mode, nil 51 | } 52 | 53 | // FileModeFromUnix converts a POSIX mode_t to a file mode. 54 | func FileModeFromUnix(mode fs.FileMode) fs.FileMode { 55 | const ( 56 | S_IFMT fs.FileMode = 0170000 57 | S_IFIFO fs.FileMode = 0010000 58 | S_IFCHR fs.FileMode = 0020000 59 | S_IFDIR fs.FileMode = 0040000 60 | S_IFBLK fs.FileMode = 0060000 61 | S_IFREG fs.FileMode = 0100000 62 | S_IFLNK fs.FileMode = 0120000 63 | S_IFSOCK fs.FileMode = 0140000 64 | ) 65 | 66 | switch mode & S_IFMT { 67 | case S_IFDIR: 68 | mode |= fs.ModeDir 69 | case S_IFLNK: 70 | mode |= fs.ModeSymlink 71 | case S_IFBLK: 72 | mode |= fs.ModeDevice 73 | case S_IFCHR: 74 | mode |= fs.ModeCharDevice | fs.ModeDevice 75 | case S_IFIFO: 76 | mode |= fs.ModeNamedPipe 77 | case S_IFSOCK: 78 | mode |= fs.ModeSocket 79 | case S_IFREG, 0: 80 | // 81 | default: 82 | mode |= fs.ModeIrregular 83 | } 84 | 85 | return mode &^ S_IFMT 86 | } 87 | 88 | // FileModeFromValue calls [FileModeFromUnix] for numeric values, 89 | // and [ParseFileMode] for textual values. 90 | func FileModeFromValue(val sqlite3.Value) fs.FileMode { 91 | if n := val.Int64(); n != 0 { 92 | return FileModeFromUnix(fs.FileMode(n)) 93 | } 94 | mode, _ := ParseFileMode(val.Text()) 95 | return mode 96 | } 97 | -------------------------------------------------------------------------------- /internal/util/mmap_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package util 4 | 5 | import ( 6 | "context" 7 | "os" 8 | "unsafe" 9 | 10 | "golang.org/x/sys/unix" 11 | 12 | "github.com/tetratelabs/wazero/api" 13 | ) 14 | 15 | type mmapState struct { 16 | regions []*MappedRegion 17 | } 18 | 19 | func (s *mmapState) new(ctx context.Context, mod api.Module, size int32) *MappedRegion { 20 | // Find unused region. 21 | for _, r := range s.regions { 22 | if !r.used && r.size == size { 23 | return r 24 | } 25 | } 26 | 27 | // Allocate page aligned memmory. 28 | alloc := mod.ExportedFunction("aligned_alloc") 29 | stack := [...]Stk_t{ 30 | Stk_t(unix.Getpagesize()), 31 | Stk_t(size), 32 | } 33 | if err := alloc.CallWithStack(ctx, stack[:]); err != nil { 34 | panic(err) 35 | } 36 | if stack[0] == 0 { 37 | panic(OOMErr) 38 | } 39 | 40 | // Save the newly allocated region. 41 | ptr := Ptr_t(stack[0]) 42 | buf := View(mod, ptr, int64(size)) 43 | ret := &MappedRegion{ 44 | Ptr: ptr, 45 | size: size, 46 | addr: unsafe.Pointer(&buf[0]), 47 | } 48 | s.regions = append(s.regions, ret) 49 | return ret 50 | } 51 | 52 | type MappedRegion struct { 53 | addr unsafe.Pointer 54 | Ptr Ptr_t 55 | size int32 56 | used bool 57 | } 58 | 59 | func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32, readOnly bool) (*MappedRegion, error) { 60 | s := ctx.Value(moduleKey{}).(*moduleState) 61 | r := s.new(ctx, mod, size) 62 | err := r.mmap(f, offset, readOnly) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return r, nil 67 | } 68 | 69 | func (r *MappedRegion) Unmap() error { 70 | // We can't munmap the region, otherwise it could be remaped. 71 | // Instead, convert it to a protected, private, anonymous mapping. 72 | // If successful, it can be reused for a subsequent mmap. 73 | _, err := unix.MmapPtr(-1, 0, r.addr, uintptr(r.size), 74 | unix.PROT_NONE, unix.MAP_PRIVATE|unix.MAP_FIXED|unix.MAP_ANON) 75 | r.used = err != nil 76 | return err 77 | } 78 | 79 | func (r *MappedRegion) mmap(f *os.File, offset int64, readOnly bool) error { 80 | prot := unix.PROT_READ 81 | if !readOnly { 82 | prot |= unix.PROT_WRITE 83 | } 84 | _, err := unix.MmapPtr(int(f.Fd()), offset, r.addr, uintptr(r.size), 85 | prot, unix.MAP_SHARED|unix.MAP_FIXED) 86 | r.used = err == nil 87 | return err 88 | } 89 | -------------------------------------------------------------------------------- /ext/stats/moments_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func Test_moments(t *testing.T) { 9 | t.Parallel() 10 | 11 | var s1 moments 12 | s1.enqueue(1) 13 | s1.dequeue(1) 14 | if !math.IsNaN(s1.skewness_pop()) { 15 | t.Errorf("want NaN") 16 | } 17 | if !math.IsNaN(s1.raw_kurtosis_pop()) { 18 | t.Errorf("want NaN") 19 | } 20 | 21 | s1.enqueue(+0.5377) 22 | s1.enqueue(+1.8339) 23 | s1.enqueue(-2.2588) 24 | s1.enqueue(+0.8622) 25 | s1.enqueue(+0.3188) 26 | s1.enqueue(-1.3077) 27 | s1.enqueue(-0.4336) 28 | s1.enqueue(+0.3426) 29 | s1.enqueue(+3.5784) 30 | s1.enqueue(+2.7694) 31 | 32 | if got := s1.skewness_pop(); float32(got) != 0.106098293 { 33 | t.Errorf("got %v, want 0.1061", got) 34 | } 35 | if got := s1.skewness_samp(); float32(got) != 0.1258171 { 36 | t.Errorf("got %v, want 0.1258", got) 37 | } 38 | if got := s1.raw_kurtosis_pop(); float32(got) != 2.3121266 { 39 | t.Errorf("got %v, want 2.3121", got) 40 | } 41 | if got := s1.raw_kurtosis_samp(); float32(got) != 2.7482237 { 42 | t.Errorf("got %v, want 2.7483", got) 43 | } 44 | 45 | var s2 welford 46 | 47 | s2.enqueue(+0.5377) 48 | s2.enqueue(+1.8339) 49 | s2.enqueue(-2.2588) 50 | s2.enqueue(+0.8622) 51 | s2.enqueue(+0.3188) 52 | s2.enqueue(-1.3077) 53 | s2.enqueue(-0.4336) 54 | s2.enqueue(+0.3426) 55 | s2.enqueue(+3.5784) 56 | s2.enqueue(+2.7694) 57 | 58 | if got, want := s1.mean(), s2.mean(); got != want { 59 | t.Errorf("got %v, want %v", got, want) 60 | } 61 | if got, want := s1.stddev_pop(), s2.stddev_pop(); got != want { 62 | t.Errorf("got %v, want %v", got, want) 63 | } 64 | if got, want := s1.stddev_samp(), s2.stddev_samp(); got != want { 65 | t.Errorf("got %v, want %v", got, want) 66 | } 67 | 68 | s1.enqueue(math.Pi) 69 | s1.enqueue(math.Sqrt2) 70 | s1.enqueue(math.E) 71 | s1.dequeue(math.Pi) 72 | s1.dequeue(math.E) 73 | s1.dequeue(math.Sqrt2) 74 | 75 | if got := s1.skewness_pop(); float32(got) != 0.106098293 { 76 | t.Errorf("got %v, want 0.1061", got) 77 | } 78 | if got := s1.skewness_samp(); float32(got) != 0.1258171 { 79 | t.Errorf("got %v, want 0.1258", got) 80 | } 81 | if got := s1.raw_kurtosis_pop(); float32(got) != 2.3121266 { 82 | t.Errorf("got %v, want 2.3121", got) 83 | } 84 | if got := s1.raw_kurtosis_samp(); float32(got) != 2.7482237 { 85 | t.Errorf("got %v, want 2.7483", got) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ext/serdes/serdes.go: -------------------------------------------------------------------------------- 1 | // Package serdes provides functions to (de)serialize databases. 2 | package serdes 3 | 4 | import ( 5 | "github.com/ncruces/go-sqlite3" 6 | "github.com/ncruces/go-sqlite3/util/vfsutil" 7 | "github.com/ncruces/go-sqlite3/vfs" 8 | ) 9 | 10 | const vfsName = "github.com/ncruces/go-sqlite3/ext/serdes.sliceVFS" 11 | 12 | func init() { 13 | vfs.Register(vfsName, sliceVFS{}) 14 | } 15 | 16 | var fileToOpen = make(chan *[]byte, 1) 17 | 18 | // Serialize backs up a database into a byte slice. 19 | // 20 | // https://sqlite.org/c3ref/serialize.html 21 | func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) { 22 | var file []byte 23 | fileToOpen <- &file 24 | err := db.Backup(schema, "file:serdes.db?nolock=1&vfs="+vfsName) 25 | return file, err 26 | } 27 | 28 | // Deserialize restores a database from a byte slice, 29 | // DESTROYING any contents previously stored in schema. 30 | // 31 | // To non-destructively open a database from a byte slice, 32 | // consider alternatives like the ["reader"] or ["memdb"] VFSes. 33 | // 34 | // This differs from the similarly named SQLite API 35 | // in that it DOES NOT disconnect from schema 36 | // to reopen as an in-memory database. 37 | // 38 | // https://sqlite.org/c3ref/deserialize.html 39 | // 40 | // ["memdb"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb 41 | // ["reader"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs 42 | func Deserialize(db *sqlite3.Conn, schema string, data []byte) error { 43 | fileToOpen <- &data 44 | return db.Restore(schema, "file:serdes.db?immutable=1&vfs="+vfsName) 45 | } 46 | 47 | type sliceVFS struct{} 48 | 49 | func (sliceVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) { 50 | if flags&vfs.OPEN_MAIN_DB == 0 || name != "serdes.db" { 51 | return nil, flags, sqlite3.CANTOPEN 52 | } 53 | select { 54 | case file := <-fileToOpen: 55 | return (*vfsutil.SliceFile)(file), flags | vfs.OPEN_MEMORY, nil 56 | default: 57 | return nil, flags, sqlite3.MISUSE 58 | } 59 | } 60 | 61 | func (sliceVFS) Delete(name string, dirSync bool) error { 62 | // notest // no journals to delete 63 | return sqlite3.IOERR_DELETE 64 | } 65 | 66 | func (sliceVFS) Access(name string, flag vfs.AccessFlag) (bool, error) { 67 | return name == "serdes.db", nil 68 | } 69 | 70 | func (sliceVFS) FullPathname(name string) (string, error) { 71 | return name, nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/alloc/alloc_windows.go: -------------------------------------------------------------------------------- 1 | package alloc 2 | 3 | import ( 4 | "math" 5 | "reflect" 6 | "unsafe" 7 | 8 | "golang.org/x/sys/windows" 9 | 10 | "github.com/tetratelabs/wazero/experimental" 11 | ) 12 | 13 | func NewMemory(cap, max uint64) experimental.LinearMemory { 14 | // Round up to the page size. 15 | rnd := uint64(windows.Getpagesize() - 1) 16 | res := (max + rnd) &^ rnd 17 | 18 | if res > math.MaxInt { 19 | // This ensures uintptr(res) overflows to a large value, 20 | // and windows.VirtualAlloc returns an error. 21 | res = math.MaxUint64 22 | } 23 | 24 | com := res 25 | kind := windows.MEM_COMMIT 26 | if cap < max { // Commit memory only if cap=max. 27 | com = 0 28 | kind = windows.MEM_RESERVE 29 | } 30 | 31 | // Reserve res bytes of address space, to ensure we won't need to move it. 32 | r, err := windows.VirtualAlloc(0, uintptr(res), uint32(kind), windows.PAGE_READWRITE) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | mem := virtualMemory{addr: r} 38 | // SliceHeader, although deprecated, avoids a go vet warning. 39 | sh := (*reflect.SliceHeader)(unsafe.Pointer(&mem.buf)) 40 | sh.Data = r 41 | sh.Len = int(com) 42 | sh.Cap = int(res) 43 | return &mem 44 | } 45 | 46 | // The slice covers the entire mmapped memory: 47 | // - len(buf) is the already committed memory, 48 | // - cap(buf) is the reserved address space. 49 | type virtualMemory struct { 50 | buf []byte 51 | addr uintptr 52 | } 53 | 54 | func (m *virtualMemory) Reallocate(size uint64) []byte { 55 | com := uint64(len(m.buf)) 56 | res := uint64(cap(m.buf)) 57 | if com < size && size <= res { 58 | // Grow geometrically, round up to the page size. 59 | rnd := uint64(windows.Getpagesize() - 1) 60 | new := com + com>>3 61 | new = min(max(size, new), res) 62 | new = (new + rnd) &^ rnd 63 | 64 | // Commit additional memory up to new bytes. 65 | _, err := windows.VirtualAlloc(m.addr, uintptr(new), windows.MEM_COMMIT, windows.PAGE_READWRITE) 66 | if err != nil { 67 | return nil 68 | } 69 | 70 | m.buf = m.buf[:new] // Update committed memory. 71 | } 72 | // Limit returned capacity because bytes beyond 73 | // len(m.buf) have not yet been committed. 74 | return m.buf[:size:len(m.buf)] 75 | } 76 | 77 | func (m *virtualMemory) Free() { 78 | err := windows.VirtualFree(m.addr, 0, windows.MEM_RELEASE) 79 | if err != nil { 80 | panic(err) 81 | } 82 | m.addr = 0 83 | } 84 | -------------------------------------------------------------------------------- /vfs/xts/aes_test.go: -------------------------------------------------------------------------------- 1 | package xts_test 2 | 3 | import ( 4 | _ "embed" 5 | "path/filepath" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/ncruces/go-sqlite3" 10 | "github.com/ncruces/go-sqlite3/driver" 11 | _ "github.com/ncruces/go-sqlite3/embed" 12 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 13 | "github.com/ncruces/go-sqlite3/util/ioutil" 14 | "github.com/ncruces/go-sqlite3/vfs" 15 | "github.com/ncruces/go-sqlite3/vfs/readervfs" 16 | "github.com/ncruces/go-sqlite3/vfs/xts" 17 | ) 18 | 19 | //go:embed testdata/test.db 20 | var testDB string 21 | 22 | func Test_fileformat(t *testing.T) { 23 | t.Parallel() 24 | 25 | readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(testDB))) 26 | vfs.Register("rxts", xts.Wrap(vfs.Find("reader"), nil)) 27 | 28 | db, err := driver.Open("file:test.db?vfs=rxts") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | defer db.Close() 33 | 34 | _, err = db.Exec(`PRAGMA textkey='correct+horse+battery+staple'`) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | var version uint32 40 | err = db.QueryRow(`PRAGMA user_version`).Scan(&version) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | if version != 0xBADDB { 45 | t.Error(version) 46 | } 47 | 48 | _, err = db.Exec(`PRAGMA integrity_check`) 49 | if err != nil { 50 | t.Error(err) 51 | } 52 | } 53 | 54 | func Benchmark_nokey(b *testing.B) { 55 | tmp := filepath.Join(b.TempDir(), "test.db") 56 | sqlite3.Initialize() 57 | 58 | for b.Loop() { 59 | db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1") 60 | if err != nil { 61 | b.Fatal(err) 62 | } 63 | db.Close() 64 | } 65 | } 66 | 67 | func Benchmark_hexkey(b *testing.B) { 68 | tmp := filepath.Join(b.TempDir(), "test.db") 69 | sqlite3.Initialize() 70 | 71 | for b.Loop() { 72 | db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1" + 73 | "&vfs=xts&hexkey=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") 74 | if err != nil { 75 | b.Fatal(err) 76 | } 77 | db.Close() 78 | } 79 | } 80 | 81 | func Benchmark_textkey(b *testing.B) { 82 | tmp := filepath.Join(b.TempDir(), "test.db") 83 | sqlite3.Initialize() 84 | 85 | for b.Loop() { 86 | db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1" + 87 | "&vfs=xts&textkey=correct+horse+battery+staple") 88 | if err != nil { 89 | b.Fatal(err) 90 | } 91 | db.Close() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /vfs/readervfs/reader.go: -------------------------------------------------------------------------------- 1 | package readervfs 2 | 3 | import ( 4 | "github.com/ncruces/go-sqlite3" 5 | "github.com/ncruces/go-sqlite3/util/ioutil" 6 | "github.com/ncruces/go-sqlite3/vfs" 7 | ) 8 | 9 | type readerVFS struct{} 10 | 11 | func (readerVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) { 12 | // Temp journals, as used by the sorter, use a temporary file. 13 | if flags&vfs.OPEN_TEMP_JOURNAL != 0 { 14 | return vfs.Find("").Open(name, flags) 15 | } 16 | // Refuse to open all other file types. 17 | if flags&vfs.OPEN_MAIN_DB == 0 { 18 | return nil, flags, sqlite3.CANTOPEN 19 | } 20 | readerMtx.RLock() 21 | defer readerMtx.RUnlock() 22 | if ra, ok := readerDBs[name]; ok { 23 | return readerFile{ra}, flags | vfs.OPEN_READONLY, nil 24 | } 25 | return nil, flags, sqlite3.CANTOPEN 26 | } 27 | 28 | func (readerVFS) Delete(name string, dirSync bool) error { 29 | // notest // IOCAP_IMMUTABLE 30 | return sqlite3.IOERR_DELETE 31 | } 32 | 33 | func (readerVFS) Access(name string, flag vfs.AccessFlag) (bool, error) { 34 | // notest // IOCAP_IMMUTABLE 35 | return false, sqlite3.IOERR_ACCESS 36 | } 37 | 38 | func (readerVFS) FullPathname(name string) (string, error) { 39 | return name, nil 40 | } 41 | 42 | type readerFile struct{ ioutil.SizeReaderAt } 43 | 44 | func (readerFile) Close() error { 45 | return nil 46 | } 47 | 48 | func (readerFile) WriteAt(b []byte, off int64) (n int, err error) { 49 | // notest // IOCAP_IMMUTABLE 50 | return 0, sqlite3.IOERR_WRITE 51 | } 52 | 53 | func (readerFile) Truncate(size int64) error { 54 | // notest // IOCAP_IMMUTABLE 55 | return sqlite3.IOERR_TRUNCATE 56 | } 57 | 58 | func (readerFile) Sync(flag vfs.SyncFlag) error { 59 | // notest // IOCAP_IMMUTABLE 60 | return sqlite3.IOERR_FSYNC 61 | } 62 | 63 | func (readerFile) Lock(lock vfs.LockLevel) error { 64 | // notest // IOCAP_IMMUTABLE 65 | return sqlite3.IOERR_LOCK 66 | } 67 | 68 | func (readerFile) Unlock(lock vfs.LockLevel) error { 69 | // notest // IOCAP_IMMUTABLE 70 | return sqlite3.IOERR_UNLOCK 71 | } 72 | 73 | func (readerFile) CheckReservedLock() (bool, error) { 74 | // notest // IOCAP_IMMUTABLE 75 | return false, sqlite3.IOERR_CHECKRESERVEDLOCK 76 | } 77 | 78 | func (readerFile) SectorSize() int { 79 | // notest // IOCAP_IMMUTABLE 80 | return 0 81 | } 82 | 83 | func (readerFile) DeviceCharacteristics() vfs.DeviceCharacteristic { 84 | return vfs.IOCAP_IMMUTABLE | vfs.IOCAP_SUBPAGE_READ 85 | } 86 | -------------------------------------------------------------------------------- /vfs/readervfs/example_test.go: -------------------------------------------------------------------------------- 1 | package readervfs_test 2 | 3 | import ( 4 | "database/sql" 5 | _ "embed" 6 | "fmt" 7 | "log" 8 | "strings" 9 | 10 | "github.com/psanford/httpreadat" 11 | 12 | _ "github.com/ncruces/go-sqlite3/driver" 13 | _ "github.com/ncruces/go-sqlite3/embed" 14 | "github.com/ncruces/go-sqlite3/util/ioutil" 15 | "github.com/ncruces/go-sqlite3/vfs/readervfs" 16 | ) 17 | 18 | func Example_http() { 19 | readervfs.Create("demo.db", httpreadat.New("https://sanford.io/demo.db")) 20 | defer readervfs.Delete("demo.db") 21 | 22 | db, err := sql.Open("sqlite3", "file:demo.db?vfs=reader") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | defer db.Close() 27 | 28 | magname := map[int]string{ 29 | 3: "thousand", 30 | 6: "million", 31 | 9: "billion", 32 | } 33 | rows, err := db.Query(` 34 | SELECT period, data_value, magntude, units FROM csv 35 | WHERE period > '2010' 36 | LIMIT 10`) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | defer rows.Close() 41 | 42 | for rows.Next() { 43 | var period, units string 44 | var value int64 45 | var mag int 46 | err = rows.Scan(&period, &value, &mag, &units) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | fmt.Printf("%s: %d %s %s\n", period, value, magname[mag], units) 51 | } 52 | // Output: 53 | // 2010.03: 17463 million Dollars 54 | // 2010.06: 17260 million Dollars 55 | // 2010.09: 15419 million Dollars 56 | // 2010.12: 17088 million Dollars 57 | // 2011.03: 18516 million Dollars 58 | // 2011.06: 18835 million Dollars 59 | // 2011.09: 16390 million Dollars 60 | // 2011.12: 18748 million Dollars 61 | // 2012.03: 18477 million Dollars 62 | // 2012.06: 18270 million Dollars 63 | } 64 | 65 | //go:embed testdata/test.db 66 | var testDB string 67 | 68 | func Example_embed() { 69 | readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(testDB))) 70 | defer readervfs.Delete("test.db") 71 | 72 | db, err := sql.Open("sqlite3", "file:test.db?vfs=reader") 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | defer db.Close() 77 | 78 | rows, err := db.Query(`SELECT id, name FROM users`) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | defer rows.Close() 83 | 84 | for rows.Next() { 85 | var id, name string 86 | err = rows.Scan(&id, &name) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | fmt.Printf("%s %s\n", id, name) 91 | } 92 | // Output: 93 | // 0 go 94 | // 1 zig 95 | // 2 whatever 96 | } 97 | -------------------------------------------------------------------------------- /vfs/adiantum/adiantum_test.go: -------------------------------------------------------------------------------- 1 | package adiantum_test 2 | 3 | import ( 4 | _ "embed" 5 | "path/filepath" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/ncruces/go-sqlite3" 10 | "github.com/ncruces/go-sqlite3/driver" 11 | _ "github.com/ncruces/go-sqlite3/embed" 12 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 13 | "github.com/ncruces/go-sqlite3/util/ioutil" 14 | "github.com/ncruces/go-sqlite3/vfs" 15 | "github.com/ncruces/go-sqlite3/vfs/adiantum" 16 | "github.com/ncruces/go-sqlite3/vfs/readervfs" 17 | ) 18 | 19 | //go:embed testdata/test.db 20 | var testDB string 21 | 22 | func Test_fileformat(t *testing.T) { 23 | t.Parallel() 24 | 25 | readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(testDB))) 26 | vfs.Register("radiantum", adiantum.Wrap(vfs.Find("reader"), nil)) 27 | 28 | db, err := driver.Open("file:test.db?vfs=radiantum") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | defer db.Close() 33 | 34 | _, err = db.Exec(`PRAGMA textkey='correct+horse+battery+staple'`) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | var version uint32 40 | err = db.QueryRow(`PRAGMA user_version`).Scan(&version) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | if version != 0xBADDB { 45 | t.Error(version) 46 | } 47 | 48 | _, err = db.Exec(`PRAGMA integrity_check`) 49 | if err != nil { 50 | t.Error(err) 51 | } 52 | } 53 | 54 | func Benchmark_nokey(b *testing.B) { 55 | tmp := filepath.Join(b.TempDir(), "test.db") 56 | sqlite3.Initialize() 57 | 58 | for b.Loop() { 59 | db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1") 60 | if err != nil { 61 | b.Fatal(err) 62 | } 63 | db.Close() 64 | } 65 | } 66 | 67 | func Benchmark_hexkey(b *testing.B) { 68 | tmp := filepath.Join(b.TempDir(), "test.db") 69 | sqlite3.Initialize() 70 | 71 | for b.Loop() { 72 | db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1" + 73 | "&vfs=adiantum&hexkey=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") 74 | if err != nil { 75 | b.Fatal(err) 76 | } 77 | db.Close() 78 | } 79 | } 80 | 81 | func Benchmark_textkey(b *testing.B) { 82 | tmp := filepath.Join(b.TempDir(), "test.db") 83 | sqlite3.Initialize() 84 | 85 | for b.Loop() { 86 | db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1" + 87 | "&vfs=adiantum&textkey=correct+horse+battery+staple") 88 | if err != nil { 89 | b.Fatal(err) 90 | } 91 | db.Close() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ext/fileio/write.go: -------------------------------------------------------------------------------- 1 | package fileio 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | 11 | "github.com/ncruces/go-sqlite3" 12 | "github.com/ncruces/go-sqlite3/internal/util" 13 | "github.com/ncruces/go-sqlite3/util/fsutil" 14 | ) 15 | 16 | func writefile(ctx sqlite3.Context, arg ...sqlite3.Value) { 17 | if len(arg) < 2 || len(arg) > 4 { 18 | ctx.ResultError(util.ErrorString("writefile: wrong number of arguments")) 19 | return 20 | } 21 | 22 | file := arg[0].Text() 23 | 24 | var mode fs.FileMode 25 | if len(arg) > 2 { 26 | mode = fsutil.FileModeFromValue(arg[2]) 27 | } 28 | 29 | n, err := createFileAndDir(file, mode, arg[1]) 30 | if err != nil { 31 | if len(arg) > 2 { 32 | ctx.ResultError(fmt.Errorf("writefile: %w", err)) // notest 33 | } 34 | return 35 | } 36 | 37 | if mode&fs.ModeSymlink == 0 { 38 | if len(arg) > 2 { 39 | err := os.Chmod(file, mode.Perm()) 40 | if err != nil { 41 | ctx.ResultError(fmt.Errorf("writefile: %w", err)) 42 | return // notest 43 | } 44 | } 45 | 46 | if len(arg) > 3 { 47 | mtime := arg[3].Time(sqlite3.TimeFormatUnixFrac) 48 | err := os.Chtimes(file, time.Time{}, mtime) 49 | if err != nil { 50 | ctx.ResultError(fmt.Errorf("writefile: %w", err)) 51 | return // notest 52 | } 53 | } 54 | } 55 | 56 | if mode.IsRegular() { 57 | ctx.ResultInt(n) 58 | } 59 | } 60 | 61 | func createFileAndDir(path string, mode fs.FileMode, data sqlite3.Value) (int, error) { 62 | n, err := createFile(path, mode, data) 63 | if errors.Is(err, fs.ErrNotExist) { 64 | if err := os.MkdirAll(filepath.Dir(path), 0777); err == nil { 65 | return createFile(path, mode, data) 66 | } 67 | } 68 | return n, err 69 | } 70 | 71 | func createFile(path string, mode fs.FileMode, data sqlite3.Value) (int, error) { 72 | if mode.IsRegular() { 73 | blob := data.RawBlob() 74 | return len(blob), os.WriteFile(path, blob, fixPerm(mode, 0666)) 75 | } 76 | if mode.IsDir() { 77 | err := os.Mkdir(path, fixPerm(mode, 0777)) 78 | if errors.Is(err, fs.ErrExist) { 79 | s, err := os.Lstat(path) 80 | if err == nil && s.IsDir() { 81 | return 0, nil 82 | } 83 | } 84 | return 0, err 85 | } 86 | if mode&fs.ModeSymlink != 0 { 87 | return 0, os.Symlink(data.Text(), path) 88 | } 89 | return 0, fmt.Errorf("invalid mode: %v", mode) 90 | } 91 | 92 | func fixPerm(mode fs.FileMode, def fs.FileMode) fs.FileMode { 93 | if mode.Perm() == 0 { 94 | return def 95 | } 96 | return mode.Perm() 97 | } 98 | -------------------------------------------------------------------------------- /vfs/os_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package vfs 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | const ( 13 | isUnix = true 14 | _O_NOFOLLOW = unix.O_NOFOLLOW 15 | ) 16 | 17 | func osAccess(path string, flags AccessFlag) error { 18 | var access uint32 // unix.F_OK 19 | switch flags { 20 | case ACCESS_READWRITE: 21 | access = unix.R_OK | unix.W_OK 22 | case ACCESS_READ: 23 | access = unix.R_OK 24 | } 25 | return unix.Access(path, access) 26 | } 27 | 28 | func osReadAt(file *os.File, p []byte, off int64) (int, error) { 29 | n, err := file.ReadAt(p, off) 30 | if errno, ok := err.(unix.Errno); ok { 31 | switch errno { 32 | case 33 | unix.ERANGE, 34 | unix.EIO, 35 | unix.ENXIO: 36 | return n, sysError{err, _IOERR_CORRUPTFS} 37 | } 38 | } 39 | return n, err 40 | } 41 | 42 | func osWriteAt(file *os.File, p []byte, off int64) (int, error) { 43 | n, err := file.WriteAt(p, off) 44 | if errno, ok := err.(unix.Errno); ok && errno == unix.ENOSPC { 45 | return n, sysError{err, _FULL} 46 | } 47 | return n, err 48 | } 49 | 50 | func osSetMode(file *os.File, modeof string) error { 51 | fi, err := os.Stat(modeof) 52 | if err != nil { 53 | return err 54 | } 55 | file.Chmod(fi.Mode()) 56 | if sys, ok := fi.Sys().(*syscall.Stat_t); ok { 57 | file.Chown(int(sys.Uid), int(sys.Gid)) 58 | } 59 | return nil 60 | } 61 | 62 | func osTestLock(file *os.File, start, len int64, def _ErrorCode) (int16, error) { 63 | lock := unix.Flock_t{ 64 | Type: unix.F_WRLCK, 65 | Start: start, 66 | Len: len, 67 | } 68 | for { 69 | err := unix.FcntlFlock(file.Fd(), unix.F_GETLK, &lock) 70 | if err == nil { 71 | return lock.Type, nil 72 | } 73 | if err != unix.EINTR { 74 | return 0, sysError{err, def} 75 | } 76 | } 77 | } 78 | 79 | func osLockErrorCode(err error, def _ErrorCode) error { 80 | if err == nil { 81 | return nil 82 | } 83 | if errno, ok := err.(unix.Errno); ok { 84 | switch errno { 85 | case 86 | unix.EACCES, 87 | unix.EAGAIN, 88 | unix.EBUSY, 89 | unix.EINTR, 90 | unix.ENOLCK, 91 | unix.EDEADLK, 92 | unix.ETIMEDOUT: 93 | return _BUSY 94 | case unix.EPERM: 95 | return sysError{err, _PERM} 96 | } 97 | // notest // usually EWOULDBLOCK == EAGAIN 98 | if errno == unix.EWOULDBLOCK && unix.EWOULDBLOCK != unix.EAGAIN { 99 | return _BUSY 100 | } 101 | } 102 | return sysError{err, def} 103 | } 104 | -------------------------------------------------------------------------------- /vfs/adiantum/api.go: -------------------------------------------------------------------------------- 1 | // Package adiantum wraps an SQLite VFS to offer encryption at rest. 2 | // 3 | // The "adiantum" [vfs.VFS] wraps the default VFS using the 4 | // Adiantum tweakable, length-preserving encryption. 5 | // 6 | // Importing package adiantum registers that VFS: 7 | // 8 | // import _ "github.com/ncruces/go-sqlite3/vfs/adiantum" 9 | // 10 | // To open an encrypted database you need to provide key material. 11 | // 12 | // The simplest way to do that is to specify the key through an [URI] parameter: 13 | // 14 | // - key: key material in binary (32 bytes) 15 | // - hexkey: key material in hex (64 hex digits) 16 | // - textkey: key material in text (any length) 17 | // 18 | // However, this makes your key easily accessible to other parts of 19 | // your application (e.g. through [vfs.Filename.URIParameters]). 20 | // 21 | // To avoid this, invoke any of the following PRAGMAs 22 | // immediately after opening a connection: 23 | // 24 | // PRAGMA key='D41d8cD98f00b204e9800998eCf8427e'; 25 | // PRAGMA hexkey='e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; 26 | // PRAGMA textkey='your-secret-key'; 27 | // 28 | // For an ATTACH-ed database, you must specify the schema name: 29 | // 30 | // ATTACH DATABASE 'demo.db' AS demo; 31 | // PRAGMA demo.textkey='your-secret-key'; 32 | // 33 | // [URI]: https://sqlite.org/uri.html 34 | package adiantum 35 | 36 | import ( 37 | "lukechampine.com/adiantum/hbsh" 38 | 39 | "github.com/ncruces/go-sqlite3/vfs" 40 | ) 41 | 42 | func init() { 43 | vfs.Register("adiantum", Wrap(vfs.Find(""), nil)) 44 | } 45 | 46 | // Wrap wraps a base VFS to create an encrypting VFS, 47 | // possibly using a custom HBSH cipher construction. 48 | // 49 | // To use the default Adiantum construction, set cipher to nil. 50 | // 51 | // The default construction uses a 32 byte key/hexkey. 52 | // If a textkey is provided, the default KDF is Argon2id 53 | // with 64 MiB of memory, 3 iterations, and 4 threads. 54 | func Wrap(base vfs.VFS, cipher HBSHCreator) vfs.VFS { 55 | if cipher == nil { 56 | cipher = adiantumCreator{} 57 | } 58 | return &hbshVFS{ 59 | VFS: base, 60 | init: cipher, 61 | } 62 | } 63 | 64 | // HBSHCreator creates an [hbsh.HBSH] cipher 65 | // given key material. 66 | type HBSHCreator interface { 67 | // KDF derives an HBSH key from a secret. 68 | // If no secret is given, a random key is generated. 69 | KDF(secret string) (key []byte) 70 | 71 | // HBSH creates an HBSH cipher given a key. 72 | // If key is not appropriate, nil is returned. 73 | HBSH(key []byte) *hbsh.HBSH 74 | } 75 | -------------------------------------------------------------------------------- /vfs/xts/api.go: -------------------------------------------------------------------------------- 1 | // Package xts wraps an SQLite VFS to offer encryption at rest. 2 | // 3 | // The "xts" [vfs.VFS] wraps the default VFS using the 4 | // AES-XTS tweakable, length-preserving encryption. 5 | // 6 | // Importing package xts registers that VFS: 7 | // 8 | // import _ "github.com/ncruces/go-sqlite3/vfs/xts" 9 | // 10 | // To open an encrypted database you need to provide key material. 11 | // 12 | // The simplest way to do that is to specify the key through an [URI] parameter: 13 | // 14 | // - key: key material in binary (32, 48 or 64 bytes) 15 | // - hexkey: key material in hex (64, 96 or 128 hex digits) 16 | // - textkey: key material in text (any length) 17 | // 18 | // However, this makes your key easily accessible to other parts of 19 | // your application (e.g. through [vfs.Filename.URIParameters]). 20 | // 21 | // To avoid this, invoke any of the following PRAGMAs 22 | // immediately after opening a connection: 23 | // 24 | // PRAGMA key='D41d8cD98f00b204e9800998eCf8427e'; 25 | // PRAGMA hexkey='e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; 26 | // PRAGMA textkey='your-secret-key'; 27 | // 28 | // For an ATTACH-ed database, you must specify the schema name: 29 | // 30 | // ATTACH DATABASE 'demo.db' AS demo; 31 | // PRAGMA demo.textkey='your-secret-key'; 32 | // 33 | // [URI]: https://sqlite.org/uri.html 34 | package xts 35 | 36 | import ( 37 | "golang.org/x/crypto/xts" 38 | 39 | "github.com/ncruces/go-sqlite3/vfs" 40 | ) 41 | 42 | func init() { 43 | vfs.Register("xts", Wrap(vfs.Find(""), nil)) 44 | } 45 | 46 | // Wrap wraps a base VFS to create an encrypting VFS, 47 | // possibly using a custom XTS cipher construction. 48 | // 49 | // To use the default AES-XTS construction, set cipher to nil. 50 | // 51 | // The default construction uses AES-128, AES-192, or AES-256 52 | // if the key/hexkey is 32, 48, or 64 bytes, respectively. 53 | // If a textkey is provided, the default KDF is PBKDF2-HMAC-SHA512 54 | // with 10,000 iterations, always producing a 32 byte key. 55 | func Wrap(base vfs.VFS, cipher XTSCreator) vfs.VFS { 56 | if cipher == nil { 57 | cipher = aesCreator{} 58 | } 59 | return &xtsVFS{ 60 | VFS: base, 61 | init: cipher, 62 | } 63 | } 64 | 65 | // XTSCreator creates an [xts.Cipher] 66 | // given key material. 67 | type XTSCreator interface { 68 | // KDF derives an XTS key from a secret. 69 | // If no secret is given, a random key is generated. 70 | KDF(secret string) (key []byte) 71 | 72 | // XTS creates an XTS cipher given a key. 73 | // If key is not appropriate, nil is returned. 74 | XTS(key []byte) *xts.Cipher 75 | } 76 | -------------------------------------------------------------------------------- /gormlite/sqlite_test.go: -------------------------------------------------------------------------------- 1 | package gormlite 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "gorm.io/gorm" 8 | 9 | "github.com/ncruces/go-sqlite3" 10 | "github.com/ncruces/go-sqlite3/driver" 11 | _ "github.com/ncruces/go-sqlite3/embed" 12 | _ "github.com/ncruces/go-sqlite3/internal/testcfg" 13 | "github.com/ncruces/go-sqlite3/vfs/memdb" 14 | ) 15 | 16 | func TestDialector(t *testing.T) { 17 | dsn := memdb.TestDB(t) 18 | 19 | // Custom connection with a custom function called "my_custom_function". 20 | db, err := driver.Open(dsn, func(conn *sqlite3.Conn) error { 21 | return conn.CreateFunction("my_custom_function", 0, sqlite3.DETERMINISTIC, 22 | func(ctx sqlite3.Context, arg ...sqlite3.Value) { 23 | ctx.ResultText("my-result") 24 | }) 25 | }) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | rows := []struct { 31 | description string 32 | dialector gorm.Dialector 33 | openSuccess bool 34 | query string 35 | querySuccess bool 36 | }{ 37 | { 38 | description: "Default driver", 39 | dialector: Open(dsn), 40 | openSuccess: true, 41 | query: "SELECT 1", 42 | querySuccess: true, 43 | }, 44 | { 45 | description: "Custom function", 46 | dialector: Open(dsn), 47 | openSuccess: true, 48 | query: "SELECT my_custom_function()", 49 | querySuccess: false, 50 | }, 51 | { 52 | description: "Custom connection", 53 | dialector: OpenDB(db), 54 | openSuccess: true, 55 | query: "SELECT 1", 56 | querySuccess: true, 57 | }, 58 | { 59 | description: "Custom connection, custom function", 60 | dialector: OpenDB(db), 61 | openSuccess: true, 62 | query: "SELECT my_custom_function()", 63 | querySuccess: true, 64 | }, 65 | } 66 | for rowIndex, row := range rows { 67 | t.Run(fmt.Sprintf("%d/%s", rowIndex, row.description), func(t *testing.T) { 68 | db, err := gorm.Open(row.dialector, &gorm.Config{}) 69 | if !row.openSuccess { 70 | if err == nil { 71 | t.Errorf("Expected Open to fail.") 72 | } 73 | return 74 | } 75 | 76 | if err != nil { 77 | t.Errorf("Expected Open to succeed; got error: %v", err) 78 | } 79 | if db == nil { 80 | t.Errorf("Expected db to be non-nil.") 81 | } 82 | if row.query != "" { 83 | err = db.Exec(row.query).Error 84 | if !row.querySuccess { 85 | if err == nil { 86 | t.Errorf("Expected query to fail.") 87 | } 88 | return 89 | } 90 | 91 | if err != nil { 92 | t.Errorf("Expected query to succeed; got error: %v", err) 93 | } 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /sqlite3/hooks.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "sqlite3.h" 4 | 5 | int go_progress_handler(void *); 6 | int go_busy_handler(void *, int); 7 | int go_busy_timeout(int count, int tmout); 8 | 9 | int go_commit_hook(void *); 10 | void go_rollback_hook(void *); 11 | void go_update_hook(void *, int, char const *, char const *, sqlite3_int64); 12 | int go_wal_hook(void *, sqlite3 *, const char *, int); 13 | int go_trace(unsigned, void *, void *, void *); 14 | int go_authorizer(void *, int, const char *, const char *, const char *, 15 | const char *); 16 | 17 | void go_log(void *, int, const char *); 18 | 19 | unsigned int go_autovacuum_pages(void *, const char *, unsigned int, 20 | unsigned int, unsigned int); 21 | 22 | void sqlite3_log_go(int iErrCode, const char *zMsg) { 23 | sqlite3_log(iErrCode, "%s", zMsg); 24 | } 25 | 26 | void sqlite3_progress_handler_go(sqlite3 *db, int n) { 27 | sqlite3_progress_handler(db, n, go_progress_handler, /*arg=*/NULL); 28 | } 29 | 30 | int sqlite3_busy_handler_go(sqlite3 *db, bool enable) { 31 | return sqlite3_busy_handler(db, enable ? go_busy_handler : NULL, /*arg=*/db); 32 | } 33 | 34 | void sqlite3_commit_hook_go(sqlite3 *db, bool enable) { 35 | sqlite3_commit_hook(db, enable ? go_commit_hook : NULL, /*arg=*/db); 36 | } 37 | 38 | void sqlite3_rollback_hook_go(sqlite3 *db, bool enable) { 39 | sqlite3_rollback_hook(db, enable ? go_rollback_hook : NULL, /*arg=*/db); 40 | } 41 | 42 | void sqlite3_update_hook_go(sqlite3 *db, bool enable) { 43 | sqlite3_update_hook(db, enable ? go_update_hook : NULL, /*arg=*/db); 44 | } 45 | 46 | void sqlite3_wal_hook_go(sqlite3 *db, bool enable) { 47 | sqlite3_wal_hook(db, enable ? go_wal_hook : NULL, /*arg=*/NULL); 48 | } 49 | 50 | int sqlite3_set_authorizer_go(sqlite3 *db, bool enable) { 51 | return sqlite3_set_authorizer(db, enable ? go_authorizer : NULL, /*arg=*/db); 52 | } 53 | 54 | int sqlite3_trace_go(sqlite3 *db, unsigned mask) { 55 | return sqlite3_trace_v2(db, mask, go_trace, /*arg=*/db); 56 | } 57 | 58 | int sqlite3_config_log_go(bool enable) { 59 | return sqlite3_config(SQLITE_CONFIG_LOG, enable ? go_log : NULL, 60 | /*arg=*/NULL); 61 | } 62 | 63 | int sqlite3_autovacuum_pages_go(sqlite3 *db, go_handle app) { 64 | if (app == NULL) { 65 | return sqlite3_autovacuum_pages(db, NULL, NULL, NULL); 66 | } 67 | return sqlite3_autovacuum_pages(db, go_autovacuum_pages, app, go_destroy); 68 | } 69 | 70 | #ifndef sqliteBusyCallback 71 | 72 | static int sqliteBusyCallback(void *ptr, int count) { 73 | return go_busy_timeout(count, ((sqlite3 *)ptr)->busyTimeout); 74 | } 75 | 76 | #endif --------------------------------------------------------------------------------