mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-17 07:59:13 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3484bda553 | ||
|
|
cf0d56271d | ||
|
|
a465458255 | ||
|
|
65af8065cd | ||
|
|
5c1c0f03a5 | ||
|
|
2d168136f1 | ||
|
|
eb8e716253 | ||
|
|
3479e8935a | ||
|
|
58e91052bb | ||
|
|
3719692349 |
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]
|
||||
|
||||
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ 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
|
||||
|
||||
2
go.sum
2
go.sum
@@ -1,3 +1,5 @@
|
||||
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=
|
||||
|
||||
@@ -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 1; }
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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