mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-12 14:09:13 +00:00
Compare commits
15 Commits
gormlite/v
...
gormlite/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa7edb1848 | ||
|
|
3484bda553 | ||
|
|
cf0d56271d | ||
|
|
a465458255 | ||
|
|
65af8065cd | ||
|
|
5c1c0f03a5 | ||
|
|
2d168136f1 | ||
|
|
eb8e716253 | ||
|
|
3479e8935a | ||
|
|
58e91052bb | ||
|
|
3719692349 | ||
|
|
f7ac77027c | ||
|
|
ef065b6baa | ||
|
|
e7f8311e2e | ||
|
|
35a3bfe2f9 |
13
.github/workflows/repro.sh
vendored
13
.github/workflows/repro.sh
vendored
@@ -18,6 +18,13 @@ mkdir -p tools/
|
||||
[ -d "tools/binaryen-version"* ] || curl -#L "$BINARYEN" | tar xzC tools &
|
||||
wait
|
||||
|
||||
sqlite3/download.sh # Download SQLite
|
||||
embed/build.sh # Build Wasm
|
||||
git diff --exit-code # Check diffs
|
||||
# Download and build SQLite
|
||||
sqlite3/download.sh
|
||||
embed/build.sh
|
||||
|
||||
# Download and build sqlite-createtable-parser
|
||||
util/vtabutil/parse/download.sh
|
||||
util/vtabutil/parse/build.sh
|
||||
|
||||
# Check diffs
|
||||
git diff --exit-code
|
||||
12
.github/workflows/repro.yml
vendored
12
.github/workflows/repro.yml
vendored
@@ -3,6 +3,11 @@ name: Reproducible build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
@@ -17,3 +22,10 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: .github/workflows/repro.sh
|
||||
|
||||
- uses: actions/attest-build-provenance@v1
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
with:
|
||||
subject-path: |
|
||||
embed/sqlite3.wasm
|
||||
util/vtabutil/parse/sql3parse_table.wasm
|
||||
@@ -33,6 +33,8 @@ Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ run
|
||||
provides the [`array`](https://sqlite.org/carray.html) table-valued function.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/blobio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blobio)
|
||||
simplifies [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/bloom`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/bloom)
|
||||
provides a [Bloom filter](https://github.com/nalgeon/sqlean/issues/27#issuecomment-1002267134) virtual table.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/csv`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/csv)
|
||||
reads [comma-separated values](https://sqlite.org/csv.html).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/fileio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/fileio)
|
||||
@@ -51,12 +53,12 @@ Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ run
|
||||
provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
|
||||
maps multidimensional data to one dimension.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)
|
||||
wraps a VFS to offer encryption at rest.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/memdb`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb)
|
||||
implements an in-memory VFS.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/readervfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs)
|
||||
implements a VFS for immutable databases.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)
|
||||
wraps a VFS to offer encryption at rest.
|
||||
|
||||
### Advanced features
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ func (c *conn) Raw() *sqlite3.Conn {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
// Deprecated: use BeginTx instead.
|
||||
func (c *conn) Begin() (driver.Tx, error) {
|
||||
return c.BeginTx(context.Background(), driver.TxOptions{})
|
||||
}
|
||||
@@ -301,7 +302,7 @@ func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, e
|
||||
s.Close()
|
||||
return nil, util.TailErr
|
||||
}
|
||||
return &stmt{Stmt: s, tmRead: c.tmRead, tmWrite: c.tmWrite}, nil
|
||||
return &stmt{Stmt: s, tmRead: c.tmRead, tmWrite: c.tmWrite, inputs: -2}, nil
|
||||
}
|
||||
|
||||
func (c *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||
@@ -335,6 +336,7 @@ type stmt struct {
|
||||
*sqlite3.Stmt
|
||||
tmWrite sqlite3.TimeFormat
|
||||
tmRead sqlite3.TimeFormat
|
||||
inputs int
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -345,12 +347,17 @@ var (
|
||||
)
|
||||
|
||||
func (s *stmt) NumInput() int {
|
||||
if s.inputs >= -1 {
|
||||
return s.inputs
|
||||
}
|
||||
n := s.Stmt.BindCount()
|
||||
for i := 1; i <= n; i++ {
|
||||
if s.Stmt.BindName(i) != "" {
|
||||
s.inputs = -1
|
||||
return -1
|
||||
}
|
||||
}
|
||||
s.inputs = n
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -389,12 +396,7 @@ func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driv
|
||||
return &rows{ctx: ctx, stmt: s}, nil
|
||||
}
|
||||
|
||||
func (s *stmt) setupBindings(args []driver.NamedValue) error {
|
||||
err := s.Stmt.ClearBindings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
|
||||
var ids [3]int
|
||||
for _, arg := range args {
|
||||
ids := ids[:0]
|
||||
@@ -558,19 +560,20 @@ func (r *rows) Next(dest []driver.Value) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rows) decodeTime(i int, v any) (_ time.Time, _ bool) {
|
||||
func (r *rows) decodeTime(i int, v any) (_ time.Time, ok bool) {
|
||||
if r.tmRead == sqlite3.TimeFormatDefault {
|
||||
return
|
||||
}
|
||||
switch r.declType(i) {
|
||||
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
|
||||
// maybe
|
||||
default:
|
||||
// handled by maybeTime
|
||||
return
|
||||
}
|
||||
switch v.(type) {
|
||||
case int64, float64, string:
|
||||
// maybe
|
||||
// could be a time value
|
||||
default:
|
||||
return
|
||||
}
|
||||
switch r.declType(i) {
|
||||
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
|
||||
// could be a time value
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,4 +24,7 @@ See the [configuration options](../sqlite3/sqlite_cfg.h),
|
||||
and [patches](../sqlite3) applied.
|
||||
|
||||
Built using [`wasi-sdk`](https://github.com/WebAssembly/wasi-sdk),
|
||||
and [`binaryen`](https://github.com/WebAssembly/binaryen).
|
||||
and [`binaryen`](https://github.com/WebAssembly/binaryen).
|
||||
|
||||
The build is easily reproducible, and verifiable, using
|
||||
[Artifact Attestations](https://github.com/ncruces/go-sqlite3/attestations).
|
||||
Binary file not shown.
@@ -16,7 +16,7 @@ import (
|
||||
// ints, floats, bools, strings or byte slices,
|
||||
// using [sqlite3.BindPointer] or [sqlite3.Pointer].
|
||||
func Register(db *sqlite3.Conn) {
|
||||
sqlite3.CreateModule[array](db, "array", nil,
|
||||
sqlite3.CreateModule(db, "array", nil,
|
||||
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (array, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(value, array HIDDEN)`)
|
||||
return array{}, err
|
||||
|
||||
340
ext/bloom/bloom.go
Normal file
340
ext/bloom/bloom.go
Normal file
@@ -0,0 +1,340 @@
|
||||
// Package bloom provides a Bloom filter virtual table.
|
||||
//
|
||||
// A Bloom filter is a space-efficient probabilistic data structure
|
||||
// used to test whether an element is a member of a set.
|
||||
//
|
||||
// https://github.com/nalgeon/sqlean/issues/27#issuecomment-1002267134
|
||||
package bloom
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/dchest/siphash"
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Register registers the bloom_filter virtual table:
|
||||
//
|
||||
// CREATE VIRTUAL TABLE foo USING bloom_filter(nElements, falseProb, kHashes)
|
||||
func Register(db *sqlite3.Conn) {
|
||||
sqlite3.CreateModule(db, "bloom_filter", create, connect)
|
||||
}
|
||||
|
||||
type bloom struct {
|
||||
db *sqlite3.Conn
|
||||
schema string
|
||||
storage string
|
||||
prob float64
|
||||
bytes int64
|
||||
hashes int
|
||||
}
|
||||
|
||||
func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom, err error) {
|
||||
t := bloom{
|
||||
db: db,
|
||||
schema: schema,
|
||||
storage: table + "_storage",
|
||||
}
|
||||
|
||||
var nelem int64
|
||||
if len(arg) > 0 {
|
||||
nelem, err = strconv.ParseInt(arg[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nelem <= 0 {
|
||||
return nil, errors.New("bloom: number of elements in filter must be positive")
|
||||
}
|
||||
} else {
|
||||
nelem = 100
|
||||
}
|
||||
|
||||
if len(arg) > 1 {
|
||||
t.prob, err = strconv.ParseFloat(arg[1], 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.prob <= 0 || t.prob >= 1 {
|
||||
return nil, errors.New("bloom: probability must be in the range (0,1)")
|
||||
}
|
||||
} else {
|
||||
t.prob = 0.01
|
||||
}
|
||||
|
||||
if len(arg) > 2 {
|
||||
t.hashes, err = strconv.Atoi(arg[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.hashes <= 0 {
|
||||
return nil, errors.New("bloom: number of hash functions must be positive")
|
||||
}
|
||||
} else {
|
||||
t.hashes = max(1, numHashes(t.prob))
|
||||
}
|
||||
|
||||
t.bytes = numBytes(nelem, t.prob)
|
||||
|
||||
err = db.Exec(fmt.Sprintf(
|
||||
`CREATE TABLE %s.%s (data BLOB, p REAL, n INTEGER, m INTEGER, k INTEGER)`,
|
||||
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := db.LastInsertRowID()
|
||||
defer db.SetLastInsertRowID(id)
|
||||
|
||||
err = db.Exec(fmt.Sprintf(
|
||||
`INSERT INTO %s.%s (rowid, data, p, n, m, k)
|
||||
VALUES (1, zeroblob(%d), %f, %d, %d, %d)`,
|
||||
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage),
|
||||
t.bytes, t.prob, nelem, 8*t.bytes, t.hashes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.DeclareVTab(
|
||||
`CREATE TABLE x(present, word HIDDEN NOT NULL PRIMARY KEY) WITHOUT ROWID`)
|
||||
if err != nil {
|
||||
t.Destroy()
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func connect(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom, err error) {
|
||||
t := bloom{
|
||||
db: db,
|
||||
schema: schema,
|
||||
storage: table + "_storage",
|
||||
}
|
||||
|
||||
err = db.DeclareVTab(
|
||||
`CREATE TABLE x(present, word HIDDEN NOT NULL PRIMARY KEY) WITHOUT ROWID`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
load, _, err := db.Prepare(fmt.Sprintf(
|
||||
`SELECT m/8, p, k FROM %s.%s WHERE rowid = 1`,
|
||||
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer load.Close()
|
||||
|
||||
if !load.Step() {
|
||||
if err = load.Err(); err == nil {
|
||||
err = sqlite3.CORRUPT_VTAB
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.bytes = load.ColumnInt64(0)
|
||||
t.prob = load.ColumnFloat(1)
|
||||
t.hashes = load.ColumnInt(2)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (b *bloom) Destroy() error {
|
||||
return b.db.Exec(fmt.Sprintf(`DROP TABLE %s.%s`,
|
||||
sqlite3.QuoteIdentifier(b.schema),
|
||||
sqlite3.QuoteIdentifier(b.storage)))
|
||||
}
|
||||
|
||||
func (b *bloom) Rename(new string) error {
|
||||
new += "_storage"
|
||||
err := b.db.Exec(fmt.Sprintf(`ALTER TABLE %s.%s RENAME TO %s`,
|
||||
sqlite3.QuoteIdentifier(b.schema),
|
||||
sqlite3.QuoteIdentifier(b.storage),
|
||||
sqlite3.QuoteIdentifier(new),
|
||||
))
|
||||
if err == nil {
|
||||
b.storage = new
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *bloom) ShadowTables() {}
|
||||
|
||||
func (t *bloom) Integrity(schema, table string, flags int) error {
|
||||
load, _, err := t.db.Prepare(fmt.Sprintf(
|
||||
`SELECT typeof(data), length(data), p, n, m, k FROM %s.%s WHERE rowid = 1`,
|
||||
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("bloom: %v", err) // can't wrap!
|
||||
}
|
||||
defer load.Close()
|
||||
|
||||
err = errors.New("bloom: invalid parameters")
|
||||
if !load.Step() {
|
||||
return err
|
||||
}
|
||||
if t := load.ColumnText(0); t != "blob" {
|
||||
return err
|
||||
}
|
||||
if m := load.ColumnInt64(4); m <= 0 || m%8 != 0 {
|
||||
return err
|
||||
} else if load.ColumnInt64(1) != m/8 {
|
||||
return err
|
||||
}
|
||||
if p := load.ColumnFloat(2); p <= 0 || p >= 1 {
|
||||
return err
|
||||
}
|
||||
if n := load.ColumnInt64(3); n <= 0 {
|
||||
return err
|
||||
}
|
||||
if k := load.ColumnInt(5); k <= 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bloom) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
for n, cst := range idx.Constraint {
|
||||
if cst.Usable && cst.Column == 1 &&
|
||||
cst.Op == sqlite3.INDEX_CONSTRAINT_EQ {
|
||||
idx.ConstraintUsage[n].ArgvIndex = 1
|
||||
idx.OrderByConsumed = true
|
||||
idx.EstimatedRows = 1
|
||||
idx.EstimatedCost = float64(b.hashes)
|
||||
idx.IdxFlags = sqlite3.INDEX_SCAN_UNIQUE
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
|
||||
func (b *bloom) Update(arg ...sqlite3.Value) (rowid int64, err error) {
|
||||
if arg[0].Type() != sqlite3.NULL {
|
||||
if len(arg) == 1 {
|
||||
return 0, errors.New("bloom: elements cannot be deleted")
|
||||
}
|
||||
return 0, errors.New("bloom: elements cannot be updated")
|
||||
}
|
||||
|
||||
blob := arg[2].RawBlob()
|
||||
|
||||
f, err := b.db.OpenBlob(b.schema, b.storage, "data", 1, true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
for n := 0; n < b.hashes; n++ {
|
||||
hash := calcHash(n, blob)
|
||||
hash %= uint64(b.bytes * 8)
|
||||
bitpos := byte(hash % 8)
|
||||
bytepos := int64(hash / 8)
|
||||
|
||||
var buf [1]byte
|
||||
_, err = f.Seek(bytepos, io.SeekStart)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, err = f.Read(buf[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
buf[0] |= 1 << bitpos
|
||||
|
||||
_, err = f.Seek(bytepos, io.SeekStart)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, err = f.Write(buf[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (b *bloom) Open() (sqlite3.VTabCursor, error) {
|
||||
return &cursor{bloom: b}, nil
|
||||
}
|
||||
|
||||
type cursor struct {
|
||||
*bloom
|
||||
eof bool
|
||||
arg *sqlite3.Value
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
if len(arg) != 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.eof = false
|
||||
c.arg = &arg[0]
|
||||
blob := arg[0].RawBlob()
|
||||
|
||||
f, err := c.db.OpenBlob(c.schema, c.storage, "data", 1, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
for n := 0; n < c.hashes && !c.eof; n++ {
|
||||
hash := calcHash(n, blob)
|
||||
hash %= uint64(c.bytes * 8)
|
||||
bitpos := byte(hash % 8)
|
||||
bytepos := int64(hash / 8)
|
||||
|
||||
var buf [1]byte
|
||||
_, err = f.Seek(bytepos, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = f.Read(buf[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.eof = buf[0]&(1<<bitpos) == 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
|
||||
switch n {
|
||||
case 0:
|
||||
ctx.ResultBool(true)
|
||||
case 1:
|
||||
ctx.ResultValue(*c.arg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) Next() error {
|
||||
c.eof = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) EOF() bool {
|
||||
return c.eof
|
||||
}
|
||||
|
||||
func (c *cursor) RowID() (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func calcHash(k int, b []byte) uint64 {
|
||||
return siphash.Hash(^uint64(k), uint64(k), b)
|
||||
}
|
||||
|
||||
func numHashes(p float64) int {
|
||||
k := math.Round(-math.Log2(p))
|
||||
return max(1, int(k))
|
||||
}
|
||||
|
||||
func numBytes(n int64, p float64) int64 {
|
||||
m := math.Ceil(float64(n) * math.Log(p) / -(math.Ln2 * math.Ln2))
|
||||
return (int64(m) + 7) / 8
|
||||
}
|
||||
140
ext/bloom/bloom_test.go
Normal file
140
ext/bloom/bloom_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package bloom_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/bloom"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
bloom.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE sports_cars USING bloom_filter(20);
|
||||
INSERT INTO sports_cars VALUES ('ferrari'), ('lamborghini'), ('alfa romeo')
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
query, _, err := db.Prepare(`SELECT COUNT(*) FROM sports_cars(?)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = query.BindText(1, "ferrari")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !query.Step() {
|
||||
t.Error("no rows")
|
||||
}
|
||||
if !query.ColumnBool(0) {
|
||||
t.Error("want true")
|
||||
}
|
||||
err = query.Reset()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = query.BindText(1, "bmw")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !query.Step() {
|
||||
t.Error("no rows")
|
||||
}
|
||||
if query.ColumnBool(0) {
|
||||
t.Error("want false")
|
||||
}
|
||||
err = query.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`DROP TABLE sports_cars`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed testdata/bloom.db
|
||||
var testDB []byte
|
||||
|
||||
func Test_compatible(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmp := filepath.Join(t.TempDir(), "bloom.db")
|
||||
err := os.WriteFile(tmp, testDB, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
bloom.Register(db)
|
||||
|
||||
query, _, err := db.Prepare(`SELECT COUNT(*) FROM plants(?)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer query.Close()
|
||||
|
||||
err = query.BindText(1, "apple")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !query.Step() {
|
||||
t.Error("no rows")
|
||||
}
|
||||
if !query.ColumnBool(0) {
|
||||
t.Error("want true")
|
||||
}
|
||||
err = query.Reset()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = query.BindText(1, "lemon")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !query.Step() {
|
||||
t.Error("no rows")
|
||||
}
|
||||
if query.ColumnBool(0) {
|
||||
t.Error("want false")
|
||||
}
|
||||
err = query.Reset()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`PRAGMA integrity_check`)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`PRAGMA quick_check`)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
BIN
ext/bloom/testdata/bloom.db
vendored
Normal file
BIN
ext/bloom/testdata/bloom.db
vendored
Normal file
Binary file not shown.
@@ -40,6 +40,8 @@ func Test_uintArg(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_boolArg(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
arg string
|
||||
key string
|
||||
@@ -76,6 +78,8 @@ func Test_boolArg(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_runeArg(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
arg string
|
||||
key string
|
||||
|
||||
@@ -9,9 +9,11 @@ package csv
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
@@ -36,6 +38,7 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
header bool
|
||||
columns int = -1
|
||||
comma rune = ','
|
||||
comment rune
|
||||
|
||||
done = map[string]struct{}{}
|
||||
)
|
||||
@@ -58,6 +61,8 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
columns, err = uintArg(key, val)
|
||||
case "comma":
|
||||
comma, err = runeArg(key, val)
|
||||
case "comment":
|
||||
comment, err = runeArg(key, val)
|
||||
default:
|
||||
return nil, fmt.Errorf("csv: unknown %q parameter", key)
|
||||
}
|
||||
@@ -68,15 +73,16 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
}
|
||||
|
||||
if (filename == "") == (data == "") {
|
||||
return nil, fmt.Errorf(`csv: must specify either "filename" or "data" but not both`)
|
||||
return nil, errors.New(`csv: must specify either "filename" or "data" but not both`)
|
||||
}
|
||||
|
||||
table := &table{
|
||||
fsys: fsys,
|
||||
name: filename,
|
||||
data: data,
|
||||
comma: comma,
|
||||
header: header,
|
||||
fsys: fsys,
|
||||
name: filename,
|
||||
data: data,
|
||||
comma: comma,
|
||||
comment: comment,
|
||||
header: header,
|
||||
}
|
||||
|
||||
if schema == "" {
|
||||
@@ -93,6 +99,12 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
}
|
||||
}
|
||||
schema = getSchema(header, columns, row)
|
||||
} else {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
table.typs, err = getColumnAffinities(schema)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
err = db.DeclareVTab(schema)
|
||||
@@ -110,11 +122,13 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
}
|
||||
|
||||
type table struct {
|
||||
fsys fs.FS
|
||||
name string
|
||||
data string
|
||||
comma rune
|
||||
header bool
|
||||
fsys fs.FS
|
||||
name string
|
||||
data string
|
||||
typs []affinity
|
||||
comma rune
|
||||
comment rune
|
||||
header bool
|
||||
}
|
||||
|
||||
func (t *table) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
@@ -171,6 +185,7 @@ func (t *table) newReader() (*csv.Reader, io.Closer, error) {
|
||||
csv := csv.NewReader(r)
|
||||
csv.ReuseRecord = true
|
||||
csv.Comma = t.comma
|
||||
csv.Comment = t.comment
|
||||
return csv, c, nil
|
||||
}
|
||||
|
||||
@@ -226,7 +241,36 @@ func (c *cursor) RowID() (int64, error) {
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
|
||||
if col < len(c.row) {
|
||||
ctx.ResultText(c.row[col])
|
||||
typ := text
|
||||
if col < len(c.table.typs) {
|
||||
typ = c.table.typs[col]
|
||||
}
|
||||
|
||||
txt := c.row[col]
|
||||
if txt == "" && typ != text {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case numeric, integer:
|
||||
if strings.TrimLeft(txt, "+-0123456789") == "" {
|
||||
if i, err := strconv.ParseInt(txt, 10, 64); err == nil {
|
||||
ctx.ResultInt64(i)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
case real:
|
||||
if strings.TrimLeft(txt, "+-.0123456789Ee") == "" {
|
||||
if f, err := strconv.ParseFloat(txt, 64); err == nil {
|
||||
ctx.ResultFloat(f)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
}
|
||||
ctx.ResultText(txt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -63,14 +63,16 @@ func TestRegister(t *testing.T) {
|
||||
csv.Register(db)
|
||||
|
||||
const data = `
|
||||
# Comment
|
||||
"Rob" "Pike" rob
|
||||
"Ken" Thompson ken
|
||||
Robert "Griesemer" "gri"`
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE temp.users USING csv(
|
||||
data = ` + sqlite3.Quote(data) + `,
|
||||
schema = 'CREATE TABLE x(first_name, last_name, username)',
|
||||
comma = '\t'
|
||||
data = ` + sqlite3.Quote(data) + `,
|
||||
schema = 'CREATE TABLE x(first_name, last_name, username)',
|
||||
comma = '\t',
|
||||
comment = '#'
|
||||
)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -113,6 +115,50 @@ Robert "Griesemer" "gri"`
|
||||
}
|
||||
}
|
||||
|
||||
func TestAffinity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
csv.Register(db)
|
||||
|
||||
const data = "01\n0.10\ne"
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE temp.nums USING csv(
|
||||
data = ` + sqlite3.Quote(data) + `,
|
||||
schema = 'CREATE TABLE x(a numeric)'
|
||||
)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT * FROM temp.nums`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnText(0); got != "1" {
|
||||
t.Errorf("got %q want 1", got)
|
||||
}
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnText(0); got != "0.1" {
|
||||
t.Errorf("got %q want 0.1", got)
|
||||
}
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnText(0); got != "e" {
|
||||
t.Errorf("got %q want e", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
54
ext/csv/types.go
Normal file
54
ext/csv/types.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/util/vtabutil"
|
||||
)
|
||||
|
||||
type affinity byte
|
||||
|
||||
const (
|
||||
blob affinity = 0
|
||||
text affinity = 1
|
||||
numeric affinity = 2
|
||||
integer affinity = 3
|
||||
real affinity = 4
|
||||
)
|
||||
|
||||
func getColumnAffinities(schema string) ([]affinity, error) {
|
||||
tab, err := vtabutil.Parse(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tab.Close()
|
||||
|
||||
types := make([]affinity, tab.NumColumns())
|
||||
for i := range types {
|
||||
col := tab.Column(i)
|
||||
types[i] = getAffinity(col.Type())
|
||||
}
|
||||
return types, nil
|
||||
}
|
||||
|
||||
func getAffinity(declType string) affinity {
|
||||
// https://sqlite.org/datatype3.html#determination_of_column_affinity
|
||||
if declType == "" {
|
||||
return blob
|
||||
}
|
||||
name := strings.ToUpper(declType)
|
||||
if strings.Contains(name, "INT") {
|
||||
return integer
|
||||
}
|
||||
if strings.Contains(name, "CHAR") || strings.Contains(name, "CLOB") || strings.Contains(name, "TEXT") {
|
||||
return text
|
||||
}
|
||||
if strings.Contains(name, "BLOB") {
|
||||
return blob
|
||||
}
|
||||
if strings.Contains(name, "REAL") || strings.Contains(name, "FLOA") || strings.Contains(name, "DOUB") {
|
||||
return real
|
||||
}
|
||||
return numeric
|
||||
}
|
||||
35
ext/csv/types_test.go
Normal file
35
ext/csv/types_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_getAffinity(t *testing.T) {
|
||||
tests := []struct {
|
||||
decl string
|
||||
want affinity
|
||||
}{
|
||||
{"", blob},
|
||||
{"INTEGER", integer},
|
||||
{"TINYINT", integer},
|
||||
{"TEXT", text},
|
||||
{"CHAR", text},
|
||||
{"CLOB", text},
|
||||
{"BLOB", blob},
|
||||
{"REAL", real},
|
||||
{"FLOAT", real},
|
||||
{"DOUBLE", real},
|
||||
{"NUMERIC", numeric},
|
||||
{"DECIMAL", numeric},
|
||||
{"BOOLEAN", numeric},
|
||||
{"DATETIME", numeric},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.decl, func(t *testing.T) {
|
||||
if got := getAffinity(tt.decl); got != tt.want {
|
||||
t.Errorf("getAffinity() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -34,13 +34,13 @@ func Register(db *sqlite3.Conn) {
|
||||
// The lines_read function reads from a file or an [io.Reader].
|
||||
// If a filename is specified, fsys is used to open the file.
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
sqlite3.CreateModule[lines](db, "lines", nil,
|
||||
sqlite3.CreateModule(db, "lines", nil,
|
||||
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_INNOCUOUS)
|
||||
return lines{}, err
|
||||
})
|
||||
sqlite3.CreateModule[lines](db, "lines_read", nil,
|
||||
sqlite3.CreateModule(db, "lines_read", nil,
|
||||
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
|
||||
@@ -65,7 +65,7 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
|
||||
}
|
||||
|
||||
if stmt.ColumnCount() != 2 {
|
||||
return nil, fmt.Errorf("pivot: column definition query expects 2 result columns")
|
||||
return nil, errors.New("pivot: column definition query expects 2 result columns")
|
||||
}
|
||||
for stmt.Step() {
|
||||
name := sqlite3.QuoteIdentifier(stmt.ColumnText(1))
|
||||
@@ -83,7 +83,7 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
|
||||
}
|
||||
|
||||
if stmt.ColumnCount() != 1 {
|
||||
return nil, fmt.Errorf("pivot: cell query expects 1 result columns")
|
||||
return nil, errors.New("pivot: cell query expects 1 result columns")
|
||||
}
|
||||
if stmt.BindCount() != len(table.keys)+1 {
|
||||
return nil, fmt.Errorf("pivot: cell query expects %d bound parameters", len(table.keys)+1)
|
||||
|
||||
@@ -8,7 +8,7 @@ package statement
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
@@ -29,7 +29,7 @@ type table struct {
|
||||
|
||||
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (*table, error) {
|
||||
if len(arg) != 1 {
|
||||
return nil, fmt.Errorf("statement: wrong number of arguments")
|
||||
return nil, errors.New("statement: wrong number of arguments")
|
||||
}
|
||||
|
||||
sql := "SELECT * FROM\n" + arg[0]
|
||||
|
||||
6
go.mod
6
go.mod
@@ -3,9 +3,11 @@ module github.com/ncruces/go-sqlite3
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/dchest/siphash v1.2.3
|
||||
github.com/ncruces/julianday v1.0.0
|
||||
github.com/ncruces/sort v0.1.2
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/tetratelabs/wazero v1.7.2
|
||||
github.com/tetratelabs/wazero v1.7.3
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/sys v0.21.0
|
||||
@@ -13,6 +15,4 @@ require (
|
||||
lukechampine.com/adiantum v1.1.1
|
||||
)
|
||||
|
||||
require github.com/ncruces/sort v0.1.2
|
||||
|
||||
retract v0.4.0 // tagged from the wrong branch
|
||||
|
||||
6
go.sum
6
go.sum
@@ -1,11 +1,13 @@
|
||||
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
|
||||
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/ncruces/sort v0.1.2 h1:zKQ9CA4fpHPF6xsUhRTfi5EEryspuBpe/QA4VWQOV1U=
|
||||
github.com/ncruces/sort v0.1.2/go.mod h1:vEJUTBJtebIuCMmXD18GKo5GJGhsay+xZFOoBEIXFmE=
|
||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
|
||||
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
|
||||
@@ -3,9 +3,11 @@ set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/ddlmod.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/ddlmod_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/error_translator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/migrator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/sqlite.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/sqlite_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/ddlmod.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/ddlmod_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/error_translator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/migrator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/sqlite.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/sqlite_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/sqlite_test.go"
|
||||
curl -#L "https://github.com/glebarez/sqlite/raw/v1.11.0/sqlite_error_translator_test.go" > error_translator_test.go
|
||||
48
gormlite/error_translator_test.go
Normal file
48
gormlite/error_translator_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func TestErrorTranslator(t *testing.T) {
|
||||
// This is the DSN of the in-memory SQLite database for these tests.
|
||||
const InMemoryDSN = "file:testdatabase?mode=memory&cache=shared"
|
||||
|
||||
// This is the example object for testing the unique constraint error
|
||||
type Article struct {
|
||||
ArticleNumber string `gorm:"unique"`
|
||||
}
|
||||
|
||||
db, err := gorm.Open(Open(InMemoryDSN), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
TranslateError: true})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected Open to succeed; got error: %v", err)
|
||||
}
|
||||
if db == nil {
|
||||
t.Errorf("Expected db to be non-nil.")
|
||||
}
|
||||
|
||||
err = db.AutoMigrate(&Article{})
|
||||
if err != nil {
|
||||
t.Errorf("Expected to migrate database models to succeed: %v", err)
|
||||
}
|
||||
|
||||
err = db.Create(&Article{ArticleNumber: "A00000XX"}).Error
|
||||
if err != nil {
|
||||
t.Errorf("Expected first create to succeed: %v", err)
|
||||
}
|
||||
|
||||
err = db.Create(&Article{ArticleNumber: "A00000XX"}).Error
|
||||
if err == nil {
|
||||
t.Errorf("Expected second create to fail.")
|
||||
}
|
||||
|
||||
if err != gorm.ErrDuplicatedKey {
|
||||
t.Errorf("Expected error from second create to be gorm.ErrDuplicatedKey: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ module github.com/ncruces/go-sqlite3/gormlite
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/ncruces/go-sqlite3 v0.16.1
|
||||
github.com/ncruces/go-sqlite3 v0.16.3
|
||||
gorm.io/gorm v1.25.10
|
||||
)
|
||||
|
||||
@@ -11,6 +11,6 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.3 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,12 +2,12 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/ncruces/go-sqlite3 v0.16.1 h1:1wHv7s8y+fWK44UIliotJ42ZV41A5T0sjIAqGmnMrkc=
|
||||
github.com/ncruces/go-sqlite3 v0.16.1/go.mod h1:feFXbBcbLtxNk6XWG1ROt8MS9+E45yCW3G8o4ixIqZ8=
|
||||
github.com/ncruces/go-sqlite3 v0.16.3 h1:Ky0denOdmAGOoCE6lQlw6GCJNMD8gTikNWe8rpu+Gjc=
|
||||
github.com/ncruces/go-sqlite3 v0.16.3/go.mod h1:sAU/vQwBmZ2hq5BlW/KTzqRFizL43bv2JQoBLgXhcMI=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
|
||||
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
|
||||
@@ -26,7 +26,7 @@ func (j JSON) Scan(value any) error {
|
||||
buf = v.AppendFormat(buf, time.RFC3339Nano)
|
||||
buf = append(buf, '"')
|
||||
case nil:
|
||||
buf = append(buf, "null"...)
|
||||
buf = []byte("null")
|
||||
default:
|
||||
panic(AssertErr())
|
||||
}
|
||||
|
||||
3
json.go
3
json.go
@@ -5,7 +5,8 @@ import "github.com/ncruces/go-sqlite3/internal/util"
|
||||
// JSON returns a value that can be used as an argument to
|
||||
// [database/sql.DB.Exec], [database/sql.Row.Scan] and similar methods to
|
||||
// store value as JSON, or decode JSON into value.
|
||||
// JSON should NOT be used with [BindJSON] or [ResultJSON].
|
||||
// JSON should NOT be used with [Stmt.BindJSON], [Stmt.ColumnJSON],
|
||||
// [Value.JSON], or [Context.ResultJSON].
|
||||
func JSON(value any) any {
|
||||
return util.JSON{Value: value}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import "github.com/ncruces/go-sqlite3/internal/util"
|
||||
|
||||
// Pointer returns a pointer to a value that can be used as an argument to
|
||||
// [database/sql.DB.Exec] and similar methods.
|
||||
// Pointer should NOT be used with [BindPointer] or [ResultPointer].
|
||||
// Pointer should NOT be used with [Stmt.BindPointer],
|
||||
// [Value.Pointer], or [Context.ResultPointer].
|
||||
//
|
||||
// https://sqlite.org/bindptr.html
|
||||
func Pointer[T any](value T) any {
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "include.h"
|
||||
#include "sqlite3.h"
|
||||
|
||||
#define SQLITE_VTAB_CREATOR_GO /******/ 0x01
|
||||
#define SQLITE_VTAB_DESTROYER_GO /****/ 0x02
|
||||
#define SQLITE_VTAB_UPDATER_GO /******/ 0x04
|
||||
#define SQLITE_VTAB_RENAMER_GO /******/ 0x08
|
||||
#define SQLITE_VTAB_OVERLOADER_GO /***/ 0x10
|
||||
#define SQLITE_VTAB_CHECKER_GO /******/ 0x20
|
||||
#define SQLITE_VTAB_TXN_GO /**********/ 0x40
|
||||
#define SQLITE_VTAB_SAVEPOINTER_GO /**/ 0x80
|
||||
#define SQLITE_VTAB_CREATOR_GO /******/ 0x001
|
||||
#define SQLITE_VTAB_DESTROYER_GO /****/ 0x002
|
||||
#define SQLITE_VTAB_UPDATER_GO /******/ 0x004
|
||||
#define SQLITE_VTAB_RENAMER_GO /******/ 0x008
|
||||
#define SQLITE_VTAB_OVERLOADER_GO /***/ 0x010
|
||||
#define SQLITE_VTAB_CHECKER_GO /******/ 0x020
|
||||
#define SQLITE_VTAB_TXN_GO /**********/ 0x040
|
||||
#define SQLITE_VTAB_SAVEPOINTER_GO /**/ 0x080
|
||||
#define SQLITE_VTAB_SHADOWTABS_GO /***/ 0x100
|
||||
|
||||
int go_vtab_create(sqlite3_module *, int argc, const char *const *argv,
|
||||
sqlite3_vtab **, char **pzErr);
|
||||
@@ -157,6 +159,8 @@ static int go_vtab_integrity_wrapper(sqlite3_vtab *pVTab, const char *zSchema,
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int go_vtab_shadown_name_wrapper(const char *zName) { return true; }
|
||||
|
||||
int sqlite3_create_module_go(sqlite3 *db, const char *zName, int flags,
|
||||
go_handle handle) {
|
||||
struct go_module *mod = malloc(sizeof(struct go_module));
|
||||
@@ -208,6 +212,9 @@ int sqlite3_create_module_go(sqlite3 *db, const char *zName, int flags,
|
||||
mod->base.xRelease = go_vtab_release;
|
||||
mod->base.xRollbackTo = go_vtab_rollback_to;
|
||||
}
|
||||
if (flags & SQLITE_VTAB_SHADOWTABS_GO) {
|
||||
mod->base.xShadowName = go_vtab_shadown_name_wrapper;
|
||||
}
|
||||
if (mod->base.xCreate && !mod->base.xDestroy) {
|
||||
mod->base.xDestroy = mod->base.xDisconnect;
|
||||
}
|
||||
|
||||
2
stmt.go
2
stmt.go
@@ -564,7 +564,7 @@ func (s *Stmt) ColumnJSON(col int, ptr any) error {
|
||||
var data []byte
|
||||
switch s.ColumnType(col) {
|
||||
case NULL:
|
||||
data = append(data, "null"...)
|
||||
data = []byte("null")
|
||||
case TEXT:
|
||||
data = s.ColumnRawText(col)
|
||||
case BLOB:
|
||||
|
||||
@@ -61,7 +61,6 @@ func Test_memdb(t *testing.T) {
|
||||
iter = 5000
|
||||
}
|
||||
|
||||
memdb.Delete("test.db")
|
||||
memdb.Create("test.db", nil)
|
||||
name := "file:/test.db?vfs=memdb"
|
||||
testParallel(t, name, iter)
|
||||
@@ -142,11 +141,42 @@ func TestChildProcess(t *testing.T) {
|
||||
testParallel(t, name, 1000)
|
||||
}
|
||||
|
||||
func Benchmark_parallel(b *testing.B) {
|
||||
if !vfs.SupportsSharedMemory {
|
||||
b.Skip("skipping without shared memory")
|
||||
}
|
||||
|
||||
sqlite3.Initialize()
|
||||
b.ResetTimer()
|
||||
|
||||
name := "file:" +
|
||||
filepath.Join(b.TempDir(), "test.db") +
|
||||
"?_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(truncate)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
testParallel(b, name, b.N)
|
||||
}
|
||||
|
||||
func Benchmark_wal(b *testing.B) {
|
||||
if !vfs.SupportsSharedMemory {
|
||||
b.Skip("skipping without shared memory")
|
||||
}
|
||||
|
||||
sqlite3.Initialize()
|
||||
b.ResetTimer()
|
||||
|
||||
name := "file:" +
|
||||
filepath.Join(b.TempDir(), "test.db") +
|
||||
"?_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(wal)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
testParallel(b, name, b.N)
|
||||
}
|
||||
|
||||
func Benchmark_memdb(b *testing.B) {
|
||||
sqlite3.Initialize()
|
||||
b.ResetTimer()
|
||||
|
||||
memdb.Delete("test.db")
|
||||
memdb.Create("test.db", nil)
|
||||
name := "file:/test.db?vfs=memdb"
|
||||
testParallel(b, name, b.N)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
@@ -52,26 +52,55 @@ func TestWAL_readonly(t *testing.T) {
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
tmp := filepath.Join(t.TempDir(), "test.db")
|
||||
err := os.WriteFile(tmp, walDB, 0666)
|
||||
tmp := filepath.ToSlash(filepath.Join(t.TempDir(), "test.db"))
|
||||
|
||||
db1, err := driver.Open("file:"+tmp+"?_pragma=journal_mode(wal)&_txlock=immediate", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db1.Close()
|
||||
|
||||
db2, err := driver.Open("file:"+tmp+"?_pragma=journal_mode(wal)&mode=ro", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db2.Close()
|
||||
|
||||
// Create the table using the first (writable) connection.
|
||||
_, err = db1.Exec(`
|
||||
CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT);
|
||||
INSERT INTO t(name) VALUES('alice');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
db, err := sqlite3.OpenFlags(tmp, sqlite3.OPEN_READONLY)
|
||||
// Select the data using the second (readonly) connection.
|
||||
var name string
|
||||
err = db2.QueryRow("SELECT name FROM t").Scan(&name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if name != "alice" {
|
||||
t.Errorf("got %q want alice", name)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT * FROM sqlite_master`)
|
||||
// Update table.
|
||||
_, err = db1.Exec(`
|
||||
DELETE FROM t;
|
||||
INSERT INTO t(name) VALUES('bob');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
t.Error("want no rows")
|
||||
// Select the data using the second (readonly) connection.
|
||||
err = db2.QueryRow("SELECT name FROM t").Scan(&name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if name != "bob" {
|
||||
t.Errorf("got %q want bob", name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
util/vtabutil/README.md
Normal file
8
util/vtabutil/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Virtual Table utility functions
|
||||
|
||||
This package implements utilities mostly useful to virtual table implementations.
|
||||
|
||||
It also wraps a [parser](https://github.com/marcobambini/sqlite-createtable-parser)
|
||||
for the [`CREATE`](https://sqlite.org/lang_createtable.html) and
|
||||
[`ALTER TABLE`](https://sqlite.org/lang_altertable.html) commands,
|
||||
created by [Marco Bambini](https://github.com/marcobambini).
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package ioutil implements virtual table utility functions.
|
||||
package vtabutil
|
||||
|
||||
import "strings"
|
||||
|
||||
141
util/vtabutil/parse.go
Normal file
141
util/vtabutil/parse.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package vtabutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
const (
|
||||
_NONE = iota
|
||||
_MEMORY
|
||||
_SYNTAX
|
||||
_UNSUPPORTEDSQL
|
||||
|
||||
codeptr = 4
|
||||
baseptr = 8
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed parse/sql3parse_table.wasm
|
||||
binary []byte
|
||||
ctx context.Context
|
||||
once sync.Once
|
||||
runtime wazero.Runtime
|
||||
module wazero.CompiledModule
|
||||
)
|
||||
|
||||
// Table holds metadata about a table.
|
||||
type Table struct {
|
||||
mod api.Module
|
||||
ptr uint32
|
||||
sql string
|
||||
}
|
||||
|
||||
// Parse parses a [CREATE] or [ALTER TABLE] command.
|
||||
//
|
||||
// [CREATE]: https://sqlite.org/lang_createtable.html
|
||||
// [ALTER TABLE]: https://sqlite.org/lang_altertable.html
|
||||
func Parse(sql string) (_ *Table, err error) {
|
||||
once.Do(func() {
|
||||
ctx = context.Background()
|
||||
cfg := wazero.NewRuntimeConfigInterpreter().WithDebugInfoEnabled(false)
|
||||
runtime = wazero.NewRuntimeWithConfig(ctx, cfg)
|
||||
module, err = runtime.CompileModule(ctx, binary)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mod, err := runtime.InstantiateModule(ctx, module, wazero.NewModuleConfig().WithName(""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if buf, ok := mod.Memory().Read(baseptr, uint32(len(sql))); ok {
|
||||
copy(buf, sql)
|
||||
}
|
||||
r, err := mod.ExportedFunction("sql3parse_table").Call(ctx, baseptr, uint64(len(sql)), codeptr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, _ := mod.Memory().ReadUint32Le(codeptr)
|
||||
switch c {
|
||||
case _MEMORY:
|
||||
panic(util.OOMErr)
|
||||
case _SYNTAX:
|
||||
return nil, util.ErrorString("sql3parse: invalid syntax")
|
||||
case _UNSUPPORTEDSQL:
|
||||
return nil, util.ErrorString("sql3parse: unsupported SQL")
|
||||
}
|
||||
if r[0] == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &Table{
|
||||
sql: sql,
|
||||
mod: mod,
|
||||
ptr: uint32(r[0]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes a table handle.
|
||||
func (t *Table) Close() error {
|
||||
mod := t.mod
|
||||
t.mod = nil
|
||||
return mod.Close(ctx)
|
||||
}
|
||||
|
||||
// NumColumns returns the number of columns of the table.
|
||||
func (t *Table) NumColumns() int {
|
||||
r, err := t.mod.ExportedFunction("sql3table_num_columns").Call(ctx, uint64(t.ptr))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return int(int32(r[0]))
|
||||
}
|
||||
|
||||
// Column returns data for the ith column of the table.
|
||||
//
|
||||
// https://sqlite.org/lang_createtable.html#column_definitions
|
||||
func (t *Table) Column(i int) Column {
|
||||
r, err := t.mod.ExportedFunction("sql3table_get_column").Call(ctx, uint64(t.ptr), uint64(i))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return Column{
|
||||
tab: t,
|
||||
ptr: uint32(r[0]),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) string(ptr uint32) string {
|
||||
if ptr == 0 {
|
||||
return ""
|
||||
}
|
||||
off, _ := t.mod.Memory().ReadUint32Le(ptr + 0)
|
||||
len, _ := t.mod.Memory().ReadUint32Le(ptr + 4)
|
||||
return t.sql[off-baseptr : off+len-baseptr]
|
||||
}
|
||||
|
||||
// Column holds metadata about a column.
|
||||
type Column struct {
|
||||
tab *Table
|
||||
ptr uint32
|
||||
}
|
||||
|
||||
// Type returns the declared type of a column.
|
||||
//
|
||||
// https://sqlite.org/lang_createtable.html#column_data_types
|
||||
func (c Column) Type() string {
|
||||
r, err := c.tab.mod.ExportedFunction("sql3column_type").Call(ctx, uint64(c.ptr))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c.tab.string(uint32(r[0]))
|
||||
}
|
||||
2
util/vtabutil/parse/.gitignore
vendored
Normal file
2
util/vtabutil/parse/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
sql3parse_table.c
|
||||
sql3parse_table.h
|
||||
28
util/vtabutil/parse/build.sh
Executable file
28
util/vtabutil/parse/build.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
ROOT=../../../
|
||||
BINARYEN="$ROOT/tools/binaryen-version_117/bin"
|
||||
WASI_SDK="$ROOT/tools/wasi-sdk-22.0/bin"
|
||||
|
||||
"$WASI_SDK/clang" --target=wasm32-wasi -std=c17 -flto -g0 -Oz \
|
||||
-Wall -Wextra -Wno-unused-parameter -Wno-unused-function \
|
||||
-o sql3parse_table.wasm sql3parse_table.c \
|
||||
-mexec-model=reactor \
|
||||
-msimd128 -mmutable-globals -mmultivalue \
|
||||
-mbulk-memory -mreference-types \
|
||||
-mnontrapping-fptoint -msign-ext \
|
||||
-fno-stack-protector -fno-stack-clash-protection \
|
||||
-Wl,--stack-first \
|
||||
-Wl,--import-undefined \
|
||||
$(awk '{print "-Wl,--export="$0}' exports.txt)
|
||||
|
||||
trap 'rm -f sql3parse_table.tmp' EXIT
|
||||
"$BINARYEN/wasm-ctor-eval" -g -c _initialize sql3parse_table.wasm -o sql3parse_table.tmp
|
||||
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -Oz \
|
||||
sql3parse_table.tmp -o sql3parse_table.wasm \
|
||||
--enable-simd --enable-mutable-globals --enable-multivalue \
|
||||
--enable-bulk-memory --enable-reference-types \
|
||||
--enable-nontrapping-float-to-int --enable-sign-ext
|
||||
7
util/vtabutil/parse/download.sh
Executable file
7
util/vtabutil/parse/download.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
curl -#OL "https://github.com/ncruces/sqlite-createtable-parser/raw/master/sql3parse_table.c"
|
||||
curl -#OL "https://github.com/ncruces/sqlite-createtable-parser/raw/master/sql3parse_table.h"
|
||||
4
util/vtabutil/parse/exports.txt
Normal file
4
util/vtabutil/parse/exports.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
sql3parse_table
|
||||
sql3table_get_column
|
||||
sql3table_num_columns
|
||||
sql3column_type
|
||||
BIN
util/vtabutil/parse/sql3parse_table.wasm
Executable file
BIN
util/vtabutil/parse/sql3parse_table.wasm
Executable file
Binary file not shown.
2
util/vtabutil/vtabutil.go
Normal file
2
util/vtabutil/vtabutil.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package vtabutil implements virtual table utility functions.
|
||||
package vtabutil
|
||||
2
value.go
2
value.go
@@ -177,7 +177,7 @@ func (v Value) JSON(ptr any) error {
|
||||
var data []byte
|
||||
switch v.Type() {
|
||||
case NULL:
|
||||
data = append(data, "null"...)
|
||||
data = []byte("null")
|
||||
case TEXT:
|
||||
data = v.RawText()
|
||||
case BLOB:
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
# Go `"adiantum"` SQLite VFS
|
||||
# Go `adiantum` SQLite VFS
|
||||
|
||||
This package wraps an SQLite VFS to offer encryption at rest.
|
||||
|
||||
> [!WARNING]
|
||||
> This work was not certified by a cryptographer.
|
||||
> If you need vetted encryption, you should purchase the
|
||||
> [SQLite Encryption Extension](https://sqlite.org/see),
|
||||
> and either wrap it, or seek assistance wrapping it.
|
||||
|
||||
The `"adiantum"` VFS wraps the default SQLite VFS using the
|
||||
[Adiantum](https://github.com/lukechampine/adiantum)
|
||||
tweakable and length-preserving encryption.\
|
||||
|
||||
@@ -51,6 +51,7 @@ const (
|
||||
_IOERR_BEGIN_ATOMIC _ErrorCode = util.IOERR_BEGIN_ATOMIC
|
||||
_IOERR_COMMIT_ATOMIC _ErrorCode = util.IOERR_COMMIT_ATOMIC
|
||||
_IOERR_ROLLBACK_ATOMIC _ErrorCode = util.IOERR_ROLLBACK_ATOMIC
|
||||
_BUSY_SNAPSHOT _ErrorCode = util.BUSY_SNAPSHOT
|
||||
_CANTOPEN_FULLPATH _ErrorCode = util.CANTOPEN_FULLPATH
|
||||
_CANTOPEN_ISDIR _ErrorCode = util.CANTOPEN_ISDIR
|
||||
_READONLY_CANTINIT _ErrorCode = util.READONLY_CANTINIT
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Go `"memdb"` SQLite VFS
|
||||
# Go `memdb` SQLite VFS
|
||||
|
||||
This package implements the [`"memdb"`](https://sqlite.org/src/doc/tip/src/memdb.c)
|
||||
SQLite VFS in pure Go.
|
||||
|
||||
@@ -75,11 +75,6 @@ func (memVFS) FullPathname(name string) (string, error) {
|
||||
type memDB struct {
|
||||
name string
|
||||
|
||||
// +checklocks:lockMtx
|
||||
pending *memFile
|
||||
// +checklocks:lockMtx
|
||||
reserved *memFile
|
||||
|
||||
// +checklocks:dataMtx
|
||||
data []*[sectorSize]byte
|
||||
|
||||
@@ -88,6 +83,10 @@ type memDB struct {
|
||||
|
||||
// +checklocks:lockMtx
|
||||
shared int
|
||||
// +checklocks:lockMtx
|
||||
reserved bool
|
||||
// +checklocks:lockMtx
|
||||
pending bool
|
||||
|
||||
// +checklocks:memoryMtx
|
||||
refs int
|
||||
@@ -214,24 +213,24 @@ func (m *memFile) Lock(lock vfs.LockLevel) error {
|
||||
|
||||
switch lock {
|
||||
case vfs.LOCK_SHARED:
|
||||
if m.pending != nil {
|
||||
if m.pending {
|
||||
return sqlite3.BUSY
|
||||
}
|
||||
m.shared++
|
||||
|
||||
case vfs.LOCK_RESERVED:
|
||||
if m.reserved != nil {
|
||||
if m.reserved {
|
||||
return sqlite3.BUSY
|
||||
}
|
||||
m.reserved = m
|
||||
m.reserved = true
|
||||
|
||||
case vfs.LOCK_EXCLUSIVE:
|
||||
if m.lock < vfs.LOCK_PENDING {
|
||||
if m.pending != nil {
|
||||
if m.pending {
|
||||
return sqlite3.BUSY
|
||||
}
|
||||
m.lock = vfs.LOCK_PENDING
|
||||
m.pending = m
|
||||
m.pending = true
|
||||
}
|
||||
|
||||
for before := time.Now(); m.shared > 1; {
|
||||
@@ -256,11 +255,11 @@ func (m *memFile) Unlock(lock vfs.LockLevel) error {
|
||||
m.lockMtx.Lock()
|
||||
defer m.lockMtx.Unlock()
|
||||
|
||||
if m.pending == m {
|
||||
m.pending = nil
|
||||
if m.pending && m.lock >= vfs.LOCK_PENDING {
|
||||
m.pending = false
|
||||
}
|
||||
if m.reserved == m {
|
||||
m.reserved = nil
|
||||
if m.reserved && m.lock >= vfs.LOCK_RESERVED {
|
||||
m.reserved = false
|
||||
}
|
||||
if lock < vfs.LOCK_SHARED {
|
||||
m.shared--
|
||||
@@ -275,7 +274,7 @@ func (m *memFile) CheckReservedLock() (bool, error) {
|
||||
}
|
||||
m.lockMtx.Lock()
|
||||
defer m.lockMtx.Unlock()
|
||||
return m.reserved != nil, nil
|
||||
return m.reserved, nil
|
||||
}
|
||||
|
||||
func (m *memFile) SectorSize() int {
|
||||
|
||||
@@ -29,5 +29,12 @@ func osReadLock(file *os.File, _ /*start*/, _ /*len*/ int64, _ /*timeout*/ time.
|
||||
}
|
||||
|
||||
func osWriteLock(file *os.File, _ /*start*/, _ /*len*/ int64, _ /*timeout*/ time.Duration) _ErrorCode {
|
||||
return osLock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK)
|
||||
rc := osLock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK)
|
||||
if rc == _BUSY {
|
||||
// The documentation states the lock is upgraded by releasing the previous lock,
|
||||
// then acquiring the new lock.
|
||||
// This is a race, so return BUSY_SNAPSHOT to ensure the transaction is aborted.
|
||||
return _BUSY_SNAPSHOT
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func osDowngradeLock(file *os.File, state LockLevel) _ErrorCode {
|
||||
// In theory, the downgrade to a SHARED cannot fail because another
|
||||
// process is holding an incompatible lock. If it does, this
|
||||
// indicates that the other process is not following the locking
|
||||
// protocol. If this happens, return _IOERR_RDLOCK. Returning
|
||||
// protocol. If this happens, return IOERR_RDLOCK. Returning
|
||||
// BUSY would confuse the upper layer.
|
||||
return _IOERR_RDLOCK
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Go `"reader"` SQLite VFS
|
||||
# Go `reader` SQLite VFS
|
||||
|
||||
This package implements a `"reader"` SQLite VFS
|
||||
that allows accessing any [`io.ReaderAt`](https://pkg.go.dev/io#ReaderAt)
|
||||
|
||||
@@ -125,6 +125,9 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext
|
||||
return 0, _IOERR_SHMMAP
|
||||
}
|
||||
s.regions = append(s.regions, r)
|
||||
if s.readOnly {
|
||||
return r.Ptr, _READONLY
|
||||
}
|
||||
return r.Ptr, _OK
|
||||
}
|
||||
|
||||
|
||||
@@ -101,13 +101,13 @@ func (s *vfsShm) shmOpen() (rc _ErrorCode) {
|
||||
return _OK
|
||||
}
|
||||
|
||||
// Open file read-write, as it will be shared.
|
||||
// Always open file read-write, as it will be shared.
|
||||
f, err := os.OpenFile(s.path,
|
||||
unix.O_RDWR|unix.O_CREAT|unix.O_NOFOLLOW, 0666)
|
||||
if err != nil {
|
||||
return _CANTOPEN
|
||||
}
|
||||
// Close if file if it's not nil.
|
||||
// Closes file if it's not nil.
|
||||
defer func() { f.Close() }()
|
||||
|
||||
fi, err := f.Stat()
|
||||
@@ -128,10 +128,11 @@ func (s *vfsShm) shmOpen() (rc _ErrorCode) {
|
||||
}
|
||||
|
||||
// Lock and truncate the file, if not readonly.
|
||||
// The lock is only released by closing the file.
|
||||
if s.readOnly {
|
||||
rc = _READONLY_CANTINIT
|
||||
} else {
|
||||
if rc := osWriteLock(f, 0, 0, 0); rc != _OK {
|
||||
if rc := osLock(f, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK); rc != _OK {
|
||||
return rc
|
||||
}
|
||||
if err := f.Truncate(0); err != nil {
|
||||
@@ -145,17 +146,14 @@ func (s *vfsShm) shmOpen() (rc _ErrorCode) {
|
||||
info: fi,
|
||||
refs: 1,
|
||||
}
|
||||
f = nil
|
||||
add := true
|
||||
f = nil // Don't close the file.
|
||||
for i, g := range vfsShmFiles {
|
||||
if g == nil {
|
||||
vfsShmFiles[i] = s.vfsShmFile
|
||||
add = false
|
||||
return rc
|
||||
}
|
||||
}
|
||||
if add {
|
||||
vfsShmFiles = append(vfsShmFiles, s.vfsShmFile)
|
||||
}
|
||||
vfsShmFiles = append(vfsShmFiles, s.vfsShmFile)
|
||||
return rc
|
||||
}
|
||||
|
||||
@@ -195,6 +193,9 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext
|
||||
return 0, _IOERR_SHMMAP
|
||||
}
|
||||
s.regions = append(s.regions, r)
|
||||
if s.readOnly {
|
||||
return r.Ptr, _READONLY
|
||||
}
|
||||
return r.Ptr, _OK
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ func Test_multiwrite01(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_config01_memory(t *testing.T) {
|
||||
memdb.Delete("test.db")
|
||||
memdb.Create("test.db", nil)
|
||||
ctx := util.NewContext(newContext(t))
|
||||
cfg := config(ctx).WithArgs("mptest", "/test.db", "config01.test",
|
||||
"--vfs", "memdb")
|
||||
@@ -193,7 +193,7 @@ func Test_multiwrite01_memory(t *testing.T) {
|
||||
t.Skip("skipping in short mode")
|
||||
}
|
||||
|
||||
memdb.Delete("test.db")
|
||||
memdb.Create("test.db", nil)
|
||||
ctx := util.NewContext(newContext(t))
|
||||
cfg := config(ctx).WithArgs("mptest", "/test.db", "multiwrite01.test",
|
||||
"--vfs", "memdb")
|
||||
|
||||
31
vtab.go
31
vtab.go
@@ -16,14 +16,15 @@ func CreateModule[T VTab](db *Conn, name string, create, connect VTabConstructor
|
||||
var flags int
|
||||
|
||||
const (
|
||||
VTAB_CREATOR = 0x01
|
||||
VTAB_DESTROYER = 0x02
|
||||
VTAB_UPDATER = 0x04
|
||||
VTAB_RENAMER = 0x08
|
||||
VTAB_OVERLOADER = 0x10
|
||||
VTAB_CHECKER = 0x20
|
||||
VTAB_TXN = 0x40
|
||||
VTAB_SAVEPOINTER = 0x80
|
||||
VTAB_CREATOR = 0x001
|
||||
VTAB_DESTROYER = 0x002
|
||||
VTAB_UPDATER = 0x004
|
||||
VTAB_RENAMER = 0x008
|
||||
VTAB_OVERLOADER = 0x010
|
||||
VTAB_CHECKER = 0x020
|
||||
VTAB_TXN = 0x040
|
||||
VTAB_SAVEPOINTER = 0x080
|
||||
VTAB_SHADOWTABS = 0x100
|
||||
)
|
||||
|
||||
if create != nil {
|
||||
@@ -52,6 +53,9 @@ func CreateModule[T VTab](db *Conn, name string, create, connect VTabConstructor
|
||||
if implements[VTabSavepointer](vtab) {
|
||||
flags |= VTAB_SAVEPOINTER
|
||||
}
|
||||
if implements[VTabShadowTabler](vtab) {
|
||||
flags |= VTAB_SHADOWTABS
|
||||
}
|
||||
|
||||
defer db.arena.mark()()
|
||||
namePtr := db.arena.string(name)
|
||||
@@ -174,6 +178,17 @@ type VTabOverloader interface {
|
||||
FindFunction(arg int, name string) (ScalarFunction, IndexConstraintOp)
|
||||
}
|
||||
|
||||
// A VTabShadowTabler allows a virtual table to protect the content
|
||||
// of shadow tables from being corrupted by hostile SQL.
|
||||
//
|
||||
// Implementing this interface signals that a virtual table named
|
||||
// "mumble" reserves all table names starting with "mumble_".
|
||||
type VTabShadowTabler interface {
|
||||
VTab
|
||||
// https://sqlite.org/vtab.html#the_xshadowname_method
|
||||
ShadowTables()
|
||||
}
|
||||
|
||||
// A VTabChecker allows a virtual table to report errors
|
||||
// to the PRAGMA integrity_check and PRAGMA quick_check commands.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user