Compare commits

...

15 Commits

Author SHA1 Message Date
Nuno Cruces
aa7edb1848 Tests. 2024-06-21 16:23:56 +01:00
Nuno Cruces
3484bda553 Attestations. 2024-06-21 15:08:33 +01:00
Nuno Cruces
cf0d56271d Integrity. 2024-06-21 13:59:19 +01:00
Nuno Cruces
a465458255 CSV comments. 2024-06-20 11:02:23 +01:00
Nuno Cruces
65af8065cd Fixes. 2024-06-20 00:16:07 +01:00
Nuno Cruces
5c1c0f03a5 Tweaks. 2024-06-19 14:43:44 +01:00
Nuno Cruces
2d168136f1 Cache bind count. 2024-06-19 13:54:58 +01:00
Nuno Cruces
eb8e716253 Fix CI. 2024-06-19 00:43:12 +01:00
Nuno Cruces
3479e8935a Bloom filter virtual table (#103) 2024-06-18 23:42:20 +01:00
Nuno Cruces
58e91052bb CSV type affinity (#102)
Use sqlite-createtable-parser compiled to Wasm to parse the CREATE TABLE statement.
2024-06-17 23:44:37 +01:00
Nuno Cruces
3719692349 Fix potential BSD locking race. (#98) 2024-06-12 20:41:51 +01:00
Nuno Cruces
f7ac77027c wazero v1.7.2. 2024-06-11 23:50:32 +01:00
Nuno Cruces
ef065b6baa More benchmarks. 2024-06-11 10:52:07 +01:00
Nuno Cruces
e7f8311e2e Fix readonly shared memory (see #94). 2024-06-10 00:24:15 +01:00
Nuno Cruces
35a3bfe2f9 Doc fixes. 2024-06-07 12:10:03 +01:00
52 changed files with 1151 additions and 130 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
// Package ioutil implements virtual table utility functions.
package vtabutil
import "strings"

141
util/vtabutil/parse.go Normal file
View 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
View File

@@ -0,0 +1,2 @@
sql3parse_table.c
sql3parse_table.h

28
util/vtabutil/parse/build.sh Executable file
View 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

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

View File

@@ -0,0 +1,4 @@
sql3parse_table
sql3table_get_column
sql3table_num_columns
sql3column_type

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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