mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-17 07:59:13 +00:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b631ff1add | ||
|
|
fdfaaa8cec | ||
|
|
6a2827f989 | ||
|
|
9d77322d50 | ||
|
|
c1915feb2e | ||
|
|
52f9af3ca0 | ||
|
|
2f90277165 | ||
|
|
356dd56e5f | ||
|
|
e2a2d447ce | ||
|
|
75190a6f98 | ||
|
|
35c5619880 | ||
|
|
b51234cc82 | ||
|
|
cf7b89d3c4 | ||
|
|
ff9f27a778 | ||
|
|
f26f1a17a9 | ||
|
|
b9b2ff13da | ||
|
|
78473b4b37 | ||
|
|
3806c1cc23 | ||
|
|
1660c41f8c | ||
|
|
62b67c937e | ||
|
|
9e9971c292 | ||
|
|
d13bf1afaa | ||
|
|
f7c9551d66 | ||
|
|
22beef91d2 | ||
|
|
c97bbc7dab | ||
|
|
800eb107f9 | ||
|
|
6a1973f530 | ||
|
|
bd141fec92 | ||
|
|
e92999bfe3 | ||
|
|
d5583b6ec9 | ||
|
|
3649c1098e | ||
|
|
f743639c8f | ||
|
|
7cb974fd9a | ||
|
|
eea6aa7493 | ||
|
|
9a610888f9 | ||
|
|
dc4113073c | ||
|
|
38cab3202a | ||
|
|
2068b97116 | ||
|
|
8f835eda79 | ||
|
|
40db26c1dd | ||
|
|
a6815531e0 | ||
|
|
6c12a8c1fa | ||
|
|
e9de84a87f | ||
|
|
3bb1898335 | ||
|
|
22132620b8 | ||
|
|
c766a4fed2 | ||
|
|
73125945f8 | ||
|
|
32d998c84b | ||
|
|
8d450f82fc | ||
|
|
64b77f1a79 | ||
|
|
19639be9f9 | ||
|
|
2996e77420 | ||
|
|
24288c0e26 | ||
|
|
06f58c35e3 | ||
|
|
28f225b32e | ||
|
|
b289fca3ca | ||
|
|
21de85e849 | ||
|
|
4498f35a39 | ||
|
|
0c7d0a097d | ||
|
|
f537ab9a94 | ||
|
|
88b5b409df | ||
|
|
51c325bc5b | ||
|
|
5872224f77 | ||
|
|
7b56989489 | ||
|
|
bd5be4cde6 | ||
|
|
c19fec1e83 | ||
|
|
b5f746aadf | ||
|
|
fff8b1c74f | ||
|
|
d27da3f390 | ||
|
|
a1fae26b66 | ||
|
|
806cc6677d | ||
|
|
da6e4d8b86 | ||
|
|
72f8ad0f14 | ||
|
|
5a4c7a58c4 | ||
|
|
90f7e502be | ||
|
|
c0b289d000 | ||
|
|
a84d905d8c | ||
|
|
aa7edb1848 | ||
|
|
3484bda553 | ||
|
|
cf0d56271d | ||
|
|
a465458255 | ||
|
|
65af8065cd | ||
|
|
5c1c0f03a5 | ||
|
|
2d168136f1 | ||
|
|
eb8e716253 | ||
|
|
3479e8935a | ||
|
|
58e91052bb | ||
|
|
3719692349 |
2
.github/workflows/cross.yml
vendored
2
.github/workflows/cross.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Build
|
||||
run: .github/workflows/cross.sh
|
||||
run: .github/workflows/cross.sh
|
||||
33
.github/workflows/repro.sh
vendored
33
.github/workflows/repro.sh
vendored
@@ -2,22 +2,33 @@
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$OSTYPE" == "linux"* ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-linux.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-linux.tar.gz"
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-linux.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_119/binaryen-version_119-x86_64-linux.tar.gz"
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-macos.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-macos.tar.gz"
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-macos.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_119/binaryen-version_119-x86_64-macos.tar.gz"
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0.m-mingw.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-windows.tar.gz"
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-windows.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_119/binaryen-version_119-x86_64-windows.tar.gz"
|
||||
fi
|
||||
|
||||
# Download tools
|
||||
mkdir -p tools/
|
||||
[ -d "tools/wasi-sdk"* ] || curl -#L "$WASI_SDK" | tar xzC tools &
|
||||
[ -d "tools/binaryen-version"* ] || curl -#L "$BINARYEN" | tar xzC tools &
|
||||
[ -d "tools/wasi-sdk" ] || curl -#L "$WASI_SDK" | tar xzC tools &
|
||||
[ -d "tools/binaryen" ] || curl -#L "$BINARYEN" | tar xzC tools &
|
||||
wait
|
||||
|
||||
sqlite3/download.sh # Download SQLite
|
||||
embed/build.sh # Build Wasm
|
||||
git diff --exit-code # Check diffs
|
||||
[ -d "tools/wasi-sdk" ] || mv "tools/wasi-sdk"* "tools/wasi-sdk"
|
||||
[ -d "tools/binaryen" ] || mv "tools/binaryen"* "tools/binaryen"
|
||||
|
||||
# Download and build SQLite
|
||||
sqlite3/download.sh
|
||||
embed/build.sh
|
||||
embed/bcw2/build.sh
|
||||
|
||||
# Download and build sqlite-createtable-parser
|
||||
util/vtabutil/parse/download.sh
|
||||
util/vtabutil/parse/build.sh
|
||||
|
||||
# Check diffs
|
||||
git diff --exit-code
|
||||
15
.github/workflows/repro.yml
vendored
15
.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:
|
||||
@@ -11,9 +16,19 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: .github/workflows/repro.sh
|
||||
|
||||
- uses: actions/attest-build-provenance@v1
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
with:
|
||||
subject-path: |
|
||||
embed/sqlite3.wasm
|
||||
embed/bcw2/bcw2.wasm
|
||||
util/vtabutil/parse/sql3parse_table.wasm
|
||||
34
.github/workflows/test.yml
vendored
34
.github/workflows/test.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
run: go mod verify
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
run: go vet -tags vet ./...
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
@@ -57,10 +57,20 @@ jobs:
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
||||
- name: Test GORM
|
||||
shell: bash
|
||||
run: gormlite/test.sh
|
||||
|
||||
- name: Collect coverage
|
||||
run: |
|
||||
go install github.com/dave/courtney@latest
|
||||
courtney
|
||||
if: |
|
||||
github.event_name == 'push' &&
|
||||
matrix.os == 'ubuntu-latest'
|
||||
|
||||
- uses: ncruces/go-coverage-report@v0
|
||||
with:
|
||||
coverage-file: coverage.out
|
||||
chart: true
|
||||
amend: true
|
||||
if: |
|
||||
@@ -83,6 +93,18 @@ jobs:
|
||||
run: go test -v ./...
|
||||
|
||||
test-bsd:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- name: freebsd
|
||||
version: '14.1'
|
||||
flags: '-test.v'
|
||||
- name: openbsd
|
||||
version: '7.5'
|
||||
flags: '-test.v -test.short'
|
||||
- name: netbsd
|
||||
version: '10.0'
|
||||
flags: '-test.v -test.short'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
@@ -96,15 +118,15 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GOOS: freebsd
|
||||
TESTFLAGS: '-test.v'
|
||||
GOOS: ${{ matrix.os.name }}
|
||||
TESTFLAGS: ${{ matrix.os.flags }}
|
||||
run: .github/workflows/build-test.sh
|
||||
|
||||
- name: Test
|
||||
uses: cross-platform-actions/action@v0.24.0
|
||||
uses: cross-platform-actions/action@v0.25.0
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '14.0'
|
||||
operating_system: ${{ matrix.os.name }}
|
||||
version: ${{ matrix.os.version }}
|
||||
shell: bash
|
||||
run: . ./test.sh
|
||||
sync_files: runner-to-vm
|
||||
|
||||
29
README.md
29
README.md
@@ -12,6 +12,20 @@ It wraps a [Wasm](https://webassembly.org/) [build](embed/) of SQLite,
|
||||
and uses [wazero](https://wazero.io/) as the runtime.\
|
||||
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ runtime dependencies [^1].
|
||||
|
||||
### Getting started
|
||||
|
||||
Using the [`database/sql`](https://pkg.go.dev/database/sql) driver:
|
||||
```go
|
||||
|
||||
import "database/sql"
|
||||
import _ "github.com/ncruces/go-sqlite3/driver"
|
||||
import _ "github.com/ncruces/go-sqlite3/embed"
|
||||
|
||||
var version string
|
||||
db, _ := sql.Open("sqlite3", "file:demo.db")
|
||||
db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
|
||||
```
|
||||
|
||||
### Packages
|
||||
|
||||
- [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
|
||||
@@ -33,6 +47,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)
|
||||
@@ -43,20 +59,24 @@ Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ run
|
||||
reads data [line-by-line](https://github.com/asg017/sqlite-lines).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/pivot`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/pivot)
|
||||
creates [pivot tables](https://github.com/jakethaw/pivot_vtab).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/regexp`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/regexp)
|
||||
provides regular expression functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/statement`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/statement)
|
||||
creates [parameterized views](https://github.com/0x09/sqlite-statement-vtab).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/stats`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
|
||||
provides [statistics](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html) functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
|
||||
provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/uuid`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/uuid)
|
||||
generates [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
|
||||
maps multidimensional data to one dimension.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)
|
||||
wraps a VFS to offer encryption at rest.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/memdb`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb)
|
||||
implements an in-memory VFS.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/readervfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs)
|
||||
implements a VFS for immutable databases.
|
||||
- [`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
|
||||
|
||||
@@ -87,9 +107,10 @@ This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3
|
||||
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
|
||||
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
|
||||
|
||||
Every commit is [tested](.github/workflows/test.yml) on
|
||||
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Test-matrix) on
|
||||
Linux (amd64/arm64/386/riscv64/s390x), macOS (amd64/arm64),
|
||||
Windows (amd64), FreeBSD (amd64), illumos (amd64), and Solaris (amd64).
|
||||
Windows (amd64), FreeBSD (amd64), OpenBSD (amd64), NetBSD (amd64),
|
||||
illumos (amd64), and Solaris (amd64).
|
||||
|
||||
The Go VFS is tested by running SQLite's
|
||||
[mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c).
|
||||
|
||||
50
blob.go
50
blob.go
@@ -21,6 +21,8 @@ type Blob struct {
|
||||
bytes int64
|
||||
offset int64
|
||||
handle uint32
|
||||
bufptr uint32
|
||||
buflen int64
|
||||
}
|
||||
|
||||
var _ io.ReadWriteSeeker = &Blob{}
|
||||
@@ -66,7 +68,7 @@ func (b *Blob) Close() error {
|
||||
}
|
||||
|
||||
r := b.c.call("sqlite3_blob_close", uint64(b.handle))
|
||||
|
||||
b.c.free(b.bufptr)
|
||||
b.handle = 0
|
||||
return b.c.error(r)
|
||||
}
|
||||
@@ -86,17 +88,18 @@ func (b *Blob) Read(p []byte) (n int, err error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
avail := b.bytes - b.offset
|
||||
want := int64(len(p))
|
||||
avail := b.bytes - b.offset
|
||||
if want > avail {
|
||||
want = avail
|
||||
}
|
||||
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.new(uint64(want))
|
||||
if want > b.buflen {
|
||||
b.bufptr = b.c.realloc(b.bufptr, uint64(want))
|
||||
b.buflen = want
|
||||
}
|
||||
|
||||
r := b.c.call("sqlite3_blob_read", uint64(b.handle),
|
||||
uint64(ptr), uint64(want), uint64(b.offset))
|
||||
uint64(b.bufptr), uint64(want), uint64(b.offset))
|
||||
err = b.c.error(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -106,7 +109,7 @@ func (b *Blob) Read(p []byte) (n int, err error) {
|
||||
err = io.EOF
|
||||
}
|
||||
|
||||
copy(p, util.View(b.c.mod, ptr, uint64(want)))
|
||||
copy(p, util.View(b.c.mod, b.bufptr, uint64(want)))
|
||||
return int(want), err
|
||||
}
|
||||
|
||||
@@ -123,19 +126,20 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
|
||||
if want > avail {
|
||||
want = avail
|
||||
}
|
||||
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.new(uint64(want))
|
||||
if want > b.buflen {
|
||||
b.bufptr = b.c.realloc(b.bufptr, uint64(want))
|
||||
b.buflen = want
|
||||
}
|
||||
|
||||
for want > 0 {
|
||||
r := b.c.call("sqlite3_blob_read", uint64(b.handle),
|
||||
uint64(ptr), uint64(want), uint64(b.offset))
|
||||
uint64(b.bufptr), uint64(want), uint64(b.offset))
|
||||
err = b.c.error(r)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
mem := util.View(b.c.mod, ptr, uint64(want))
|
||||
mem := util.View(b.c.mod, b.bufptr, uint64(want))
|
||||
m, err := w.Write(mem[:want])
|
||||
b.offset += int64(m)
|
||||
n += int64(m)
|
||||
@@ -143,6 +147,7 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
|
||||
return n, err
|
||||
}
|
||||
if int64(m) != want {
|
||||
// notest // Write misbehaving
|
||||
return n, io.ErrShortWrite
|
||||
}
|
||||
|
||||
@@ -158,11 +163,15 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
|
||||
//
|
||||
// https://sqlite.org/c3ref/blob_write.html
|
||||
func (b *Blob) Write(p []byte) (n int, err error) {
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.bytes(p)
|
||||
want := int64(len(p))
|
||||
if want > b.buflen {
|
||||
b.bufptr = b.c.realloc(b.bufptr, uint64(want))
|
||||
b.buflen = want
|
||||
}
|
||||
util.WriteBytes(b.c.mod, b.bufptr, p)
|
||||
|
||||
r := b.c.call("sqlite3_blob_write", uint64(b.handle),
|
||||
uint64(ptr), uint64(len(p)), uint64(b.offset))
|
||||
uint64(b.bufptr), uint64(want), uint64(b.offset))
|
||||
err = b.c.error(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -186,16 +195,17 @@ func (b *Blob) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
if want < 1 {
|
||||
want = 1
|
||||
}
|
||||
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.new(uint64(want))
|
||||
if want > b.buflen {
|
||||
b.bufptr = b.c.realloc(b.bufptr, uint64(want))
|
||||
b.buflen = want
|
||||
}
|
||||
|
||||
for {
|
||||
mem := util.View(b.c.mod, ptr, uint64(want))
|
||||
mem := util.View(b.c.mod, b.bufptr, uint64(want))
|
||||
m, err := r.Read(mem[:want])
|
||||
if m > 0 {
|
||||
r := b.c.call("sqlite3_blob_write", uint64(b.handle),
|
||||
uint64(ptr), uint64(m), uint64(b.offset))
|
||||
uint64(b.bufptr), uint64(m), uint64(b.offset))
|
||||
err := b.c.error(r)
|
||||
if err != nil {
|
||||
return n, err
|
||||
|
||||
158
config.go
158
config.go
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
@@ -56,6 +57,99 @@ func logCallback(ctx context.Context, mod api.Module, _, iCode, zMsg uint32) {
|
||||
}
|
||||
}
|
||||
|
||||
// FileControl allows low-level control of database files.
|
||||
// Only a subset of opcodes are supported.
|
||||
//
|
||||
// https://sqlite.org/c3ref/file_control.html
|
||||
func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, error) {
|
||||
defer c.arena.mark()()
|
||||
|
||||
var schemaPtr uint32
|
||||
if schema != "" {
|
||||
schemaPtr = c.arena.string(schema)
|
||||
}
|
||||
|
||||
switch op {
|
||||
case FCNTL_RESET_CACHE:
|
||||
r := c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), 0)
|
||||
return nil, c.error(r)
|
||||
|
||||
case FCNTL_PERSIST_WAL, FCNTL_POWERSAFE_OVERWRITE:
|
||||
var flag int
|
||||
switch {
|
||||
case len(arg) == 0:
|
||||
flag = -1
|
||||
case arg[0]:
|
||||
flag = 1
|
||||
}
|
||||
ptr := c.arena.new(4)
|
||||
util.WriteUint32(c.mod, ptr, uint32(flag))
|
||||
r := c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
return util.ReadUint32(c.mod, ptr) != 0, c.error(r)
|
||||
|
||||
case FCNTL_CHUNK_SIZE:
|
||||
ptr := c.arena.new(4)
|
||||
util.WriteUint32(c.mod, ptr, uint32(arg[0].(int)))
|
||||
r := c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
return nil, c.error(r)
|
||||
|
||||
case FCNTL_RESERVE_BYTES:
|
||||
bytes := -1
|
||||
if len(arg) > 0 {
|
||||
bytes = arg[0].(int)
|
||||
}
|
||||
ptr := c.arena.new(4)
|
||||
util.WriteUint32(c.mod, ptr, uint32(bytes))
|
||||
r := c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
return int(util.ReadUint32(c.mod, ptr)), c.error(r)
|
||||
|
||||
case FCNTL_DATA_VERSION:
|
||||
ptr := c.arena.new(4)
|
||||
r := c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
return util.ReadUint32(c.mod, ptr), c.error(r)
|
||||
|
||||
case FCNTL_LOCKSTATE:
|
||||
ptr := c.arena.new(4)
|
||||
r := c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
return vfs.LockLevel(util.ReadUint32(c.mod, ptr)), c.error(r)
|
||||
|
||||
case FCNTL_VFS_POINTER:
|
||||
ptr := c.arena.new(4)
|
||||
r := c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
const zNameOffset = 16
|
||||
ptr = util.ReadUint32(c.mod, ptr)
|
||||
ptr = util.ReadUint32(c.mod, ptr+zNameOffset)
|
||||
name := util.ReadString(c.mod, ptr, _MAX_NAME)
|
||||
return vfs.Find(name), c.error(r)
|
||||
|
||||
case FCNTL_FILE_POINTER, FCNTL_JOURNAL_POINTER:
|
||||
ptr := c.arena.new(4)
|
||||
r := c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
const fileHandleOffset = 4
|
||||
ptr = util.ReadUint32(c.mod, ptr)
|
||||
ptr = util.ReadUint32(c.mod, ptr+fileHandleOffset)
|
||||
return util.GetHandle(c.ctx, ptr), c.error(r)
|
||||
}
|
||||
|
||||
return nil, MISUSE
|
||||
}
|
||||
|
||||
// Limit allows the size of various constructs to be
|
||||
// limited on a connection by connection basis.
|
||||
//
|
||||
@@ -68,7 +162,7 @@ func (c *Conn) Limit(id LimitCategory, value int) int {
|
||||
// SetAuthorizer registers an authorizer callback with the database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/set_authorizer.html
|
||||
func (c *Conn) SetAuthorizer(cb func(action AuthorizerActionCode, name3rd, name4th, schema, nameInner string) AuthorizerReturnCode) error {
|
||||
func (c *Conn) SetAuthorizer(cb func(action AuthorizerActionCode, name3rd, name4th, schema, inner string) AuthorizerReturnCode) error {
|
||||
var enable uint64
|
||||
if cb != nil {
|
||||
enable = 1
|
||||
@@ -82,9 +176,9 @@ func (c *Conn) SetAuthorizer(cb func(action AuthorizerActionCode, name3rd, name4
|
||||
|
||||
}
|
||||
|
||||
func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action AuthorizerActionCode, zName3rd, zName4th, zSchema, zNameInner uint32) (rc AuthorizerReturnCode) {
|
||||
func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action AuthorizerActionCode, zName3rd, zName4th, zSchema, zInner uint32) (rc AuthorizerReturnCode) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.authorizer != nil {
|
||||
var name3rd, name4th, schema, nameInner string
|
||||
var name3rd, name4th, schema, inner string
|
||||
if zName3rd != 0 {
|
||||
name3rd = util.ReadString(mod, zName3rd, _MAX_NAME)
|
||||
}
|
||||
@@ -94,10 +188,48 @@ func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action
|
||||
if zSchema != 0 {
|
||||
schema = util.ReadString(mod, zSchema, _MAX_NAME)
|
||||
}
|
||||
if zNameInner != 0 {
|
||||
nameInner = util.ReadString(mod, zNameInner, _MAX_NAME)
|
||||
if zInner != 0 {
|
||||
inner = util.ReadString(mod, zInner, _MAX_NAME)
|
||||
}
|
||||
rc = c.authorizer(action, name3rd, name4th, schema, inner)
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
// Trace registers a trace callback function against the database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/trace_v2.html
|
||||
func (c *Conn) Trace(mask TraceEvent, cb func(evt TraceEvent, arg1 any, arg2 any) error) error {
|
||||
r := c.call("sqlite3_trace_go", uint64(c.handle), uint64(mask))
|
||||
if err := c.error(r); err != nil {
|
||||
return err
|
||||
}
|
||||
c.trace = cb
|
||||
return nil
|
||||
}
|
||||
|
||||
func traceCallback(ctx context.Context, mod api.Module, evt TraceEvent, pDB, pArg1, pArg2 uint32) (rc uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.trace != nil {
|
||||
var arg1, arg2 any
|
||||
if evt == TRACE_CLOSE {
|
||||
arg1 = c
|
||||
} else {
|
||||
for _, s := range c.stmts {
|
||||
if pArg1 == s.handle {
|
||||
arg1 = s
|
||||
switch evt {
|
||||
case TRACE_STMT:
|
||||
arg2 = s.SQL()
|
||||
case TRACE_PROFILE:
|
||||
arg2 = int64(util.ReadUint64(mod, pArg2))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if arg1 != nil {
|
||||
_, rc = errorCode(c.trace(evt, arg1, arg2), ERROR)
|
||||
}
|
||||
rc = c.authorizer(action, name3rd, name4th, schema, nameInner)
|
||||
}
|
||||
return rc
|
||||
}
|
||||
@@ -162,3 +294,17 @@ func autoVacuumCallback(ctx context.Context, mod api.Module, pApp, zSchema, nDbP
|
||||
schema := util.ReadString(mod, zSchema, _MAX_NAME)
|
||||
return uint32(fn(schema, uint(nDbPage), uint(nFreePage), uint(nBytePerPage)))
|
||||
}
|
||||
|
||||
// SoftHeapLimit imposes a soft limit on heap size.
|
||||
//
|
||||
// https://sqlite.org/c3ref/hard_heap_limit64.html
|
||||
func (c *Conn) SoftHeapLimit(n int64) int64 {
|
||||
return int64(c.call("sqlite3_soft_heap_limit64", uint64(n)))
|
||||
}
|
||||
|
||||
// SoftHeapLimit imposes a hard limit on heap size.
|
||||
//
|
||||
// https://sqlite.org/c3ref/hard_heap_limit64.html
|
||||
func (c *Conn) HardHeapLimit(n int64) int64 {
|
||||
return int64(c.call("sqlite3_hard_heap_limit64", uint64(n)))
|
||||
}
|
||||
|
||||
107
conn.go
107
conn.go
@@ -22,14 +22,17 @@ type Conn struct {
|
||||
|
||||
interrupt context.Context
|
||||
pending *Stmt
|
||||
stmts []*Stmt
|
||||
timer *time.Timer
|
||||
busy func(int) bool
|
||||
log func(xErrorCode, string)
|
||||
collation func(*Conn, string)
|
||||
wal func(*Conn, string, int) error
|
||||
trace func(TraceEvent, any, any) error
|
||||
authorizer func(AuthorizerActionCode, string, string, string, string) AuthorizerReturnCode
|
||||
update func(AuthorizerActionCode, string, string, int64)
|
||||
commit func() bool
|
||||
rollback func()
|
||||
wal func(*Conn, string, int) error
|
||||
arena arena
|
||||
|
||||
handle uint32
|
||||
@@ -72,6 +75,9 @@ func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
|
||||
c.arena = c.newArena(1024)
|
||||
c.ctx = context.WithValue(c.ctx, connKey{}, c)
|
||||
c.handle, err = c.openDB(filename, flags)
|
||||
if err == nil {
|
||||
err = initExtensions(c)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -199,6 +205,7 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
|
||||
if stmt.handle == 0 {
|
||||
return nil, "", nil
|
||||
}
|
||||
c.stmts = append(c.stmts, stmt)
|
||||
return stmt, tail, nil
|
||||
}
|
||||
|
||||
@@ -224,9 +231,8 @@ func (c *Conn) Filename(schema string) *vfs.Filename {
|
||||
defer c.arena.mark()()
|
||||
ptr = c.arena.string(schema)
|
||||
}
|
||||
|
||||
r := c.call("sqlite3_db_filename", uint64(c.handle), uint64(ptr))
|
||||
return vfs.OpenFilename(c.ctx, c.mod, uint32(r), vfs.OPEN_MAIN_DB)
|
||||
return vfs.GetFilename(c.ctx, c.mod, uint32(r), vfs.OPEN_MAIN_DB)
|
||||
}
|
||||
|
||||
// ReadOnly determines if a database is read-only.
|
||||
@@ -324,7 +330,12 @@ func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
|
||||
// A busy SQL statement prevents SQLite from ignoring an interrupt
|
||||
// that comes before any other statements are started.
|
||||
if c.pending == nil {
|
||||
c.pending, _, _ = c.Prepare(`WITH RECURSIVE c(x) AS (VALUES(0) UNION ALL SELECT x FROM c) SELECT x FROM c`)
|
||||
defer c.arena.mark()()
|
||||
stmtPtr := c.arena.new(ptrlen)
|
||||
loopPtr := c.arena.string(`WITH RECURSIVE c(x) AS (VALUES(0) UNION ALL SELECT x FROM c) SELECT x FROM c`)
|
||||
c.call("sqlite3_prepare_v3", uint64(c.handle), uint64(loopPtr), math.MaxUint64, 0, uint64(stmtPtr), 0)
|
||||
c.pending = &Stmt{c: c}
|
||||
c.pending.handle = util.ReadUint32(c.mod, stmtPtr)
|
||||
}
|
||||
|
||||
old = c.interrupt
|
||||
@@ -379,11 +390,25 @@ func timeoutCallback(ctx context.Context, mod api.Module, pDB uint32, count, tmo
|
||||
}
|
||||
|
||||
if delay = min(delay, tmout-prior); delay > 0 {
|
||||
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||
retry = 1
|
||||
delay := time.Duration(delay) * time.Millisecond
|
||||
if c.interrupt == nil || c.interrupt.Done() == nil {
|
||||
time.Sleep(delay)
|
||||
return 1
|
||||
}
|
||||
if c.timer == nil {
|
||||
c.timer = time.NewTimer(delay)
|
||||
} else {
|
||||
c.timer.Reset(delay)
|
||||
}
|
||||
select {
|
||||
case <-c.interrupt.Done():
|
||||
c.timer.Stop()
|
||||
case <-c.timer.C:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return retry
|
||||
return 0
|
||||
}
|
||||
|
||||
// BusyHandler registers a callback to handle [BUSY] errors.
|
||||
@@ -412,15 +437,77 @@ func busyCallback(ctx context.Context, mod api.Module, pDB uint32, count int32)
|
||||
return retry
|
||||
}
|
||||
|
||||
// Status retrieves runtime status information about a database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/db_status.html
|
||||
func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int, err error) {
|
||||
defer c.arena.mark()()
|
||||
hiPtr := c.arena.new(4)
|
||||
curPtr := c.arena.new(4)
|
||||
|
||||
var i uint64
|
||||
if reset {
|
||||
i = 1
|
||||
}
|
||||
|
||||
r := c.call("sqlite3_db_status", uint64(c.handle),
|
||||
uint64(op), uint64(curPtr), uint64(hiPtr), i)
|
||||
if err = c.error(r); err == nil {
|
||||
current = int(util.ReadUint32(c.mod, curPtr))
|
||||
highwater = int(util.ReadUint32(c.mod, hiPtr))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TableColumnMetadata extracts metadata about a column of a table.
|
||||
//
|
||||
// https://sqlite.org/c3ref/table_column_metadata.html
|
||||
func (c *Conn) TableColumnMetadata(schema, table, column string) (declType, collSeq string, notNull, primaryKey, autoInc bool, err error) {
|
||||
defer c.arena.mark()()
|
||||
|
||||
var schemaPtr, columnPtr uint32
|
||||
declTypePtr := c.arena.new(ptrlen)
|
||||
collSeqPtr := c.arena.new(ptrlen)
|
||||
notNullPtr := c.arena.new(ptrlen)
|
||||
primaryKeyPtr := c.arena.new(ptrlen)
|
||||
autoIncPtr := c.arena.new(ptrlen)
|
||||
if schema != "" {
|
||||
schemaPtr = c.arena.string(schema)
|
||||
}
|
||||
tablePtr := c.arena.string(table)
|
||||
if column != "" {
|
||||
columnPtr = c.arena.string(column)
|
||||
}
|
||||
|
||||
r := c.call("sqlite3_table_column_metadata", uint64(c.handle),
|
||||
uint64(schemaPtr), uint64(tablePtr), uint64(columnPtr),
|
||||
uint64(declTypePtr), uint64(collSeqPtr),
|
||||
uint64(notNullPtr), uint64(primaryKeyPtr), uint64(autoIncPtr))
|
||||
if err = c.error(r); err == nil && column != "" {
|
||||
declType = util.ReadString(c.mod, util.ReadUint32(c.mod, declTypePtr), _MAX_NAME)
|
||||
collSeq = util.ReadString(c.mod, util.ReadUint32(c.mod, collSeqPtr), _MAX_NAME)
|
||||
notNull = util.ReadUint32(c.mod, notNullPtr) != 0
|
||||
autoInc = util.ReadUint32(c.mod, autoIncPtr) != 0
|
||||
primaryKey = util.ReadUint32(c.mod, primaryKeyPtr) != 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) error(rc uint64, sql ...string) error {
|
||||
return c.sqlite.error(rc, c.handle, sql...)
|
||||
}
|
||||
|
||||
func (c *Conn) stmtsIter(yield func(*Stmt) bool) {
|
||||
for _, s := range c.stmts {
|
||||
if !yield(s) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DriverConn is implemented by the SQLite [database/sql] driver connection.
|
||||
//
|
||||
// It can be used to access SQLite features like [online backup].
|
||||
//
|
||||
// [online backup]: https://sqlite.org/backup.html
|
||||
// Deprecated: use [github.com/ncruces/go-sqlite3/driver.Conn] instead.
|
||||
type DriverConn interface {
|
||||
Raw() *Conn
|
||||
}
|
||||
|
||||
11
conn_iter.go
Normal file
11
conn_iter.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build (go1.23 || goexperiment.rangefunc) && !vet
|
||||
|
||||
package sqlite3
|
||||
|
||||
import "iter"
|
||||
|
||||
// Stmts returns an iterator for the prepared statements
|
||||
// associated with the database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/next_stmt.html
|
||||
func (c *Conn) Stmts() iter.Seq[*Stmt] { return c.stmtsIter }
|
||||
9
conn_old.go
Normal file
9
conn_old.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !(go1.23 || goexperiment.rangefunc) || vet
|
||||
|
||||
package sqlite3
|
||||
|
||||
// Stmts returns an iterator for the prepared statements
|
||||
// associated with the database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/next_stmt.html
|
||||
func (c *Conn) Stmts() func(func(*Stmt) bool) { return c.stmtsIter }
|
||||
76
const.go
76
const.go
@@ -7,13 +7,10 @@ const (
|
||||
_ROW = 100 /* sqlite3_step() has another row ready */
|
||||
_DONE = 101 /* sqlite3_step() has finished executing */
|
||||
|
||||
_UTF8 = 1
|
||||
|
||||
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
|
||||
_MAX_LENGTH = 1e9
|
||||
_MAX_SQL_LENGTH = 1e9
|
||||
_MAX_ALLOCATION_SIZE = 0x7ffffeff
|
||||
_MAX_FUNCTION_ARG = 100
|
||||
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
|
||||
_MAX_LENGTH = 1e9
|
||||
_MAX_SQL_LENGTH = 1e9
|
||||
_MAX_FUNCTION_ARG = 100
|
||||
|
||||
ptrlen = 4
|
||||
)
|
||||
@@ -109,7 +106,7 @@ const (
|
||||
CANTOPEN_ISDIR ExtendedErrorCode = xErrorCode(CANTOPEN) | (2 << 8)
|
||||
CANTOPEN_FULLPATH ExtendedErrorCode = xErrorCode(CANTOPEN) | (3 << 8)
|
||||
CANTOPEN_CONVPATH ExtendedErrorCode = xErrorCode(CANTOPEN) | (4 << 8)
|
||||
CANTOPEN_DIRTYWAL ExtendedErrorCode = xErrorCode(CANTOPEN) | (5 << 8) /* Not Used */
|
||||
// CANTOPEN_DIRTYWAL ExtendedErrorCode = xErrorCode(CANTOPEN) | (5 << 8) /* Not Used */
|
||||
CANTOPEN_SYMLINK ExtendedErrorCode = xErrorCode(CANTOPEN) | (6 << 8)
|
||||
CORRUPT_VTAB ExtendedErrorCode = xErrorCode(CORRUPT) | (1 << 8)
|
||||
CORRUPT_SEQUENCE ExtendedErrorCode = xErrorCode(CORRUPT) | (2 << 8)
|
||||
@@ -177,11 +174,11 @@ const (
|
||||
type FunctionFlag uint32
|
||||
|
||||
const (
|
||||
DETERMINISTIC FunctionFlag = 0x000000800
|
||||
DIRECTONLY FunctionFlag = 0x000080000
|
||||
SUBTYPE FunctionFlag = 0x000100000
|
||||
INNOCUOUS FunctionFlag = 0x000200000
|
||||
RESULT_SUBTYPE FunctionFlag = 0x001000000
|
||||
DETERMINISTIC FunctionFlag = 0x000000800
|
||||
DIRECTONLY FunctionFlag = 0x000080000
|
||||
INNOCUOUS FunctionFlag = 0x000200000
|
||||
// SUBTYPE FunctionFlag = 0x000100000
|
||||
// RESULT_SUBTYPE FunctionFlag = 0x001000000
|
||||
)
|
||||
|
||||
// StmtStatus name counter values associated with the [Stmt.Status] method.
|
||||
@@ -201,6 +198,27 @@ const (
|
||||
STMTSTATUS_MEMUSED StmtStatus = 99
|
||||
)
|
||||
|
||||
// DBStatus are the available "verbs" that can be passed to the [Conn.Status] method.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_dbstatus_options.html
|
||||
type DBStatus uint32
|
||||
|
||||
const (
|
||||
DBSTATUS_LOOKASIDE_USED DBStatus = 0
|
||||
DBSTATUS_CACHE_USED DBStatus = 1
|
||||
DBSTATUS_SCHEMA_USED DBStatus = 2
|
||||
DBSTATUS_STMT_USED DBStatus = 3
|
||||
DBSTATUS_LOOKASIDE_HIT DBStatus = 4
|
||||
DBSTATUS_LOOKASIDE_MISS_SIZE DBStatus = 5
|
||||
DBSTATUS_LOOKASIDE_MISS_FULL DBStatus = 6
|
||||
DBSTATUS_CACHE_HIT DBStatus = 7
|
||||
DBSTATUS_CACHE_MISS DBStatus = 8
|
||||
DBSTATUS_CACHE_WRITE DBStatus = 9
|
||||
DBSTATUS_DEFERRED_FKS DBStatus = 10
|
||||
DBSTATUS_CACHE_USED_SHARED DBStatus = 11
|
||||
DBSTATUS_CACHE_SPILL DBStatus = 12
|
||||
)
|
||||
|
||||
// DBConfig are the available database connection configuration options.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_dbconfig_defensive.html
|
||||
@@ -229,6 +247,24 @@ const (
|
||||
DBCONFIG_REVERSE_SCANORDER DBConfig = 1019
|
||||
)
|
||||
|
||||
// FcntlOpcode are the available opcodes for [Conn.FileControl].
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html
|
||||
type FcntlOpcode uint32
|
||||
|
||||
const (
|
||||
FCNTL_LOCKSTATE FcntlOpcode = 1
|
||||
FCNTL_CHUNK_SIZE FcntlOpcode = 6
|
||||
FCNTL_FILE_POINTER FcntlOpcode = 7
|
||||
FCNTL_PERSIST_WAL FcntlOpcode = 10
|
||||
FCNTL_POWERSAFE_OVERWRITE FcntlOpcode = 13
|
||||
FCNTL_VFS_POINTER FcntlOpcode = 27
|
||||
FCNTL_JOURNAL_POINTER FcntlOpcode = 28
|
||||
FCNTL_DATA_VERSION FcntlOpcode = 35
|
||||
FCNTL_RESERVE_BYTES FcntlOpcode = 38
|
||||
FCNTL_RESET_CACHE FcntlOpcode = 42
|
||||
)
|
||||
|
||||
// LimitCategory are the available run-time limit categories.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_limit_attached.html
|
||||
@@ -289,8 +325,8 @@ const (
|
||||
AUTH_DROP_VTABLE AuthorizerActionCode = 30 /* Table Name Module Name */
|
||||
AUTH_FUNCTION AuthorizerActionCode = 31 /* NULL Function Name */
|
||||
AUTH_SAVEPOINT AuthorizerActionCode = 32 /* Operation Savepoint Name */
|
||||
AUTH_COPY AuthorizerActionCode = 0 /* No longer used */
|
||||
AUTH_RECURSIVE AuthorizerActionCode = 33 /* NULL NULL */
|
||||
// AUTH_COPY AuthorizerActionCode = 0 /* No longer used */
|
||||
)
|
||||
|
||||
// AuthorizerReturnCode are the integer codes
|
||||
@@ -328,6 +364,18 @@ const (
|
||||
TXN_WRITE TxnState = 2
|
||||
)
|
||||
|
||||
// TraceEvent identify classes of events that can be monitored with [Conn.Trace].
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_trace.html
|
||||
type TraceEvent uint32
|
||||
|
||||
const (
|
||||
TRACE_STMT TraceEvent = 0x01
|
||||
TRACE_PROFILE TraceEvent = 0x02
|
||||
TRACE_ROW TraceEvent = 0x04
|
||||
TRACE_CLOSE TraceEvent = 0x08
|
||||
)
|
||||
|
||||
// Datatype is a fundamental datatype of SQLite.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_blob.html
|
||||
|
||||
28
context.go
28
context.go
@@ -84,9 +84,8 @@ func (ctx Context) ResultFloat(value float64) {
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultText(value string) {
|
||||
ptr := ctx.c.newString(value)
|
||||
ctx.c.call("sqlite3_result_text64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
|
||||
uint64(ctx.c.freer), _UTF8)
|
||||
ctx.c.call("sqlite3_result_text_go",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)))
|
||||
}
|
||||
|
||||
// ResultRawText sets the text result of the function to a []byte.
|
||||
@@ -94,9 +93,8 @@ func (ctx Context) ResultText(value string) {
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultRawText(value []byte) {
|
||||
ptr := ctx.c.newBytes(value)
|
||||
ctx.c.call("sqlite3_result_text64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
|
||||
uint64(ctx.c.freer), _UTF8)
|
||||
ctx.c.call("sqlite3_result_text_go",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)))
|
||||
}
|
||||
|
||||
// ResultBlob sets the result of the function to a []byte.
|
||||
@@ -105,9 +103,8 @@ func (ctx Context) ResultRawText(value []byte) {
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultBlob(value []byte) {
|
||||
ptr := ctx.c.newBytes(value)
|
||||
ctx.c.call("sqlite3_result_blob64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
|
||||
uint64(ctx.c.freer))
|
||||
ctx.c.call("sqlite3_result_blob_go",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)))
|
||||
}
|
||||
|
||||
// ResultZeroBlob sets the result of the function to a zero-filled, length n BLOB.
|
||||
@@ -130,7 +127,8 @@ func (ctx Context) ResultNull() {
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultTime(value time.Time, format TimeFormat) {
|
||||
if format == TimeFormatDefault {
|
||||
switch format {
|
||||
case TimeFormatDefault, TimeFormatAuto, time.RFC3339Nano:
|
||||
ctx.resultRFC3339Nano(value)
|
||||
return
|
||||
}
|
||||
@@ -153,9 +151,8 @@ func (ctx Context) resultRFC3339Nano(value time.Time) {
|
||||
buf := util.View(ctx.c.mod, ptr, maxlen)
|
||||
buf = value.AppendFormat(buf[:0], time.RFC3339Nano)
|
||||
|
||||
ctx.c.call("sqlite3_result_text64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(buf)),
|
||||
uint64(ctx.c.freer), _UTF8)
|
||||
ctx.c.call("sqlite3_result_text_go",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(buf)))
|
||||
}
|
||||
|
||||
// ResultPointer sets the result of the function to NULL, just like [Context.ResultNull],
|
||||
@@ -165,7 +162,8 @@ func (ctx Context) resultRFC3339Nano(value time.Time) {
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultPointer(ptr any) {
|
||||
valPtr := util.AddHandle(ctx.c.ctx, ptr)
|
||||
ctx.c.call("sqlite3_result_pointer_go", uint64(valPtr))
|
||||
ctx.c.call("sqlite3_result_pointer_go",
|
||||
uint64(ctx.handle), uint64(valPtr))
|
||||
}
|
||||
|
||||
// ResultJSON sets the result of the function to the JSON encoding of value.
|
||||
@@ -175,7 +173,7 @@ func (ctx Context) ResultJSON(value any) {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
ctx.ResultRawText(data)
|
||||
}
|
||||
|
||||
229
driver/driver.go
229
driver/driver.go
@@ -8,21 +8,50 @@
|
||||
//
|
||||
// The data source name for "sqlite3" databases can be a filename or a "file:" [URI].
|
||||
//
|
||||
// # Default transaction mode
|
||||
//
|
||||
// The [TRANSACTION] mode can be specified using "_txlock":
|
||||
//
|
||||
// sql.Open("sqlite3", "file:demo.db?_txlock=immediate")
|
||||
//
|
||||
// Possible values are: "deferred", "immediate", "exclusive".
|
||||
// A [read-only] transaction is always "deferred", regardless of "_txlock".
|
||||
// Possible values are: "deferred" (the default), "immediate", "exclusive".
|
||||
// Regardless of "_txlock":
|
||||
// - a [linearizable] transaction is always "exclusive";
|
||||
// - a [serializable] transaction is always "immediate";
|
||||
// - a [read-only] transaction is always "deferred".
|
||||
//
|
||||
// # Working with time
|
||||
//
|
||||
// The time encoding/decoding format can be specified using "_timefmt":
|
||||
//
|
||||
// sql.Open("sqlite3", "file:demo.db?_timefmt=sqlite")
|
||||
//
|
||||
// Possible values are: "auto" (the default), "sqlite", "rfc3339";
|
||||
// "auto" encodes as RFC 3339 and decodes any [format] supported by SQLite;
|
||||
// "sqlite" encodes as SQLite and decodes any [format] supported by SQLite;
|
||||
// "rfc3339" encodes and decodes RFC 3339 only.
|
||||
// - "auto" encodes as RFC 3339 and decodes any [format] supported by SQLite;
|
||||
// - "sqlite" encodes as SQLite and decodes any [format] supported by SQLite;
|
||||
// - "rfc3339" encodes and decodes RFC 3339 only.
|
||||
//
|
||||
// If you encode as RFC 3339 (the default),
|
||||
// consider using the TIME [collating sequence] to produce a time-ordered sequence.
|
||||
//
|
||||
// To scan values in other formats, [sqlite3.TimeFormat.Scanner] may be helpful.
|
||||
// To bind values in other formats, [sqlite3.TimeFormat.Encode] them before binding.
|
||||
//
|
||||
// When using a custom time struct, you'll have to implement
|
||||
// [database/sql/driver.Valuer] and [database/sql.Scanner].
|
||||
//
|
||||
// The Value method should ideally serialise to a time [format] supported by SQLite.
|
||||
// This ensures SQL date and time functions work as they should,
|
||||
// and that your schema works with other SQLite tools.
|
||||
// [sqlite3.TimeFormat.Encode] may help.
|
||||
//
|
||||
// The Scan method needs to take into account that the value it receives can be of differing types.
|
||||
// It can already be a [time.Time], if the driver decoded the value according to "_timefmt" rules.
|
||||
// Or it can be a: string, int64, float64, []byte, nil,
|
||||
// depending on the column type and what whoever wrote the value.
|
||||
// [sqlite3.TimeFormat.Decode] may help.
|
||||
//
|
||||
// # Setting PRAGMAs
|
||||
//
|
||||
// [PRAGMA] statements can be specified using "_pragma":
|
||||
//
|
||||
@@ -31,13 +60,17 @@
|
||||
// If no PRAGMAs are specified, a busy timeout of 1 minute is set.
|
||||
//
|
||||
// Order matters:
|
||||
// busy timeout and locking mode should be the first PRAGMAs set, in that order.
|
||||
// encryption keys, busy timeout and locking mode should be the first PRAGMAs set,
|
||||
// in that order.
|
||||
//
|
||||
// [URI]: https://sqlite.org/uri.html
|
||||
// [PRAGMA]: https://sqlite.org/pragma.html
|
||||
// [format]: https://sqlite.org/lang_datefunc.html#time_values
|
||||
// [TRANSACTION]: https://sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions
|
||||
// [linearizable]: https://pkg.go.dev/database/sql#TxOptions
|
||||
// [serializable]: https://pkg.go.dev/database/sql#TxOptions
|
||||
// [read-only]: https://pkg.go.dev/database/sql#TxOptions
|
||||
// [format]: https://sqlite.org/lang_datefunc.html#time_values
|
||||
// [collating sequence]: https://sqlite.org/datatype3.html#collating_sequences
|
||||
package driver
|
||||
|
||||
import (
|
||||
@@ -69,11 +102,22 @@ func init() {
|
||||
|
||||
// Open opens the SQLite database specified by dataSourceName as a [database/sql.DB].
|
||||
//
|
||||
// The init function is called by the driver on new connections.
|
||||
// Open accepts zero, one, or two callbacks (nil callbacks are ignored).
|
||||
// The first callback is called when the driver opens a new connection.
|
||||
// The second callback is called before the driver closes a connection.
|
||||
// The [sqlite3.Conn] can be used to execute queries, register functions, etc.
|
||||
// Any error returned closes the connection and is returned to [database/sql].
|
||||
func Open(dataSourceName string, init func(*sqlite3.Conn) error) (*sql.DB, error) {
|
||||
c, err := (&SQLite{Init: init}).OpenConnector(dataSourceName)
|
||||
func Open(dataSourceName string, fn ...func(*sqlite3.Conn) error) (*sql.DB, error) {
|
||||
var drv SQLite
|
||||
if len(fn) > 2 {
|
||||
return nil, sqlite3.MISUSE
|
||||
}
|
||||
if len(fn) > 1 {
|
||||
drv.term = fn[1]
|
||||
}
|
||||
if len(fn) > 0 {
|
||||
drv.init = fn[0]
|
||||
}
|
||||
c, err := drv.OpenConnector(dataSourceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -82,12 +126,15 @@ func Open(dataSourceName string, init func(*sqlite3.Conn) error) (*sql.DB, error
|
||||
|
||||
// SQLite implements [database/sql/driver.Driver].
|
||||
type SQLite struct {
|
||||
// Init function is called by the driver on new connections.
|
||||
// The [sqlite3.Conn] can be used to execute queries, register functions, etc.
|
||||
// Any error returned closes the connection and is returned to [database/sql].
|
||||
Init func(*sqlite3.Conn) error
|
||||
init func(*sqlite3.Conn) error
|
||||
term func(*sqlite3.Conn) error
|
||||
}
|
||||
|
||||
var (
|
||||
// Ensure these interfaces are implemented:
|
||||
_ driver.DriverContext = &SQLite{}
|
||||
)
|
||||
|
||||
// Open implements [database/sql/driver.Driver].
|
||||
func (d *SQLite) Open(name string) (driver.Conn, error) {
|
||||
c, err := d.newConnector(name)
|
||||
@@ -119,10 +166,8 @@ func (d *SQLite) newConnector(name string) (*connector, error) {
|
||||
}
|
||||
|
||||
switch txlock {
|
||||
case "":
|
||||
c.txBegin = "BEGIN"
|
||||
case "deferred", "immediate", "exclusive":
|
||||
c.txBegin = "BEGIN " + txlock
|
||||
case "", "deferred", "concurrent", "immediate", "exclusive":
|
||||
c.txLock = txlock
|
||||
default:
|
||||
return nil, fmt.Errorf("sqlite3: invalid _txlock: %s", txlock)
|
||||
}
|
||||
@@ -147,7 +192,7 @@ func (d *SQLite) newConnector(name string) (*connector, error) {
|
||||
type connector struct {
|
||||
driver *SQLite
|
||||
name string
|
||||
txBegin string
|
||||
txLock string
|
||||
tmRead sqlite3.TimeFormat
|
||||
tmWrite sqlite3.TimeFormat
|
||||
pragmas bool
|
||||
@@ -159,7 +204,7 @@ func (n *connector) Driver() driver.Driver {
|
||||
|
||||
func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
c := &conn{
|
||||
txBegin: n.txBegin,
|
||||
txLock: n.txLock,
|
||||
tmRead: n.tmRead,
|
||||
tmWrite: n.tmWrite,
|
||||
}
|
||||
@@ -178,18 +223,18 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
|
||||
if !n.pragmas {
|
||||
err = c.Conn.BusyTimeout(60 * time.Second)
|
||||
err = c.Conn.BusyTimeout(time.Minute)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if n.driver.Init != nil {
|
||||
err = n.driver.Init(c.Conn)
|
||||
if n.driver.init != nil {
|
||||
err = n.driver.init(c.Conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if n.pragmas || n.driver.Init != nil {
|
||||
if n.pragmas || n.driver.init != nil {
|
||||
s, _, err := c.Conn.Prepare(`PRAGMA query_only`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -204,57 +249,93 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if n.driver.term != nil {
|
||||
err = c.Conn.Trace(sqlite3.TRACE_CLOSE, func(sqlite3.TraceEvent, any, any) error {
|
||||
return n.driver.term(c.Conn)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Conn is implemented by the SQLite [database/sql] driver connections.
|
||||
//
|
||||
// It can be used to access SQLite features like [online backup]:
|
||||
//
|
||||
// db, err := driver.Open("temp.db")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer db.Close()
|
||||
//
|
||||
// conn, err := db.Conn(context.TODO())
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// err = conn.Raw(func(driverConn any) error {
|
||||
// conn := driverConn.(driver.Conn)
|
||||
// return conn.Raw().Backup("main", "backup.db")
|
||||
// })
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// [online backup]: https://sqlite.org/backup.html
|
||||
type Conn interface {
|
||||
Raw() *sqlite3.Conn
|
||||
driver.Conn
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
*sqlite3.Conn
|
||||
txBegin string
|
||||
txCommit string
|
||||
txRollback string
|
||||
tmRead sqlite3.TimeFormat
|
||||
tmWrite sqlite3.TimeFormat
|
||||
readOnly byte
|
||||
txLock string
|
||||
txReset string
|
||||
tmRead sqlite3.TimeFormat
|
||||
tmWrite sqlite3.TimeFormat
|
||||
readOnly byte
|
||||
}
|
||||
|
||||
var (
|
||||
// Ensure these interfaces are implemented:
|
||||
_ Conn = &conn{}
|
||||
_ driver.ConnBeginTx = &conn{}
|
||||
_ driver.ConnPrepareContext = &conn{}
|
||||
_ driver.ExecerContext = &conn{}
|
||||
_ driver.ConnBeginTx = &conn{}
|
||||
_ sqlite3.DriverConn = &conn{}
|
||||
)
|
||||
|
||||
func (c *conn) Raw() *sqlite3.Conn {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
// Deprecated: use BeginTx instead.
|
||||
func (c *conn) Begin() (driver.Tx, error) {
|
||||
// notest
|
||||
return c.BeginTx(context.Background(), driver.TxOptions{})
|
||||
}
|
||||
|
||||
func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
||||
txBegin := c.txBegin
|
||||
c.txCommit = `COMMIT`
|
||||
c.txRollback = `ROLLBACK`
|
||||
|
||||
if opts.ReadOnly {
|
||||
txBegin = `
|
||||
BEGIN deferred;
|
||||
PRAGMA query_only=on`
|
||||
c.txRollback = `
|
||||
ROLLBACK;
|
||||
PRAGMA query_only=` + string(c.readOnly)
|
||||
c.txCommit = c.txRollback
|
||||
}
|
||||
|
||||
var txLock string
|
||||
switch opts.Isolation {
|
||||
default:
|
||||
return nil, util.IsolationErr
|
||||
case
|
||||
driver.IsolationLevel(sql.LevelDefault),
|
||||
driver.IsolationLevel(sql.LevelSerializable):
|
||||
break
|
||||
case driver.IsolationLevel(sql.LevelLinearizable):
|
||||
txLock = "exclusive"
|
||||
case driver.IsolationLevel(sql.LevelSerializable):
|
||||
txLock = "immediate"
|
||||
case driver.IsolationLevel(sql.LevelDefault):
|
||||
if !opts.ReadOnly {
|
||||
txLock = c.txLock
|
||||
}
|
||||
}
|
||||
|
||||
c.txReset = ``
|
||||
txBegin := `BEGIN ` + txLock
|
||||
if opts.ReadOnly {
|
||||
txBegin += ` ; PRAGMA query_only=on`
|
||||
c.txReset = `; PRAGMA query_only=` + string(c.readOnly)
|
||||
}
|
||||
|
||||
old := c.Conn.SetInterrupt(ctx)
|
||||
@@ -268,7 +349,7 @@ func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, e
|
||||
}
|
||||
|
||||
func (c *conn) Commit() error {
|
||||
err := c.Conn.Exec(c.txCommit)
|
||||
err := c.Conn.Exec(`COMMIT` + c.txReset)
|
||||
if err != nil && !c.Conn.GetAutocommit() {
|
||||
c.Rollback()
|
||||
}
|
||||
@@ -276,16 +357,17 @@ func (c *conn) Commit() error {
|
||||
}
|
||||
|
||||
func (c *conn) Rollback() error {
|
||||
err := c.Conn.Exec(c.txRollback)
|
||||
err := c.Conn.Exec(`ROLLBACK` + c.txReset)
|
||||
if errors.Is(err, sqlite3.INTERRUPT) {
|
||||
old := c.Conn.SetInterrupt(context.Background())
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
err = c.Conn.Exec(c.txRollback)
|
||||
err = c.Conn.Exec(`ROLLBACK` + c.txReset)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) Prepare(query string) (driver.Stmt, error) {
|
||||
// notest
|
||||
return c.PrepareContext(context.Background(), query)
|
||||
}
|
||||
|
||||
@@ -301,7 +383,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) {
|
||||
@@ -328,6 +410,8 @@ func (c *conn) ExecContext(ctx context.Context, query string, args []driver.Name
|
||||
}
|
||||
|
||||
func (c *conn) CheckNamedValue(arg *driver.NamedValue) error {
|
||||
// Fast path: short circuit argument verification.
|
||||
// Arguments will be rejected by conn.ExecContext.
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -335,6 +419,7 @@ type stmt struct {
|
||||
*sqlite3.Stmt
|
||||
tmWrite sqlite3.TimeFormat
|
||||
tmRead sqlite3.TimeFormat
|
||||
inputs int
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -345,22 +430,29 @@ 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
|
||||
}
|
||||
|
||||
// Deprecated: use ExecContext instead.
|
||||
func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {
|
||||
// notest
|
||||
return s.ExecContext(context.Background(), namedValues(args))
|
||||
}
|
||||
|
||||
// Deprecated: use QueryContext instead.
|
||||
func (s *stmt) Query(args []driver.Value) (driver.Rows, error) {
|
||||
// notest
|
||||
return s.QueryContext(context.Background(), namedValues(args))
|
||||
}
|
||||
|
||||
@@ -389,12 +481,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 +645,21 @@ func (r *rows) Next(dest []driver.Value) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rows) decodeTime(i int, v any) (_ time.Time, _ bool) {
|
||||
if r.tmRead == sqlite3.TimeFormatDefault {
|
||||
return
|
||||
}
|
||||
switch r.declType(i) {
|
||||
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
|
||||
// maybe
|
||||
default:
|
||||
func (r *rows) decodeTime(i int, v any) (_ time.Time, ok bool) {
|
||||
switch r.tmRead {
|
||||
case sqlite3.TimeFormatDefault, time.RFC3339Nano:
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"errors"
|
||||
"math"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -15,9 +14,21 @@ import (
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func Test_Open_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := Open("", nil, nil, nil)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
if !errors.Is(err, sqlite3.MISUSE) {
|
||||
t.Errorf("got %v, want sqlite3.MISUSE", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Open_dir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -38,8 +49,11 @@ func Test_Open_dir(t *testing.T) {
|
||||
|
||||
func Test_Open_pragma(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
"_pragma": {"busy_timeout(1000)"},
|
||||
})
|
||||
|
||||
db, err := sql.Open("sqlite3", "file::memory:?_pragma=busy_timeout(1000)")
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -57,8 +71,11 @@ func Test_Open_pragma(t *testing.T) {
|
||||
|
||||
func Test_Open_pragma_invalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
"_pragma": {"busy_timeout 1000"},
|
||||
})
|
||||
|
||||
db, err := sql.Open("sqlite3", "file::memory:?_pragma=busy_timeout+1000")
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -81,14 +98,13 @@ func Test_Open_pragma_invalid(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_Open_txLock(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
"_txlock": {"exclusive"},
|
||||
"_pragma": {"busy_timeout(1000)"},
|
||||
})
|
||||
|
||||
db, err := sql.Open("sqlite3", "file:"+
|
||||
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db"))+
|
||||
"?_txlock=exclusive&_pragma=busy_timeout(0)")
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -119,8 +135,11 @@ func Test_Open_txLock(t *testing.T) {
|
||||
|
||||
func Test_Open_txLock_invalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
"_txlock": {"xclusive"},
|
||||
})
|
||||
|
||||
_, err := sql.Open("sqlite3", "file::memory:?_txlock=xclusive")
|
||||
_, err := sql.Open("sqlite3", tmp+"_txlock=xclusive")
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
}
|
||||
@@ -130,17 +149,16 @@ func Test_Open_txLock_invalid(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_BeginTx(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
"_txlock": {"exclusive"},
|
||||
"_pragma": {"busy_timeout(0)"},
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
db, err := sql.Open("sqlite3", "file:"+
|
||||
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db"))+
|
||||
"?_txlock=exclusive&_pragma=busy_timeout(0)")
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -182,8 +200,9 @@ func Test_BeginTx(t *testing.T) {
|
||||
|
||||
func Test_Prepare(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -222,11 +241,12 @@ func Test_Prepare(t *testing.T) {
|
||||
|
||||
func Test_QueryRow_named(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -274,8 +294,9 @@ func Test_QueryRow_named(t *testing.T) {
|
||||
|
||||
func Test_QueryRow_blob_null(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -310,7 +331,11 @@ func Test_time(t *testing.T) {
|
||||
|
||||
for _, fmt := range []string{"auto", "sqlite", "rfc3339", time.ANSIC} {
|
||||
t.Run(fmt, func(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", "file::memory:?_timefmt="+url.QueryEscape(fmt))
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
"_timefmt": {fmt},
|
||||
})
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build (linux || darwin || windows || freebsd || illumos) && !sqlite3_nosys
|
||||
//go:build (linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock) && !sqlite3_nosys
|
||||
|
||||
package driver_test
|
||||
|
||||
@@ -6,12 +6,16 @@ package driver_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
@@ -149,3 +153,129 @@ func addAlbum(alb Album) (int64, error) {
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func Example_customTime() {
|
||||
db, err := sql.Open("sqlite3", "file:/time.db?vfs=memdb")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE data (
|
||||
id INTEGER PRIMARY KEY,
|
||||
date_time TEXT
|
||||
) STRICT;
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// This one will be returned as string to [sql.Scanner] because it doesn't
|
||||
// pass the driver's round-trip test when it tries to figure out if it's
|
||||
// a time. 2009-11-17T20:34:58.650Z goes in, but parsing and formatting
|
||||
// it with [time.RFC3338Nano] results in 2009-11-17T20:34:58.65Z. Though
|
||||
// the times are identical, the trailing zero is lost in the string
|
||||
// representation so the driver considers the conversion unsuccessful.
|
||||
c1 := CustomTime{time.Date(
|
||||
2009, 11, 17, 20, 34, 58, 650000000, time.UTC)}
|
||||
|
||||
// Store our custom time in the database.
|
||||
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var strc1 string
|
||||
// Retrieve it as a string, the result of Value().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&strc1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("in db:", strc1)
|
||||
|
||||
var resc1 CustomTime
|
||||
// Retrieve it as our custom time type, going through Scan().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&resc1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("custom time:", resc1)
|
||||
|
||||
// This one will be returned as [time.Time] to [sql.Scanner] because it does
|
||||
// pass the driver's round-trip test when it tries to figure out if it's
|
||||
// a time. 2009-11-17T20:34:58.651Z goes in, and parsing and formatting
|
||||
// it with [time.RFC3339Nano] results in 2009-11-17T20:34:58.651Z.
|
||||
c2 := CustomTime{time.Date(
|
||||
2009, 11, 17, 20, 34, 58, 651000000, time.UTC)}
|
||||
// Store our custom time in the database.
|
||||
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var strc2 string
|
||||
// Retrieve it as a string, the result of Value().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&strc2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("in db:", strc2)
|
||||
|
||||
var resc2 CustomTime
|
||||
// Retrieve it as our custom time type, going through Scan().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&resc2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("custom time:", resc2)
|
||||
// Output:
|
||||
// in db: 2009-11-17T20:34:58.650Z
|
||||
// scan type string: 2009-11-17T20:34:58.650Z
|
||||
// custom time: 2009-11-17 20:34:58.65 +0000 UTC
|
||||
// in db: 2009-11-17T20:34:58.651Z
|
||||
// scan type time: 2009-11-17 20:34:58.651 +0000 UTC
|
||||
// custom time: 2009-11-17 20:34:58.651 +0000 UTC
|
||||
}
|
||||
|
||||
type CustomTime struct{ time.Time }
|
||||
|
||||
func (c CustomTime) Value() (driver.Value, error) {
|
||||
return sqlite3.TimeFormat7TZ.Encode(c.UTC()), nil
|
||||
}
|
||||
|
||||
func (c *CustomTime) Scan(value any) error {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
*c = CustomTime{time.Time{}}
|
||||
case time.Time:
|
||||
fmt.Println("scan type time:", v)
|
||||
*c = CustomTime{v}
|
||||
case string:
|
||||
fmt.Println("scan type string:", v)
|
||||
t, err := sqlite3.TimeFormat7TZ.Decode(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*c = CustomTime{t}
|
||||
default:
|
||||
panic("unsupported value type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func Example_json() {
|
||||
db, err := driver.Open("file:/test.db?vfs=memdb", nil)
|
||||
db, err := driver.Open("file:/json.db?vfs=memdb")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -21,8 +21,8 @@ func Example_json() {
|
||||
CREATE TABLE orders (
|
||||
cart_id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
cart TEXT
|
||||
);
|
||||
cart BLOB -- stored as JSONB
|
||||
) STRICT;
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -39,7 +39,8 @@ func Example_json() {
|
||||
Items []CartItem `json:"items"`
|
||||
}
|
||||
|
||||
_, err = db.Exec(`INSERT INTO orders (user_id, cart) VALUES (?, ?)`, 123, sqlite3.JSON(Cart{
|
||||
// convert to JSONB on insertion
|
||||
_, err = db.Exec(`INSERT INTO orders (user_id, cart) VALUES (?, jsonb(?))`, 123, sqlite3.JSON(Cart{
|
||||
[]CartItem{
|
||||
{ItemID: "111", Name: "T-shirt", Quantity: 1, Price: 250},
|
||||
{ItemID: "222", Name: "Trousers", Quantity: 1, Price: 600},
|
||||
@@ -60,6 +61,24 @@ func Example_json() {
|
||||
}
|
||||
|
||||
fmt.Println("total:", total)
|
||||
|
||||
var cart Cart
|
||||
err = db.QueryRow(`
|
||||
SELECT json(cart) -- convert to JSON on retrieval
|
||||
FROM orders
|
||||
WHERE cart_id = last_insert_rowid()
|
||||
`).Scan(sqlite3.JSON(&cart))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, item := range cart.Items {
|
||||
fmt.Printf("id: %s, name: %s, quantity: %d, price: %d\n",
|
||||
item.ItemID, item.Name, item.Quantity, item.Price)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// total: 850
|
||||
// id: 111, name: T-shirt, quantity: 1, price: 250
|
||||
// id: 222, name: Trousers, quantity: 1, price: 600
|
||||
}
|
||||
|
||||
@@ -16,12 +16,25 @@ func Savepoint(tx *sql.Tx) sqlite3.Savepoint {
|
||||
return ctx.Savepoint
|
||||
}
|
||||
|
||||
// A saveptCtx is never canceled, has no values, and has no deadline.
|
||||
type saveptCtx struct{ sqlite3.Savepoint }
|
||||
|
||||
func (*saveptCtx) Deadline() (deadline time.Time, ok bool) { return }
|
||||
func (*saveptCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
// notest
|
||||
return
|
||||
}
|
||||
|
||||
func (*saveptCtx) Done() <-chan struct{} { return nil }
|
||||
func (*saveptCtx) Done() <-chan struct{} {
|
||||
// notest
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*saveptCtx) Err() error { return nil }
|
||||
func (*saveptCtx) Err() error {
|
||||
// notest
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*saveptCtx) Value(key any) any { return nil }
|
||||
func (*saveptCtx) Value(key any) any {
|
||||
// notest
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func ExampleSavepoint() {
|
||||
db, err := driver.Open("file:/test.db?vfs=memdb", nil)
|
||||
db, err := driver.Open("file:/svpt.db?vfs=memdb")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Embeddable Wasm build of SQLite
|
||||
|
||||
This folder includes an embeddable Wasm build of SQLite 3.46.0 for use with
|
||||
This folder includes an embeddable Wasm build of SQLite 3.46.1 for use with
|
||||
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
|
||||
|
||||
The following optional features are compiled in:
|
||||
@@ -17,11 +17,24 @@ The following optional features are compiled in:
|
||||
- [regexp](https://github.com/sqlite/sqlite/blob/master/ext/misc/regexp.c)
|
||||
- [series](https://github.com/sqlite/sqlite/blob/master/ext/misc/series.c)
|
||||
- [uint](https://github.com/sqlite/sqlite/blob/master/ext/misc/uint.c)
|
||||
- [uuid](https://github.com/sqlite/sqlite/blob/master/ext/misc/uuid.c)
|
||||
- [time](../sqlite3/time.c)
|
||||
|
||||
See the [configuration options](../sqlite3/sqlite_cfg.h),
|
||||
See the [configuration options](../sqlite3/sqlite_opt.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).
|
||||
|
||||
### Customizing the build
|
||||
|
||||
You can use your own custom build of SQLite.
|
||||
|
||||
Examples of custom builds of SQLite are:
|
||||
- [`github.com/ncruces/go-sqlite3/embed/bcw2`](https://github.com/ncruces/go-sqlite3/tree/main/embed/bcw2)
|
||||
built from a branch supporting [`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md)
|
||||
and [Wal2](https://www.sqlite.org/cgi/src/doc/wal2/doc/wal2.md).
|
||||
- [`github.com/asg017/sqlite-vec-go-bindings/ncruces`](https://github.com/asg017/sqlite-vec-go-bindings)
|
||||
which includes the [`sqlite-vec`](https://github.com/asg017/sqlite-vec) vector search extension.
|
||||
2
embed/bcw2/.gitignore
vendored
Normal file
2
embed/bcw2/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
build/
|
||||
sqlite/
|
||||
16
embed/bcw2/README.md
Normal file
16
embed/bcw2/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Embeddable Wasm build of SQLite
|
||||
|
||||
This folder includes an embeddable Wasm build of SQLite 3.46.1, including the experimental
|
||||
[`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md) and
|
||||
[Wal2](https://www.sqlite.org/cgi/src/doc/wal2/doc/wal2.md) patches.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This package is experimental.
|
||||
> It is built from the `bedrock` branch of SQLite,
|
||||
> since that is _currently_ the most stable, maintained branch to include both features.
|
||||
|
||||
> [!CAUTION]
|
||||
> The Wal2 journaling mode creates databases that other versions of SQLite cannot access.
|
||||
|
||||
The build is easily reproducible, and verifiable, using
|
||||
[Artifact Attestations](https://github.com/ncruces/go-sqlite3/attestations).
|
||||
BIN
embed/bcw2/bcw2.wasm
Executable file
BIN
embed/bcw2/bcw2.wasm
Executable file
Binary file not shown.
48
embed/bcw2/bcw2_test.go
Normal file
48
embed/bcw2/bcw2_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package bcw2
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
)
|
||||
|
||||
func Test_bcw2(t *testing.T) {
|
||||
if !vfs.SupportsSharedMemory {
|
||||
t.Skip("skipping without shared memory")
|
||||
}
|
||||
|
||||
tmp := filepath.ToSlash(filepath.Join(t.TempDir(), "test.db"))
|
||||
|
||||
db, err := driver.Open("file:" + tmp + "?_pragma=journal_mode(wal2)&_txlock=concurrent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
_, err = tx.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var version string
|
||||
err = db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != "3.46.1" {
|
||||
t.Error(version)
|
||||
}
|
||||
}
|
||||
63
embed/bcw2/build.sh
Executable file
63
embed/bcw2/build.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
ROOT=../../
|
||||
BINARYEN="$ROOT/tools/binaryen/bin"
|
||||
WASI_SDK="$ROOT/tools/wasi-sdk/bin"
|
||||
|
||||
trap 'rm -rf build/ sqlite/ bcw2.tmp' EXIT
|
||||
|
||||
mkdir -p build/ext/
|
||||
cp "$ROOT"/sqlite3/*.[ch] build/
|
||||
cp "$ROOT"/sqlite3/*.patch build/
|
||||
|
||||
curl -# https://www.sqlite.org/src/tarball/sqlite.tar.gz?r=bedrock-3.46 | tar xz
|
||||
|
||||
cd sqlite
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c
|
||||
else
|
||||
sh configure
|
||||
make sqlite3.c
|
||||
fi
|
||||
cd ~-
|
||||
|
||||
mv sqlite/sqlite3.c build/
|
||||
mv sqlite/sqlite3.h build/
|
||||
mv sqlite/sqlite3ext.h build/
|
||||
mv sqlite/ext/misc/anycollseq.c build/ext/
|
||||
mv sqlite/ext/misc/base64.c build/ext/
|
||||
mv sqlite/ext/misc/decimal.c build/ext/
|
||||
mv sqlite/ext/misc/ieee754.c build/ext/
|
||||
mv sqlite/ext/misc/regexp.c build/ext/
|
||||
mv sqlite/ext/misc/series.c build/ext/
|
||||
mv sqlite/ext/misc/uint.c build/ext/
|
||||
|
||||
cd build
|
||||
cat *.patch | patch --no-backup-if-mismatch
|
||||
cd ~-
|
||||
|
||||
"$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -g0 -O2 \
|
||||
-Wall -Wextra -Wno-unused-parameter -Wno-unused-function \
|
||||
-o bcw2.wasm "build/main.c" \
|
||||
-I"build" \
|
||||
-mexec-model=reactor \
|
||||
-matomics -msimd128 -mmutable-globals \
|
||||
-mbulk-memory -mreference-types \
|
||||
-mnontrapping-fptoint -msign-ext \
|
||||
-fno-stack-protector -fno-stack-clash-protection \
|
||||
-Wl,--stack-first \
|
||||
-Wl,--import-undefined \
|
||||
-Wl,--initial-memory=327680 \
|
||||
-D_HAVE_SQLITE_CONFIG_H \
|
||||
-DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \
|
||||
$(awk '{print "-Wl,--export="$0}' ../exports.txt)
|
||||
|
||||
"$BINARYEN/wasm-ctor-eval" -g -c _initialize bcw2.wasm -o bcw2.tmp
|
||||
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
|
||||
bcw2.tmp -o bcw2.wasm \
|
||||
--enable-simd --enable-mutable-globals --enable-multivalue \
|
||||
--enable-bulk-memory --enable-reference-types \
|
||||
--enable-nontrapping-float-to-int --enable-sign-ext
|
||||
23
embed/bcw2/init.go
Normal file
23
embed/bcw2/init.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Package bcw2 embeds SQLite into your application.
|
||||
//
|
||||
// Importing package bcw2 initializes the [sqlite3.Binary] variable
|
||||
// with a build of SQLite that includes the [BEGIN CONCURRENT] and [Wal2] patches:
|
||||
//
|
||||
// import _ "github.com/ncruces/go-sqlite3/embed/bcw2"
|
||||
//
|
||||
// [BEGIN CONCURRENT]: https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md
|
||||
// [Wal2]: https://www.sqlite.org/cgi/src/doc/wal2/doc/wal2.md
|
||||
package bcw2
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
//go:embed bcw2.wasm
|
||||
var binary []byte
|
||||
|
||||
func init() {
|
||||
sqlite3.Binary = binary
|
||||
}
|
||||
@@ -4,26 +4,27 @@ set -euo pipefail
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
ROOT=../
|
||||
BINARYEN="$ROOT/tools/binaryen-version_117/bin"
|
||||
WASI_SDK="$ROOT/tools/wasi-sdk-22.0/bin"
|
||||
BINARYEN="$ROOT/tools/binaryen/bin"
|
||||
WASI_SDK="$ROOT/tools/wasi-sdk/bin"
|
||||
|
||||
"$WASI_SDK/clang" --target=wasm32-wasi -std=c17 -flto -g0 -O2 \
|
||||
trap 'rm -f sqlite3.tmp' EXIT
|
||||
|
||||
"$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -g0 -O2 \
|
||||
-Wall -Wextra -Wno-unused-parameter -Wno-unused-function \
|
||||
-o sqlite3.wasm "$ROOT/sqlite3/main.c" \
|
||||
-I"$ROOT/sqlite3" \
|
||||
-mexec-model=reactor \
|
||||
-msimd128 -mmutable-globals \
|
||||
-matomics -msimd128 -mmutable-globals \
|
||||
-mbulk-memory -mreference-types \
|
||||
-mnontrapping-fptoint -msign-ext \
|
||||
-fno-stack-protector -fno-stack-clash-protection \
|
||||
-Wl,--initial-memory=327680 \
|
||||
-Wl,--stack-first \
|
||||
-Wl,--import-undefined \
|
||||
-Wl,--initial-memory=327680 \
|
||||
-D_HAVE_SQLITE_CONFIG_H \
|
||||
-DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \
|
||||
$(awk '{print "-Wl,--export="$0}' exports.txt)
|
||||
|
||||
trap 'rm -f sqlite3.tmp' EXIT
|
||||
"$BINARYEN/wasm-ctor-eval" -g -c _initialize sqlite3.wasm -o sqlite3.tmp
|
||||
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
|
||||
sqlite3.tmp -o sqlite3.wasm \
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
aligned_alloc
|
||||
free
|
||||
malloc
|
||||
malloc_destructor
|
||||
sqlite3_anycollseq_init
|
||||
sqlite3_autovacuum_pages_go
|
||||
sqlite3_backup_finish
|
||||
@@ -9,7 +6,7 @@ sqlite3_backup_init
|
||||
sqlite3_backup_pagecount
|
||||
sqlite3_backup_remaining
|
||||
sqlite3_backup_step
|
||||
sqlite3_bind_blob64
|
||||
sqlite3_bind_blob_go
|
||||
sqlite3_bind_double
|
||||
sqlite3_bind_int64
|
||||
sqlite3_bind_null
|
||||
@@ -17,7 +14,7 @@ sqlite3_bind_parameter_count
|
||||
sqlite3_bind_parameter_index
|
||||
sqlite3_bind_parameter_name
|
||||
sqlite3_bind_pointer_go
|
||||
sqlite3_bind_text64
|
||||
sqlite3_bind_text_go
|
||||
sqlite3_bind_value
|
||||
sqlite3_bind_zeroblob64
|
||||
sqlite3_blob_bytes
|
||||
@@ -55,32 +52,40 @@ sqlite3_create_function_go
|
||||
sqlite3_create_module_go
|
||||
sqlite3_create_window_function_go
|
||||
sqlite3_database_file_object
|
||||
sqlite3_db_cacheflush
|
||||
sqlite3_db_config
|
||||
sqlite3_db_filename
|
||||
sqlite3_db_name
|
||||
sqlite3_db_readonly
|
||||
sqlite3_db_release_memory
|
||||
sqlite3_db_status
|
||||
sqlite3_declare_vtab
|
||||
sqlite3_errcode
|
||||
sqlite3_errmsg
|
||||
sqlite3_error_offset
|
||||
sqlite3_errstr
|
||||
sqlite3_exec
|
||||
sqlite3_expanded_sql
|
||||
sqlite3_file_control
|
||||
sqlite3_filename_database
|
||||
sqlite3_filename_journal
|
||||
sqlite3_filename_wal
|
||||
sqlite3_finalize
|
||||
sqlite3_free
|
||||
sqlite3_get_autocommit
|
||||
sqlite3_get_auxdata
|
||||
sqlite3_hard_heap_limit64
|
||||
sqlite3_interrupt
|
||||
sqlite3_last_insert_rowid
|
||||
sqlite3_limit
|
||||
sqlite3_malloc64
|
||||
sqlite3_open_v2
|
||||
sqlite3_overload_function
|
||||
sqlite3_prepare_v3
|
||||
sqlite3_progress_handler_go
|
||||
sqlite3_realloc64
|
||||
sqlite3_reset
|
||||
sqlite3_result_blob64
|
||||
sqlite3_result_blob_go
|
||||
sqlite3_result_double
|
||||
sqlite3_result_error
|
||||
sqlite3_result_error_code
|
||||
@@ -89,27 +94,30 @@ sqlite3_result_error_toobig
|
||||
sqlite3_result_int64
|
||||
sqlite3_result_null
|
||||
sqlite3_result_pointer_go
|
||||
sqlite3_result_text64
|
||||
sqlite3_result_text_go
|
||||
sqlite3_result_value
|
||||
sqlite3_result_zeroblob64
|
||||
sqlite3_rollback_hook_go
|
||||
sqlite3_set_authorizer_go
|
||||
sqlite3_set_auxdata_go
|
||||
sqlite3_set_last_insert_rowid
|
||||
sqlite3_soft_heap_limit64
|
||||
sqlite3_step
|
||||
sqlite3_stmt_busy
|
||||
sqlite3_stmt_readonly
|
||||
sqlite3_stmt_status
|
||||
sqlite3_table_column_metadata
|
||||
sqlite3_total_changes64
|
||||
sqlite3_trace_go
|
||||
sqlite3_txn_state
|
||||
sqlite3_update_hook_go
|
||||
sqlite3_uri_key
|
||||
sqlite3_uri_parameter
|
||||
sqlite3_value_blob
|
||||
sqlite3_value_bytes
|
||||
sqlite3_value_double
|
||||
sqlite3_value_dup
|
||||
sqlite3_value_free
|
||||
sqlite3_value_frombind
|
||||
sqlite3_value_int64
|
||||
sqlite3_value_nochange
|
||||
sqlite3_value_numeric_type
|
||||
|
||||
25
embed/init_test.go
Normal file
25
embed/init_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package embed
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func Test_init(t *testing.T) {
|
||||
db, err := driver.Open("file:/test.db?vfs=memdb")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var version string
|
||||
err = db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != "3.46.1" {
|
||||
t.Error(version)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -2,6 +2,7 @@ package sqlite3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
|
||||
func Test_assertErr(t *testing.T) {
|
||||
err := util.AssertErr()
|
||||
if s := err.Error(); !strings.HasPrefix(s, "sqlite3: assertion failed") || !strings.HasSuffix(s, "error_test.go:12)") {
|
||||
if s := err.Error(); !strings.HasPrefix(s, "sqlite3: assertion failed") || !strings.HasSuffix(s, "error_test.go:13)") {
|
||||
t.Errorf("got %q", s)
|
||||
}
|
||||
}
|
||||
@@ -166,3 +167,32 @@ func Test_ExtendedErrorCode_Error(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_errorCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
arg error
|
||||
wantMsg string
|
||||
wantCode uint32
|
||||
}{
|
||||
{nil, "", _OK},
|
||||
{ERROR, "", util.ERROR},
|
||||
{IOERR, "", util.IOERR},
|
||||
{IOERR_READ, "", util.IOERR_READ},
|
||||
{&Error{code: util.ERROR}, "", util.ERROR},
|
||||
{fmt.Errorf("%w", ERROR), ERROR.Error(), util.ERROR},
|
||||
{fmt.Errorf("%w", IOERR), IOERR.Error(), util.IOERR},
|
||||
{fmt.Errorf("%w", IOERR_READ), IOERR_READ.Error(), util.IOERR_READ},
|
||||
{fmt.Errorf("error"), "error", util.ERROR},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
gotMsg, gotCode := errorCode(tt.arg, ERROR)
|
||||
if gotMsg != tt.wantMsg {
|
||||
t.Errorf("errorCode() gotMsg = %q, want %q", gotMsg, tt.wantMsg)
|
||||
}
|
||||
if gotCode != uint32(tt.wantCode) {
|
||||
t.Errorf("errorCode() gotCode = %d, want %d", gotCode, tt.wantCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
// The argument must be bound to a Go slice or array of
|
||||
// ints, floats, bools, strings or byte slices,
|
||||
// using [sqlite3.BindPointer] or [sqlite3.Pointer].
|
||||
func Register(db *sqlite3.Conn) {
|
||||
sqlite3.CreateModule[array](db, "array", nil,
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
return 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
|
||||
@@ -62,7 +62,7 @@ func (c *cursor) RowID() (int64, error) {
|
||||
return int64(c.rowID), nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
|
||||
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
|
||||
if n != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,13 +12,11 @@ import (
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/array"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func Example_driver() {
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
array.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open("file:/test.db?vfs=memdb", array.Register)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -53,14 +51,14 @@ func Example_driver() {
|
||||
}
|
||||
|
||||
func Example() {
|
||||
sqlite3.AutoExtension(array.Register)
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
array.Register(db)
|
||||
|
||||
stmt, _, err := db.Prepare(`
|
||||
SELECT name
|
||||
FROM pragma_function_list
|
||||
@@ -90,11 +88,9 @@ func Example() {
|
||||
|
||||
func Test_cursor_Column(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
array.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, array.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -126,7 +122,7 @@ func Test_cursor_Column(t *testing.T) {
|
||||
want = want[1:]
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +135,10 @@ func Test_array_errors(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
array.Register(db)
|
||||
err = array.Register(db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT * FROM array()`)
|
||||
if err == nil {
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
|
||||
// Register registers the SQL functions:
|
||||
//
|
||||
// readblob(schema, table, column, rowid, offset, n)
|
||||
// readblob(schema, table, column, rowid, offset, n/writer)
|
||||
//
|
||||
// Reads n bytes of a blob, starting at offset.
|
||||
//
|
||||
// writeblob(schema, table, column, rowid, offset, data)
|
||||
// writeblob(schema, table, column, rowid, offset, data/reader)
|
||||
//
|
||||
// Writes data into a blob, at the given offset.
|
||||
//
|
||||
@@ -27,12 +27,17 @@ import (
|
||||
// using [sqlite3.BindPointer] or [sqlite3.Pointer].
|
||||
// The optional args will be passed to the callback,
|
||||
// along with the [sqlite3.Blob] handle.
|
||||
// The [sqlite3.Blob] handle is only valid during
|
||||
// the execution of the callback. Callers cannot
|
||||
// read or write to the handle after the callback
|
||||
// exits.
|
||||
//
|
||||
// https://sqlite.org/c3ref/blob.html
|
||||
func Register(db *sqlite3.Conn) {
|
||||
db.CreateFunction("readblob", 6, 0, readblob)
|
||||
db.CreateFunction("writeblob", 6, 0, writeblob)
|
||||
db.CreateFunction("openblob", -1, 0, openblob)
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
return errors.Join(
|
||||
db.CreateFunction("readblob", 6, 0, readblob),
|
||||
db.CreateFunction("writeblob", 6, 0, writeblob),
|
||||
db.CreateFunction("openblob", -1, 0, openblob))
|
||||
}
|
||||
|
||||
// OpenCallback is the type for the openblob callback.
|
||||
@@ -42,28 +47,33 @@ func readblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
blob, err := getAuxBlob(ctx, arg, false)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
|
||||
_, err = blob.Seek(arg[4].Int64(), io.SeekStart)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
|
||||
n := arg[5].Int64()
|
||||
if n <= 0 {
|
||||
return
|
||||
if p, ok := arg[5].Pointer().(io.Writer); ok {
|
||||
var n int64
|
||||
n, err = blob.WriteTo(p)
|
||||
ctx.ResultInt64(n)
|
||||
} else {
|
||||
n := arg[5].Int64()
|
||||
if n <= 0 {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
_, err = io.ReadFull(blob, buf)
|
||||
ctx.ResultBlob(buf)
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
|
||||
_, err = io.ReadFull(blob, buf)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
|
||||
ctx.ResultBlob(buf)
|
||||
setAuxBlob(ctx, blob, false)
|
||||
}
|
||||
|
||||
@@ -71,19 +81,25 @@ func writeblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
blob, err := getAuxBlob(ctx, arg, true)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
|
||||
_, err = blob.Seek(arg[4].Int64(), io.SeekStart)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
|
||||
_, err = blob.Write(arg[5].RawBlob())
|
||||
if p, ok := arg[5].Pointer().(io.Reader); ok {
|
||||
var n int64
|
||||
n, err = blob.ReadFrom(p)
|
||||
ctx.ResultInt64(n)
|
||||
} else {
|
||||
_, err = blob.Write(arg[5].RawBlob())
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
|
||||
setAuxBlob(ctx, blob, false)
|
||||
@@ -98,14 +114,14 @@ func openblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
blob, err := getAuxBlob(ctx, arg, arg[4].Bool())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
|
||||
fn := arg[5].Pointer().(OpenCallback)
|
||||
err = fn(blob, arg[6:]...)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
|
||||
setAuxBlob(ctx, blob, true)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
@@ -18,10 +19,7 @@ import (
|
||||
|
||||
func Example() {
|
||||
// Open the database, registering the extension.
|
||||
db, err := driver.Open("file:/test.db?vfs=memdb", func(conn *sqlite3.Conn) error {
|
||||
blobio.Register(conn)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open("file:/test.db?vfs=memdb", blobio.Register)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -36,23 +34,26 @@ func Example() {
|
||||
const message = "Hello BLOB!"
|
||||
|
||||
// Create the BLOB.
|
||||
_, err = db.Exec(`INSERT INTO test VALUES (?)`, sqlite3.ZeroBlob(len(message)))
|
||||
r, err := db.Exec(`INSERT INTO test VALUES (?)`, sqlite3.ZeroBlob(len(message)))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := r.LastInsertId()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Write the BLOB.
|
||||
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', last_insert_rowid(), 0, ?)`, message)
|
||||
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', ?, 0, ?)`,
|
||||
id, message)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Read the BLOB.
|
||||
_, err = db.Exec(`SELECT openblob('main', 'test', 'col', rowid, false, ?) FROM test`,
|
||||
sqlite3.Pointer[blobio.OpenCallback](func(blob *sqlite3.Blob, _ ...sqlite3.Value) error {
|
||||
_, err = io.Copy(os.Stdout, blob)
|
||||
return err
|
||||
}))
|
||||
_, err = db.Exec(`SELECT readblob('main', 'test', 'col', ?, 0, ?)`,
|
||||
id, sqlite3.Pointer(os.Stdout))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -60,6 +61,12 @@ func Example() {
|
||||
// Hello BLOB!
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(blobio.Register)
|
||||
sqlite3.AutoExtension(array.Register)
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func Test_readblob(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -69,9 +76,6 @@ func Test_readblob(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
blobio.Register(db)
|
||||
array.Register(db)
|
||||
|
||||
err = db.Exec(`SELECT readblob()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
@@ -79,44 +83,135 @@ func Test_readblob(t *testing.T) {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT readblob('main', 'test1', 'col', 1, 1, 1)`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE test1 (col);
|
||||
CREATE TABLE test2 (col);
|
||||
INSERT INTO test1 VALUES (x'cafe');
|
||||
INSERT INTO test1 VALUES (x'dead');
|
||||
INSERT INTO test2 VALUES (x'babe');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT readblob('main', value, 'col', 1, 1, 1) FROM array(?)`)
|
||||
err = db.Exec(`SELECT readblob('main', 'test1', 'col', 1, -1, 1)`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT readblob('main', 'test1', 'col', 1, 1, 0)`)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sql string
|
||||
want1 string
|
||||
want2 string
|
||||
}{
|
||||
{"rows", `SELECT readblob('main', 'test1', 'col', rowid, 1, 1) FROM test1`, "\xfe", "\xad"},
|
||||
{"tables", `SELECT readblob('main', value, 'col', 1, 1, 1) FROM array(?)`, "\xfe", "\xbe"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
stmt, _, err := db.Prepare(tt.sql)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.BindCount() == 1 {
|
||||
err = stmt.BindPointer(1, []string{"test1", "test2"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnText(0)
|
||||
if got != tt.want1 {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnText(0)
|
||||
if got != tt.want2 {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
err = stmt.Err()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_writeblob(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`SELECT writeblob()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT writeblob('main', 'test', 'col', 1, 1, x'')`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE test (col);
|
||||
INSERT INTO test VALUES (x'cafe');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT writeblob('main', 'test', 'col', 1, -1, x'')`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT writeblob('main', 'test', 'col', 1, 0, ?)`)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
err = stmt.BindPointer(1, []string{"test1", "test2"})
|
||||
err = stmt.BindPointer(1, strings.NewReader("\xba\xbe"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnText(0)
|
||||
if got != "\xfe" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnText(0)
|
||||
if got != "\xbe" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
err = stmt.Err()
|
||||
err = stmt.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Log(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,9 +224,6 @@ func Test_openblob(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
blobio.Register(db)
|
||||
array.Register(db)
|
||||
|
||||
err = db.Exec(`SELECT openblob()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
@@ -139,6 +231,13 @@ func Test_openblob(t *testing.T) {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT openblob('main', 'test1', 'col', 1, false, NULL)`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE test1 (col);
|
||||
CREATE TABLE test2 (col);
|
||||
|
||||
349
ext/bloom/bloom.go
Normal file
349
ext/bloom/bloom.go
Normal file
@@ -0,0 +1,349 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/dchest/siphash"
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Register registers the bloom_filter virtual table:
|
||||
//
|
||||
// CREATE VIRTUAL TABLE foo USING bloom_filter(nElements, falseProb, kHashes)
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
return 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, util.ErrorString("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, util.ErrorString("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, util.ErrorString("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 {
|
||||
return nil, err
|
||||
}
|
||||
return nil, sqlite3.CORRUPT_VTAB
|
||||
}
|
||||
|
||||
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() {
|
||||
// notest // not meant to be called
|
||||
}
|
||||
|
||||
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 = util.ErrorString("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, util.ErrorString("bloom: elements cannot be deleted")
|
||||
}
|
||||
return 0, util.ErrorString("bloom: elements cannot be updated")
|
||||
}
|
||||
|
||||
if arg[2].NoChange() {
|
||||
return 0, nil
|
||||
}
|
||||
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
|
||||
arg *sqlite3.Value
|
||||
eof bool
|
||||
}
|
||||
|
||||
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 {
|
||||
if ctx.VTabNoChange() {
|
||||
return nil
|
||||
}
|
||||
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) {
|
||||
// notest // WITHOUT ROWID
|
||||
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
|
||||
}
|
||||
195
ext/bloom/bloom_test.go
Normal file
195
ext/bloom/bloom_test.go
Normal file
@@ -0,0 +1,195 @@
|
||||
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 TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(bloom.Register)
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE sports_cars USING bloom_filter();
|
||||
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(`DELETE FROM sports_cars WHERE word = 'lamborghini'`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
err = db.Exec(`UPDATE sports_cars SET word = 'ferrari' WHERE word = 'lamborghini'`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
err = db.Exec(`ALTER TABLE sports_cars RENAME TO fast_cars`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`DROP TABLE fast_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()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_errors(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(0)`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter('a')`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter(20, 2)`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter(20, 'a')`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter(20, 0.9, 0)`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter(20, 0.9, 'a')`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
@@ -12,22 +12,24 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/util/osutil"
|
||||
"github.com/ncruces/go-sqlite3/util/vtabutil"
|
||||
)
|
||||
|
||||
// Register registers the CSV virtual table.
|
||||
// If a filename is specified, [os.Open] is used to open the file.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
RegisterFS(db, osutil.FS{})
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
return RegisterFS(db, osutil.FS{})
|
||||
}
|
||||
|
||||
// RegisterFS registers the CSV virtual table.
|
||||
// If a filename is specified, fsys is used to open the file.
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
|
||||
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
|
||||
var (
|
||||
filename string
|
||||
@@ -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, util.ErrorString(`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)
|
||||
@@ -106,15 +118,17 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
return table, nil
|
||||
}
|
||||
|
||||
sqlite3.CreateModule(db, "csv", declare, declare)
|
||||
return sqlite3.CreateModule(db, "csv", declare, declare)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -224,9 +239,38 @@ func (c *cursor) RowID() (int64, error) {
|
||||
return c.rowID, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, col int) 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
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ func Example() {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
csv.Register(db)
|
||||
err = csv.Register(db)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE eurofxref USING csv(
|
||||
@@ -51,6 +54,11 @@ func Example() {
|
||||
// On Twosday, 1€ = $1.1342
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(csv.Register)
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -60,17 +68,17 @@ func TestRegister(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
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,7 +121,7 @@ Robert "Griesemer" "gri"`
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_errors(t *testing.T) {
|
||||
func TestAffinity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
@@ -122,7 +130,47 @@ func TestRegister_errors(t *testing.T) {
|
||||
}
|
||||
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()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv()`)
|
||||
if err == nil {
|
||||
|
||||
51
ext/csv/types.go
Normal file
51
ext/csv/types.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"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
|
||||
}
|
||||
|
||||
types := make([]affinity, len(tab.Columns))
|
||||
for i, col := range tab.Columns {
|
||||
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
|
||||
}
|
||||
32
ext/csv/types_test.go
Normal file
32
ext/csv/types_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package csv
|
||||
|
||||
import "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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !(go1.23 || goexperiment.rangefunc) || vet
|
||||
|
||||
package fileio
|
||||
|
||||
import (
|
||||
|
||||
@@ -14,24 +14,26 @@ import (
|
||||
|
||||
// Register registers SQL functions readfile, writefile, lsmode,
|
||||
// and the table-valued function fsdir.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
RegisterFS(db, nil)
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
return RegisterFS(db, nil)
|
||||
}
|
||||
|
||||
// Register registers SQL functions readfile, lsmode,
|
||||
// and the table-valued function fsdir;
|
||||
// fsys will be used to read files and list directories.
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
db.CreateFunction("lsmode", 1, sqlite3.DETERMINISTIC, lsmode)
|
||||
db.CreateFunction("readfile", 1, sqlite3.DIRECTONLY, readfile(fsys))
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
|
||||
var err error
|
||||
if fsys == nil {
|
||||
db.CreateFunction("writefile", -1, sqlite3.DIRECTONLY, writefile)
|
||||
err = db.CreateFunction("writefile", -1, sqlite3.DIRECTONLY, writefile)
|
||||
}
|
||||
sqlite3.CreateModule(db, "fsdir", nil, func(db *sqlite3.Conn, _, _, _ string, _ ...string) (fsdir, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(name,mode,mtime TIMESTAMP,data,path HIDDEN,dir HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
return fsdir{fsys}, err
|
||||
})
|
||||
return errors.Join(err,
|
||||
db.CreateFunction("readfile", 1, sqlite3.DIRECTONLY, readfile(fsys)),
|
||||
db.CreateFunction("lsmode", 1, sqlite3.DETERMINISTIC, lsmode),
|
||||
sqlite3.CreateModule(db, "fsdir", nil, func(db *sqlite3.Conn, _, _, _ string, _ ...string) (fsdir, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(name,mode,mtime TIMESTAMP,data,path HIDDEN,dir HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
return fsdir{fsys}, err
|
||||
}))
|
||||
}
|
||||
|
||||
func lsmode(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
@@ -53,7 +55,7 @@ func readfile(fsys fs.FS) func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
case err == nil:
|
||||
ctx.ResultBlob(data)
|
||||
case !errors.Is(err, fs.ErrNotExist):
|
||||
ctx.ResultError(fmt.Errorf("readfile: %w", err))
|
||||
ctx.ResultError(fmt.Errorf("readfile: %w", err)) // notest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,14 @@ import (
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/fileio"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func Test_lsmode(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
fileio.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, fileio.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -54,7 +53,9 @@ func Test_readfile(t *testing.T) {
|
||||
|
||||
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
|
||||
t.Run("", func(t *testing.T) {
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, func(c *sqlite3.Conn) error {
|
||||
fileio.RegisterFS(c, fsys)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -54,7 +54,7 @@ func (d fsdir) Open() (sqlite3.VTabCursor, error) {
|
||||
type cursor struct {
|
||||
fsdir
|
||||
base string
|
||||
resume func(struct{}) (entry, bool)
|
||||
resume resume
|
||||
cancel func()
|
||||
curr entry
|
||||
eof bool
|
||||
@@ -92,25 +92,14 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
c.base = base
|
||||
}
|
||||
|
||||
c.resume, c.cancel = coroNew(func(_ struct{}, yield func(entry) struct{}) entry {
|
||||
walkDir := func(path string, d fs.DirEntry, err error) error {
|
||||
yield(entry{d, err, path})
|
||||
return nil
|
||||
}
|
||||
if c.fsys != nil {
|
||||
fs.WalkDir(c.fsys, root, walkDir)
|
||||
} else {
|
||||
filepath.WalkDir(root, walkDir)
|
||||
}
|
||||
return entry{}
|
||||
})
|
||||
c.resume, c.cancel = pull(c, root)
|
||||
c.eof = false
|
||||
c.rowID = 0
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (c *cursor) Next() error {
|
||||
curr, ok := c.resume(struct{}{})
|
||||
curr, ok := next(c)
|
||||
c.curr = curr
|
||||
c.eof = !ok
|
||||
c.rowID++
|
||||
@@ -125,7 +114,7 @@ func (c *cursor) RowID() (int64, error) {
|
||||
return c.rowID, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
|
||||
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
|
||||
switch n {
|
||||
case 0: // name
|
||||
name := strings.TrimPrefix(c.curr.path, c.base)
|
||||
|
||||
29
ext/fileio/fsdir_coro.go
Normal file
29
ext/fileio/fsdir_coro.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build !(go1.23 || goexperiment.rangefunc) || vet
|
||||
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type resume = func(struct{}) (entry, bool)
|
||||
|
||||
func next(c *cursor) (entry, bool) {
|
||||
return c.resume(struct{}{})
|
||||
}
|
||||
|
||||
func pull(c *cursor, root string) (resume, func()) {
|
||||
return coroNew(func(_ struct{}, yield func(entry) struct{}) entry {
|
||||
walkDir := func(path string, d fs.DirEntry, err error) error {
|
||||
yield(entry{d, err, path})
|
||||
return nil
|
||||
}
|
||||
if c.fsys != nil {
|
||||
fs.WalkDir(c.fsys, root, walkDir)
|
||||
} else {
|
||||
filepath.WalkDir(root, walkDir)
|
||||
}
|
||||
return entry{}
|
||||
})
|
||||
}
|
||||
31
ext/fileio/fsdir_iter.go
Normal file
31
ext/fileio/fsdir_iter.go
Normal file
@@ -0,0 +1,31 @@
|
||||
//go:build (go1.23 || goexperiment.rangefunc) && !vet
|
||||
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"iter"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type resume = func() (entry, bool)
|
||||
|
||||
func next(c *cursor) (entry, bool) {
|
||||
return c.resume()
|
||||
}
|
||||
|
||||
func pull(c *cursor, root string) (resume, func()) {
|
||||
return iter.Pull(func(yield func(entry) bool) {
|
||||
walkDir := func(path string, d fs.DirEntry, err error) error {
|
||||
if yield(entry{d, err, path}) {
|
||||
return nil
|
||||
}
|
||||
return fs.SkipAll
|
||||
}
|
||||
if c.fsys != nil {
|
||||
fs.WalkDir(c.fsys, root, walkDir)
|
||||
} else {
|
||||
filepath.WalkDir(root, walkDir)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/fileio"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func Test_fsdir(t *testing.T) {
|
||||
@@ -20,7 +21,9 @@ func Test_fsdir(t *testing.T) {
|
||||
|
||||
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
|
||||
t.Run("", func(t *testing.T) {
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, func(c *sqlite3.Conn) error {
|
||||
fileio.RegisterFS(c, fsys)
|
||||
return nil
|
||||
})
|
||||
@@ -68,7 +71,10 @@ func Test_fsdir_errors(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
fileio.Register(db)
|
||||
err = fileio.Register(db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT name FROM fsdir()`)
|
||||
if err == nil {
|
||||
|
||||
@@ -29,7 +29,7 @@ func writefile(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
n, err := createFileAndDir(file, mode, arg[1])
|
||||
if err != nil {
|
||||
if len(arg) > 2 {
|
||||
ctx.ResultError(fmt.Errorf("writefile: %w", err))
|
||||
ctx.ResultError(fmt.Errorf("writefile: %w", err)) // notest
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func writefile(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
err := os.Chmod(file, mode.Perm())
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("writefile: %w", err))
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func writefile(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
err := os.Chtimes(file, time.Time{}, mtime)
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("writefile: %w", err))
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,17 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/memdb"
|
||||
)
|
||||
|
||||
func Test_writefile(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -21,47 +21,60 @@ package hash
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"errors"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Register registers cryptographic hash functions for a database connection.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
|
||||
var errs util.ErrorJoiner
|
||||
if crypto.MD4.Available() {
|
||||
db.CreateFunction("md4", 1, flags, md4Func)
|
||||
errs.Join(
|
||||
db.CreateFunction("md4", 1, flags, md4Func))
|
||||
}
|
||||
if crypto.MD5.Available() {
|
||||
db.CreateFunction("md5", 1, flags, md5Func)
|
||||
errs.Join(
|
||||
db.CreateFunction("md5", 1, flags, md5Func))
|
||||
}
|
||||
if crypto.SHA1.Available() {
|
||||
db.CreateFunction("sha1", 1, flags, sha1Func)
|
||||
errs.Join(
|
||||
db.CreateFunction("sha1", 1, flags, sha1Func))
|
||||
}
|
||||
if crypto.SHA3_512.Available() {
|
||||
db.CreateFunction("sha3", 1, flags, sha3Func)
|
||||
db.CreateFunction("sha3", 2, flags, sha3Func)
|
||||
errs.Join(
|
||||
db.CreateFunction("sha3", 1, flags, sha3Func),
|
||||
db.CreateFunction("sha3", 2, flags, sha3Func))
|
||||
}
|
||||
if crypto.SHA256.Available() {
|
||||
db.CreateFunction("sha224", 1, flags, sha224Func)
|
||||
db.CreateFunction("sha256", 1, flags, sha256Func)
|
||||
db.CreateFunction("sha256", 2, flags, sha256Func)
|
||||
errs.Join(
|
||||
db.CreateFunction("sha224", 1, flags, sha224Func),
|
||||
db.CreateFunction("sha256", 1, flags, sha256Func),
|
||||
db.CreateFunction("sha256", 2, flags, sha256Func))
|
||||
}
|
||||
if crypto.SHA512.Available() {
|
||||
db.CreateFunction("sha384", 1, flags, sha384Func)
|
||||
db.CreateFunction("sha512", 1, flags, sha512Func)
|
||||
db.CreateFunction("sha512", 2, flags, sha512Func)
|
||||
errs.Join(
|
||||
db.CreateFunction("sha384", 1, flags, sha384Func),
|
||||
db.CreateFunction("sha512", 1, flags, sha512Func),
|
||||
db.CreateFunction("sha512", 2, flags, sha512Func))
|
||||
}
|
||||
if crypto.BLAKE2s_256.Available() {
|
||||
db.CreateFunction("blake2s", 1, flags, blake2sFunc)
|
||||
errs.Join(
|
||||
db.CreateFunction("blake2s", 1, flags, blake2sFunc))
|
||||
}
|
||||
if crypto.BLAKE2b_512.Available() {
|
||||
db.CreateFunction("blake2b", 1, flags, blake2bFunc)
|
||||
db.CreateFunction("blake2b", 2, flags, blake2bFunc)
|
||||
errs.Join(
|
||||
db.CreateFunction("blake2b", 1, flags, blake2bFunc),
|
||||
db.CreateFunction("blake2b", 2, flags, blake2bFunc))
|
||||
}
|
||||
if crypto.RIPEMD160.Available() {
|
||||
db.CreateFunction("ripemd160", 1, flags, ripemd160Func)
|
||||
errs.Join(
|
||||
db.CreateFunction("ripemd160", 1, flags, ripemd160Func))
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func md4Func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
_ "crypto/sha512"
|
||||
"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/memdb"
|
||||
_ "golang.org/x/crypto/blake2b"
|
||||
_ "golang.org/x/crypto/blake2s"
|
||||
_ "golang.org/x/crypto/md4"
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -53,10 +54,7 @@ func TestRegister(t *testing.T) {
|
||||
{"blake2b('', 256)", "0E5751C026E543B2E8AB2EB06099DAA1D1E5DF47778F7787FAAB45CDF12FE3A8"},
|
||||
}
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ package lines
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
@@ -25,27 +26,28 @@ import (
|
||||
// The lines function reads from a database blob or text.
|
||||
// The lines_read function reads from a file or an [io.Reader].
|
||||
// If a filename is specified, [os.Open] is used to open the file.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
RegisterFS(db, osutil.FS{})
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
return RegisterFS(db, osutil.FS{})
|
||||
}
|
||||
|
||||
// RegisterFS registers the lines and lines_read table-valued functions.
|
||||
// The lines function reads from a database blob or text.
|
||||
// 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,
|
||||
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,
|
||||
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
return lines{fsys}, err
|
||||
})
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
|
||||
return errors.Join(
|
||||
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(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)
|
||||
return lines{fsys}, err
|
||||
}))
|
||||
}
|
||||
|
||||
type lines struct {
|
||||
@@ -89,7 +91,7 @@ func (c *cursor) RowID() (int64, error) {
|
||||
return c.rowID, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
|
||||
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
|
||||
if n == 0 {
|
||||
ctx.ResultRawText(c.line)
|
||||
}
|
||||
|
||||
@@ -15,13 +15,11 @@ import (
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/lines"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
lines.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open("file:/test.db?vfs=memdb", lines.Register)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -69,11 +67,9 @@ func Example() {
|
||||
|
||||
func Test_lines(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
lines.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, lines.Register)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -102,11 +98,9 @@ func Test_lines(t *testing.T) {
|
||||
|
||||
func Test_lines_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
lines.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, lines.Register)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -129,11 +123,9 @@ func Test_lines_error(t *testing.T) {
|
||||
|
||||
func Test_lines_read(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
lines.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, lines.Register)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -163,11 +155,9 @@ func Test_lines_read(t *testing.T) {
|
||||
|
||||
func Test_lines_test(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
lines.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, lines.Register)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Register registers the pivot virtual table.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
sqlite3.CreateModule(db, "pivot", declare, declare)
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
return sqlite3.CreateModule(db, "pivot", declare, declare)
|
||||
}
|
||||
|
||||
type table struct {
|
||||
@@ -65,7 +66,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, util.ErrorString("pivot: column definition query expects 2 result columns")
|
||||
}
|
||||
for stmt.Step() {
|
||||
name := sqlite3.QuoteIdentifier(stmt.ColumnText(1))
|
||||
@@ -83,7 +84,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, util.ErrorString("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)
|
||||
@@ -224,7 +225,7 @@ func (c *cursor) RowID() (int64, error) {
|
||||
return c.rowID, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
|
||||
func (c *cursor) Column(ctx sqlite3.Context, col int) error {
|
||||
count := c.scan.ColumnCount()
|
||||
if col < count {
|
||||
ctx.ResultValue(c.scan.ColumnValue(col))
|
||||
|
||||
@@ -14,14 +14,14 @@ import (
|
||||
|
||||
// https://antonz.org/sqlite-pivot-table/
|
||||
func Example() {
|
||||
sqlite3.AutoExtension(pivot.Register)
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
pivot.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE sales(product TEXT, year INT, income DECIMAL);
|
||||
INSERT INTO sales(product, year, income) VALUES
|
||||
@@ -83,6 +83,11 @@ func Example() {
|
||||
// gamma 80 75 78 80
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(pivot.Register)
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -92,8 +97,6 @@ func TestRegister(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
pivot.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE r AS
|
||||
SELECT 1 id UNION SELECT 2 UNION SELECT 3;
|
||||
@@ -142,6 +145,11 @@ func TestRegister(t *testing.T) {
|
||||
t.Errorf("got %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
err = db.Exec(`ALTER TABLE v_x RENAME TO v_y`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_errors(t *testing.T) {
|
||||
@@ -153,8 +161,6 @@ func TestRegister_errors(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
pivot.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE pivot USING pivot()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
|
||||
78
ext/regexp/regexp.go
Normal file
78
ext/regexp/regexp.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Package regexp provides additional regular expression functions.
|
||||
//
|
||||
// It provides the following Unicode aware functions:
|
||||
// - regexp_like(),
|
||||
// - regexp_substr(),
|
||||
// - regexp_replace(),
|
||||
// - and a REGEXP operator.
|
||||
//
|
||||
// The implementation uses Go [regexp/syntax] for regular expressions.
|
||||
//
|
||||
// https://github.com/nalgeon/sqlean/blob/main/docs/regexp.md
|
||||
package regexp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Register registers Unicode aware functions for a database connection.
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
return errors.Join(
|
||||
db.CreateFunction("regexp", 2, flags, regex),
|
||||
db.CreateFunction("regexp_like", 2, flags, regexLike),
|
||||
db.CreateFunction("regexp_substr", 2, flags, regexSubstr),
|
||||
db.CreateFunction("regexp_replace", 3, flags, regexReplace))
|
||||
}
|
||||
|
||||
func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
|
||||
re, ok := ctx.GetAuxData(i).(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
re = r
|
||||
ctx.SetAuxData(0, r)
|
||||
}
|
||||
return re, nil
|
||||
}
|
||||
|
||||
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 0, arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultBool(re.Match(arg[1].RawText()))
|
||||
}
|
||||
}
|
||||
|
||||
func regexLike(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultBool(re.Match(arg[0].RawText()))
|
||||
}
|
||||
}
|
||||
|
||||
func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultRawText(re.Find(arg[0].RawText()))
|
||||
}
|
||||
}
|
||||
|
||||
func regexReplace(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultRawText(re.ReplaceAll(arg[0].RawText(), arg[2].RawText()))
|
||||
}
|
||||
}
|
||||
71
ext/regexp/regexp_test.go
Normal file
71
ext/regexp/regexp_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package regexp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
tests := []struct {
|
||||
test string
|
||||
want string
|
||||
}{
|
||||
{`'Hello' REGEXP 'elo'`, "0"},
|
||||
{`'Hello' REGEXP 'ell'`, "1"},
|
||||
{`'Hello' REGEXP 'el.'`, "1"},
|
||||
{`regexp_like('Hello', 'elo')`, "0"},
|
||||
{`regexp_like('Hello', 'ell')`, "1"},
|
||||
{`regexp_like('Hello', 'el.')`, "1"},
|
||||
{`regexp_substr('Hello', 'el.')`, "ell"},
|
||||
{`regexp_replace('Hello', 'llo', 'll')`, "Hell"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
var got string
|
||||
err := db.QueryRow(`SELECT ` + tt.test).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q, want %q", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
tests := []string{
|
||||
`'' REGEXP ?`,
|
||||
`regexp_like('', ?)`,
|
||||
`regexp_substr('', ?)`,
|
||||
`regexp_replace('', ?, '')`,
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
err := db.QueryRow(`SELECT `+tt, `\`).Scan(nil)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,17 @@ package statement
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Register registers the statement virtual table.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
sqlite3.CreateModule(db, "statement", declare, declare)
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
return sqlite3.CreateModule(db, "statement", declare, declare)
|
||||
}
|
||||
|
||||
type table struct {
|
||||
@@ -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, util.ErrorString("statement: wrong number of arguments")
|
||||
}
|
||||
|
||||
sql := "SELECT * FROM\n" + arg[0]
|
||||
@@ -123,12 +123,11 @@ func (t *table) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *table) Open() (sqlite3.VTabCursor, error) {
|
||||
func (t *table) Open() (_ sqlite3.VTabCursor, err error) {
|
||||
stmt := t.stmt
|
||||
if !t.inuse {
|
||||
t.inuse = true
|
||||
} else {
|
||||
var err error
|
||||
stmt, _, err = t.stmt.Conn().Prepare(t.sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -202,7 +201,7 @@ func (c *cursor) RowID() (int64, error) {
|
||||
return c.rowID, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
|
||||
func (c *cursor) Column(ctx sqlite3.Context, col int) error {
|
||||
switch outputs := c.stmt.ColumnCount(); {
|
||||
case col < outputs:
|
||||
ctx.ResultValue(c.stmt.ColumnValue(col))
|
||||
|
||||
@@ -12,14 +12,14 @@ import (
|
||||
)
|
||||
|
||||
func Example() {
|
||||
sqlite3.AutoExtension(statement.Register)
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
statement.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE split_date USING statement((
|
||||
SELECT
|
||||
@@ -48,6 +48,11 @@ func Example() {
|
||||
// Twosday was 2022-2-22
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(statement.Register)
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -57,8 +62,6 @@ func TestRegister(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
statement.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE arguments USING statement((SELECT ? AS a, ? AS b, ? AS c))
|
||||
`)
|
||||
@@ -107,8 +110,6 @@ func TestRegister_errors(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
statement.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/stats"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
@@ -18,8 +17,6 @@ func TestRegister_boolean(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stats.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE data (x)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -32,7 +32,7 @@ func (q *percentile) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
q.nums = append(q.nums, a.Float())
|
||||
}
|
||||
if q.kind != median {
|
||||
q.arg1 = arg[1].Blob(q.arg1[:0])
|
||||
q.arg1 = append(q.arg1[:0], arg[1].RawText()...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (q *percentile) Value(ctx sqlite3.Context) {
|
||||
ctx.ResultJSON(floats)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("percentile: %w", err))
|
||||
ctx.ResultError(fmt.Errorf("percentile: %w", err)) // notest
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func getPercentile(nums []float64, pos float64, disc bool) (float64, error) {
|
||||
}
|
||||
|
||||
m1 := slices.Min(nums[int(i)+1:])
|
||||
return math.FMA(f, m1, -math.FMA(f, m0, -m0)), nil
|
||||
return math.FMA(f, m1, math.FMA(-f, m0, m0)), nil
|
||||
}
|
||||
|
||||
func getPercentiles(nums []float64, pos []float64, disc bool) error {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/stats"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
@@ -19,8 +18,6 @@ func TestRegister_percentile(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stats.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE data (x)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -44,33 +44,38 @@
|
||||
// [ANSI SQL Aggregate Functions]: https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
|
||||
package stats
|
||||
|
||||
import "github.com/ncruces/go-sqlite3"
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Register registers statistics functions.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
db.CreateWindowFunction("var_pop", 1, flags, newVariance(var_pop))
|
||||
db.CreateWindowFunction("var_samp", 1, flags, newVariance(var_samp))
|
||||
db.CreateWindowFunction("stddev_pop", 1, flags, newVariance(stddev_pop))
|
||||
db.CreateWindowFunction("stddev_samp", 1, flags, newVariance(stddev_samp))
|
||||
db.CreateWindowFunction("covar_pop", 2, flags, newCovariance(var_pop))
|
||||
db.CreateWindowFunction("covar_samp", 2, flags, newCovariance(var_samp))
|
||||
db.CreateWindowFunction("corr", 2, flags, newCovariance(corr))
|
||||
db.CreateWindowFunction("regr_r2", 2, flags, newCovariance(regr_r2))
|
||||
db.CreateWindowFunction("regr_sxx", 2, flags, newCovariance(regr_sxx))
|
||||
db.CreateWindowFunction("regr_syy", 2, flags, newCovariance(regr_syy))
|
||||
db.CreateWindowFunction("regr_sxy", 2, flags, newCovariance(regr_sxy))
|
||||
db.CreateWindowFunction("regr_avgx", 2, flags, newCovariance(regr_avgx))
|
||||
db.CreateWindowFunction("regr_avgy", 2, flags, newCovariance(regr_avgy))
|
||||
db.CreateWindowFunction("regr_slope", 2, flags, newCovariance(regr_slope))
|
||||
db.CreateWindowFunction("regr_intercept", 2, flags, newCovariance(regr_intercept))
|
||||
db.CreateWindowFunction("regr_count", 2, flags, newCovariance(regr_count))
|
||||
db.CreateWindowFunction("regr_json", 2, flags, newCovariance(regr_json))
|
||||
db.CreateWindowFunction("median", 1, flags, newPercentile(median))
|
||||
db.CreateWindowFunction("percentile_cont", 2, flags, newPercentile(percentile_cont))
|
||||
db.CreateWindowFunction("percentile_disc", 2, flags, newPercentile(percentile_disc))
|
||||
db.CreateWindowFunction("every", 1, flags, newBoolean(every))
|
||||
db.CreateWindowFunction("some", 1, flags, newBoolean(some))
|
||||
return errors.Join(
|
||||
db.CreateWindowFunction("var_pop", 1, flags, newVariance(var_pop)),
|
||||
db.CreateWindowFunction("var_samp", 1, flags, newVariance(var_samp)),
|
||||
db.CreateWindowFunction("stddev_pop", 1, flags, newVariance(stddev_pop)),
|
||||
db.CreateWindowFunction("stddev_samp", 1, flags, newVariance(stddev_samp)),
|
||||
db.CreateWindowFunction("covar_pop", 2, flags, newCovariance(var_pop)),
|
||||
db.CreateWindowFunction("covar_samp", 2, flags, newCovariance(var_samp)),
|
||||
db.CreateWindowFunction("corr", 2, flags, newCovariance(corr)),
|
||||
db.CreateWindowFunction("regr_r2", 2, flags, newCovariance(regr_r2)),
|
||||
db.CreateWindowFunction("regr_sxx", 2, flags, newCovariance(regr_sxx)),
|
||||
db.CreateWindowFunction("regr_syy", 2, flags, newCovariance(regr_syy)),
|
||||
db.CreateWindowFunction("regr_sxy", 2, flags, newCovariance(regr_sxy)),
|
||||
db.CreateWindowFunction("regr_avgx", 2, flags, newCovariance(regr_avgx)),
|
||||
db.CreateWindowFunction("regr_avgy", 2, flags, newCovariance(regr_avgy)),
|
||||
db.CreateWindowFunction("regr_slope", 2, flags, newCovariance(regr_slope)),
|
||||
db.CreateWindowFunction("regr_intercept", 2, flags, newCovariance(regr_intercept)),
|
||||
db.CreateWindowFunction("regr_count", 2, flags, newCovariance(regr_count)),
|
||||
db.CreateWindowFunction("regr_json", 2, flags, newCovariance(regr_json)),
|
||||
db.CreateWindowFunction("median", 1, flags, newPercentile(median)),
|
||||
db.CreateWindowFunction("percentile_cont", 2, flags, newPercentile(percentile_cont)),
|
||||
db.CreateWindowFunction("percentile_disc", 2, flags, newPercentile(percentile_disc)),
|
||||
db.CreateWindowFunction("every", 1, flags, newBoolean(every)),
|
||||
db.CreateWindowFunction("some", 1, flags, newBoolean(some)))
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -10,6 +10,11 @@ import (
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(stats.Register)
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestRegister_variance(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -19,8 +24,6 @@ func TestRegister_variance(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stats.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE data (x)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -88,8 +91,6 @@ func TestRegister_covariance(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stats.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE data (y, x)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -217,8 +218,6 @@ func Benchmark_variance(b *testing.B) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stats.Register(db)
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT var_pop(value) FROM generate_series(0, ?)`)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
|
||||
@@ -18,6 +18,7 @@ package unicode
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -30,29 +31,29 @@ import (
|
||||
)
|
||||
|
||||
// Register registers Unicode aware functions for a database connection.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
return errors.Join(
|
||||
db.CreateFunction("like", 2, flags, like),
|
||||
db.CreateFunction("like", 3, flags, like),
|
||||
db.CreateFunction("upper", 1, flags, upper),
|
||||
db.CreateFunction("upper", 2, flags, upper),
|
||||
db.CreateFunction("lower", 1, flags, lower),
|
||||
db.CreateFunction("lower", 2, flags, lower),
|
||||
db.CreateFunction("regexp", 2, flags, regex),
|
||||
db.CreateFunction("icu_load_collation", 2, sqlite3.DIRECTONLY,
|
||||
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
name := arg[1].Text()
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
|
||||
db.CreateFunction("like", 2, flags, like)
|
||||
db.CreateFunction("like", 3, flags, like)
|
||||
db.CreateFunction("upper", 1, flags, upper)
|
||||
db.CreateFunction("upper", 2, flags, upper)
|
||||
db.CreateFunction("lower", 1, flags, lower)
|
||||
db.CreateFunction("lower", 2, flags, lower)
|
||||
db.CreateFunction("regexp", 2, flags, regex)
|
||||
db.CreateFunction("icu_load_collation", 2, sqlite3.DIRECTONLY,
|
||||
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
name := arg[1].Text()
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err := RegisterCollation(db, arg[0].Text(), name)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
err := RegisterCollation(db, arg[0].Text(), name)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterCollation registers a Unicode collation sequence for a database connection.
|
||||
@@ -74,7 +75,7 @@ func upper(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
t, err := language.Parse(arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
c := cases.Upper(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
@@ -93,7 +94,7 @@ func lower(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
t, err := language.Parse(arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
c := cases.Lower(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
@@ -108,10 +109,10 @@ func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
r, err := regexp.Compile(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
return // notest
|
||||
}
|
||||
re = r
|
||||
ctx.SetAuxData(0, re)
|
||||
ctx.SetAuxData(0, r)
|
||||
}
|
||||
ctx.ResultBool(re.Match(arg[1].RawText()))
|
||||
}
|
||||
|
||||
168
ext/uuid/uuid.go
Normal file
168
ext/uuid/uuid.go
Normal file
@@ -0,0 +1,168 @@
|
||||
// Package uuid provides functions to generate RFC 4122 UUIDs.
|
||||
//
|
||||
// https://sqlite.org/src/file/ext/misc/uuid.c
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Register registers the SQL functions:
|
||||
//
|
||||
// uuid([version], [domain/namespace], [id/data])
|
||||
//
|
||||
// Generates a UUID as a string.
|
||||
//
|
||||
// uuid_str(u)
|
||||
//
|
||||
// Converts a UUID into a well-formed UUID string.
|
||||
//
|
||||
// uuid_blob(u)
|
||||
//
|
||||
// Converts a UUID into a 16-byte blob.
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
return errors.Join(
|
||||
db.CreateFunction("uuid", 0, sqlite3.INNOCUOUS, generate),
|
||||
db.CreateFunction("uuid", 1, sqlite3.INNOCUOUS, generate),
|
||||
db.CreateFunction("uuid", 2, sqlite3.INNOCUOUS, generate),
|
||||
db.CreateFunction("uuid", 3, sqlite3.INNOCUOUS, generate),
|
||||
db.CreateFunction("uuid_str", 1, flags, toString),
|
||||
db.CreateFunction("uuid_blob", 1, flags, toBlob))
|
||||
}
|
||||
|
||||
func generate(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
var (
|
||||
ver int
|
||||
err error
|
||||
u uuid.UUID
|
||||
)
|
||||
|
||||
if len(arg) > 0 {
|
||||
ver = arg[0].Int()
|
||||
} else {
|
||||
ver = 4
|
||||
}
|
||||
|
||||
switch ver {
|
||||
case 1:
|
||||
u, err = uuid.NewUUID()
|
||||
case 4:
|
||||
u, err = uuid.NewRandom()
|
||||
case 6:
|
||||
u, err = uuid.NewV6()
|
||||
case 7:
|
||||
u, err = uuid.NewV7()
|
||||
|
||||
case 2:
|
||||
var domain uuid.Domain
|
||||
if len(arg) > 1 {
|
||||
domain = uuid.Domain(arg[1].Int64())
|
||||
if domain == 0 {
|
||||
if txt := arg[1].RawText(); len(txt) > 0 {
|
||||
switch txt[0] | 0x20 { // to lower
|
||||
case 'g': // group
|
||||
domain = uuid.Group
|
||||
case 'o': // org
|
||||
domain = uuid.Org
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case len(arg) > 2:
|
||||
u, err = uuid.NewDCESecurity(domain, uint32(arg[2].Int64()))
|
||||
case domain == uuid.Person:
|
||||
u, err = uuid.NewDCEPerson()
|
||||
case domain == uuid.Group:
|
||||
u, err = uuid.NewDCEGroup()
|
||||
default:
|
||||
err = util.ErrorString("missing id")
|
||||
}
|
||||
|
||||
case 3, 5:
|
||||
if len(arg) < 2 {
|
||||
err = util.ErrorString("missing data")
|
||||
break
|
||||
}
|
||||
ns, err := fromValue(arg[1])
|
||||
if err != nil {
|
||||
space := arg[1].RawText()
|
||||
switch {
|
||||
case bytes.EqualFold(space, []byte("url")):
|
||||
ns = uuid.NameSpaceURL
|
||||
case bytes.EqualFold(space, []byte("oid")):
|
||||
ns = uuid.NameSpaceOID
|
||||
case bytes.EqualFold(space, []byte("dns")):
|
||||
ns = uuid.NameSpaceDNS
|
||||
case bytes.EqualFold(space, []byte("fqdn")):
|
||||
ns = uuid.NameSpaceDNS
|
||||
case bytes.EqualFold(space, []byte("x500")):
|
||||
ns = uuid.NameSpaceX500
|
||||
default:
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
}
|
||||
if ver == 3 {
|
||||
u = uuid.NewMD5(ns, arg[2].RawBlob())
|
||||
} else {
|
||||
u = uuid.NewSHA1(ns, arg[2].RawBlob())
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("invalid version: %d", ver)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("uuid: %w", err)) // notest
|
||||
} else {
|
||||
ctx.ResultText(u.String())
|
||||
}
|
||||
}
|
||||
|
||||
func fromValue(arg sqlite3.Value) (u uuid.UUID, err error) {
|
||||
switch t := arg.Type(); t {
|
||||
case sqlite3.TEXT:
|
||||
u, err = uuid.ParseBytes(arg.RawText())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("uuid: %w", err)
|
||||
}
|
||||
|
||||
case sqlite3.BLOB:
|
||||
blob := arg.RawBlob()
|
||||
if len := len(blob); len != 16 {
|
||||
err = fmt.Errorf("uuid: invalid BLOB length: %d", len)
|
||||
} else {
|
||||
copy(u[:], blob)
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("uuid: invalid type: %v", t)
|
||||
}
|
||||
return u, err
|
||||
}
|
||||
|
||||
func toBlob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
u, err := fromValue(arg[0])
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultBlob(u[:])
|
||||
}
|
||||
}
|
||||
|
||||
func toString(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
u, err := fromValue(arg[0])
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultText(u.String())
|
||||
}
|
||||
}
|
||||
180
ext/uuid/uuid_test.go
Normal file
180
ext/uuid/uuid_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func Test_generate(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var u uuid.UUID
|
||||
|
||||
// Version 4, SQLite compatible
|
||||
err = db.QueryRow(`SELECT uuid()`).Scan(&u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := u.Version(); got != 4 {
|
||||
t.Errorf("got %d, want 4", got)
|
||||
}
|
||||
|
||||
// Invalid version
|
||||
err = db.QueryRow(`SELECT uuid(8)`).Scan(&u)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
// Custom version, no arguments
|
||||
for _, want := range []uuid.Version{1, 2, 4, 6, 7} {
|
||||
err = db.QueryRow(`SELECT uuid(?)`, want).Scan(&u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := u.Version(); got != want {
|
||||
t.Errorf("got %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Version 2, custom arguments
|
||||
err = db.QueryRow(`SELECT uuid(2, 4)`).Scan(&u)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT uuid(2, 'group')`).Scan(&u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := u.Version(); got != 2 {
|
||||
t.Errorf("got %d, want 2", got)
|
||||
}
|
||||
if got := u.Domain(); got != uuid.Group {
|
||||
t.Errorf("got %d, want 1", got)
|
||||
}
|
||||
|
||||
dce := []struct {
|
||||
out uuid.Domain
|
||||
in any
|
||||
id uint32
|
||||
}{
|
||||
{uuid.Person, "user", 42},
|
||||
{uuid.Group, "group", 42},
|
||||
{uuid.Org, "org", 42},
|
||||
{uuid.Person, 0, 42},
|
||||
{uuid.Group, 1, 42},
|
||||
{uuid.Org, 2, 42},
|
||||
{3, 3, 42},
|
||||
}
|
||||
for _, tt := range dce {
|
||||
err = db.QueryRow(`SELECT uuid(2, ?, ?)`, tt.in, tt.id).Scan(&u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := u.Version(); got != 2 {
|
||||
t.Errorf("got %d, want 2", got)
|
||||
}
|
||||
if got := u.Domain(); got != tt.out {
|
||||
t.Errorf("got %d, want %d", got, tt.out)
|
||||
}
|
||||
if got := u.ID(); got != tt.id {
|
||||
t.Errorf("got %d, want %d", got, tt.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Versions 3 and 5
|
||||
err = db.QueryRow(`SELECT uuid(3)`).Scan(&u)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT uuid(3, 0, '')`).Scan(&u)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
hash := []struct {
|
||||
ver uuid.Version
|
||||
ns any
|
||||
data string
|
||||
u uuid.UUID
|
||||
}{
|
||||
{3, "oid", "2.999", uuid.MustParse("31cb1efa-18c4-3d19-89ba-df6a74ddbd1d")},
|
||||
{3, "dns", "www.example.com", uuid.MustParse("5df41881-3aed-3515-88a7-2f4a814cf09e")},
|
||||
{3, "fqdn", "www.example.com", uuid.MustParse("5df41881-3aed-3515-88a7-2f4a814cf09e")},
|
||||
{3, "url", "https://www.example.com/", uuid.MustParse("7fed185f-0864-319f-875b-a3d5458e30ac")},
|
||||
{3, "x500", "CN=Test User 1, O=Example Organization, ST=California, C=US", uuid.MustParse("addf5e97-9287-3834-abfd-7edcbe7db56f")},
|
||||
{3, "url", "https://www.php.net", uuid.MustParse("3f703955-aaba-3e70-a3cb-baff6aa3b28f")},
|
||||
{5, "url", "https://www.php.net", uuid.MustParse("a8f6ae40-d8a7-58f0-be05-a22f94eca9ec")},
|
||||
}
|
||||
for _, tt := range hash {
|
||||
err = db.QueryRow(`SELECT uuid(?, ?, ?)`, tt.ver, tt.ns, tt.data).Scan(&u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u != tt.u {
|
||||
t.Errorf("got %v, want %v", u, tt.u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_convert(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var u uuid.UUID
|
||||
lits := []string{
|
||||
"'6ba7b8119dad11d180b400c04fd430c8'",
|
||||
"'6ba7b811-9dad-11d1-80b4-00c04fd430c8'",
|
||||
"'{6ba7b811-9dad-11d1-80b4-00c04fd430c8}'",
|
||||
"X'6ba7b8119dad11d180b400c04fd430c8'",
|
||||
}
|
||||
|
||||
for _, tt := range lits {
|
||||
err = db.QueryRow(`SELECT uuid_str(` + tt + `)`).Scan(&u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u != uuid.NameSpaceURL {
|
||||
t.Errorf("got %v, want %v", u, uuid.NameSpaceURL)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tt := range lits {
|
||||
err = db.QueryRow(`SELECT uuid_blob(` + tt + `)`).Scan(&u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u != uuid.NameSpaceURL {
|
||||
t.Errorf("got %v, want %v", u, uuid.NameSpaceURL)
|
||||
}
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT uuid_str(X'cafe')`).Scan(&u)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT uuid_blob(X'cafe')`).Scan(&u)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
}
|
||||
}
|
||||
@@ -4,30 +4,33 @@
|
||||
package zorder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Register registers the zorder and unzorder SQL functions.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
db.CreateFunction("zorder", -1, flags, zorder)
|
||||
db.CreateFunction("unzorder", 3, flags, unzorder)
|
||||
return errors.Join(
|
||||
db.CreateFunction("zorder", -1, flags, zorder),
|
||||
db.CreateFunction("unzorder", 3, flags, unzorder))
|
||||
}
|
||||
|
||||
func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
var x [63]int64
|
||||
for i := range arg {
|
||||
x[i] = arg[i].Int64()
|
||||
}
|
||||
if len(arg) > len(x) {
|
||||
ctx.ResultError(util.ErrorString("zorder: too many parameters"))
|
||||
return
|
||||
}
|
||||
for i := range arg {
|
||||
x[i] = arg[i].Int64()
|
||||
}
|
||||
|
||||
var z int64
|
||||
if len(arg) > 0 {
|
||||
for i := 0; i < 63; i++ {
|
||||
for i := range x {
|
||||
j := i % len(arg)
|
||||
z |= (x[j] & 1) << i
|
||||
x[j] >>= 1
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
package zorder_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/zorder"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func TestRegister_zorder(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
zorder.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, zorder.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -59,11 +59,9 @@ func TestRegister_zorder(t *testing.T) {
|
||||
|
||||
func TestRegister_unzorder(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
zorder.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, zorder.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -89,11 +87,9 @@ func TestRegister_unzorder(t *testing.T) {
|
||||
|
||||
func TestRegister_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
zorder.Register(c)
|
||||
return nil
|
||||
})
|
||||
db, err := driver.Open(tmp, zorder.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -104,4 +100,16 @@ func TestRegister_error(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
buf.WriteString("SELECT zorder(0")
|
||||
for i := 1; i < 80; i++ {
|
||||
buf.WriteByte(',')
|
||||
buf.WriteString(strconv.Itoa(0))
|
||||
}
|
||||
buf.WriteByte(')')
|
||||
err = db.QueryRow(buf.String()).Scan(&got)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
}
|
||||
|
||||
5
func.go
5
func.go
@@ -31,8 +31,9 @@ func (c *Conn) CollationNeeded(cb func(db *Conn, name string)) error {
|
||||
//
|
||||
// This can be used to load schemas that contain
|
||||
// one or more unknown collating sequences.
|
||||
func (c *Conn) AnyCollationNeeded() {
|
||||
c.call("sqlite3_anycollseq_init", uint64(c.handle), 0, 0)
|
||||
func (c Conn) AnyCollationNeeded() error {
|
||||
r := c.call("sqlite3_anycollseq_init", uint64(c.handle), 0, 0)
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
// CreateCollation defines a new collating sequence.
|
||||
|
||||
@@ -130,8 +130,8 @@ func ExampleContext_SetAuxData() {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
ctx.SetAuxData(0, r)
|
||||
re = r
|
||||
ctx.SetAuxData(0, r)
|
||||
}
|
||||
ctx.ResultBool(re.Match(arg[1].RawText()))
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ func (f *countASCII) isASCII(arg sqlite3.Value) bool {
|
||||
if arg.Type() != sqlite3.TEXT {
|
||||
return false
|
||||
}
|
||||
for _, c := range arg.RawBlob() {
|
||||
for _, c := range arg.RawText() {
|
||||
if c > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
|
||||
15
go.mod
15
go.mod
@@ -2,16 +2,21 @@ module github.com/ncruces/go-sqlite3
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
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.3
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/text v0.16.0
|
||||
github.com/tetratelabs/wazero v1.8.0
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/sys v0.25.0
|
||||
golang.org/x/text v0.18.0
|
||||
lukechampine.com/adiantum v1.1.1
|
||||
)
|
||||
|
||||
require github.com/google/uuid v1.6.0
|
||||
|
||||
retract v0.4.0 // tagged from the wrong branch
|
||||
|
||||
24
go.sum
24
go.sum
@@ -1,18 +1,22 @@
|
||||
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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.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=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
|
||||
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
|
||||
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=
|
||||
|
||||
@@ -3,5 +3,8 @@ golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
|
||||
@@ -3,9 +3,11 @@ set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/ddlmod.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/ddlmod_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/error_translator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/migrator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/sqlite.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/sqlite_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/ddlmod.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/ddlmod_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/error_translator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/migrator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/sqlite.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/sqlite_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/sqlite_test.go"
|
||||
curl -#L "https://github.com/glebarez/sqlite/raw/v1.11.0/sqlite_error_translator_test.go" > error_translator_test.go
|
||||
48
gormlite/error_translator_test.go
Normal file
48
gormlite/error_translator_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func TestErrorTranslator(t *testing.T) {
|
||||
// This is the DSN of the in-memory SQLite database for these tests.
|
||||
const InMemoryDSN = "file:testdatabase?mode=memory&cache=shared"
|
||||
|
||||
// This is the example object for testing the unique constraint error
|
||||
type Article struct {
|
||||
ArticleNumber string `gorm:"unique"`
|
||||
}
|
||||
|
||||
db, err := gorm.Open(Open(InMemoryDSN), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
TranslateError: true})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected Open to succeed; got error: %v", err)
|
||||
}
|
||||
if db == nil {
|
||||
t.Errorf("Expected db to be non-nil.")
|
||||
}
|
||||
|
||||
err = db.AutoMigrate(&Article{})
|
||||
if err != nil {
|
||||
t.Errorf("Expected to migrate database models to succeed: %v", err)
|
||||
}
|
||||
|
||||
err = db.Create(&Article{ArticleNumber: "A00000XX"}).Error
|
||||
if err != nil {
|
||||
t.Errorf("Expected first create to succeed: %v", err)
|
||||
}
|
||||
|
||||
err = db.Create(&Article{ArticleNumber: "A00000XX"}).Error
|
||||
if err == nil {
|
||||
t.Errorf("Expected second create to fail.")
|
||||
}
|
||||
|
||||
if err != gorm.ErrDuplicatedKey {
|
||||
t.Errorf("Expected error from second create to be gorm.ErrDuplicatedKey: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,18 @@ module github.com/ncruces/go-sqlite3/gormlite
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/ncruces/go-sqlite3 v0.16.1
|
||||
gorm.io/gorm v1.25.10
|
||||
github.com/ncruces/go-sqlite3 v0.18.1
|
||||
gorm.io/gorm v1.25.11
|
||||
)
|
||||
|
||||
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.3 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,15 +2,15 @@ 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.18.1 h1:iN8IMZV5EMxpH88NUac9vId23eTKNFUhP7jgY0EBbNc=
|
||||
github.com/ncruces/go-sqlite3 v0.18.1/go.mod h1:eEOyZnW1dGTJ+zDpMuzfYamEUBtdFz5zeYhqLBtHxvM=
|
||||
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.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=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
|
||||
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -21,7 +20,7 @@ func Open(dsn string) gorm.Dialector {
|
||||
}
|
||||
|
||||
// Open opens a GORM dialector from a database handle.
|
||||
func OpenDB(db *sql.DB) gorm.Dialector {
|
||||
func OpenDB(db gorm.ConnPool) gorm.Dialector {
|
||||
return &_Dialector{Conn: db}
|
||||
}
|
||||
|
||||
@@ -38,7 +37,7 @@ func (dialector _Dialector) Initialize(db *gorm.DB) (err error) {
|
||||
if dialector.Conn != nil {
|
||||
db.ConnPool = dialector.Conn
|
||||
} else {
|
||||
conn, err := driver.Open(dialector.DSN, nil)
|
||||
conn, err := driver.Open(dialector.DSN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func TestDialector(t *testing.T) {
|
||||
// This is the DSN of the in-memory SQLite database for these tests.
|
||||
const InMemoryDSN = "file:testdatabase?mode=memory&cache=shared"
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
// Custom connection with a custom function called "my_custom_function".
|
||||
db, err := driver.Open(InMemoryDSN, func(conn *sqlite3.Conn) error {
|
||||
db, err := driver.Open(tmp, func(conn *sqlite3.Conn) error {
|
||||
return conn.CreateFunction("my_custom_function", 0, sqlite3.DETERMINISTIC,
|
||||
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultText("my-result")
|
||||
@@ -36,14 +36,14 @@ func TestDialector(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
description: "Default driver",
|
||||
dialector: Open(InMemoryDSN),
|
||||
dialector: Open(tmp),
|
||||
openSuccess: true,
|
||||
query: "SELECT 1",
|
||||
querySuccess: true,
|
||||
},
|
||||
{
|
||||
description: "Custom function",
|
||||
dialector: Open(InMemoryDSN),
|
||||
dialector: Open(tmp),
|
||||
openSuccess: true,
|
||||
query: "SELECT my_custom_function()",
|
||||
querySuccess: false,
|
||||
|
||||
@@ -7,7 +7,7 @@ rm -rf gorm/ tests/
|
||||
go work use -r .
|
||||
go test
|
||||
|
||||
git clone --branch v1.25.10 --filter=blob:none https://github.com/go-gorm/gorm.git
|
||||
git clone --branch v1.25.11 --filter=blob:none https://github.com/go-gorm/gorm.git
|
||||
mv gorm/tests tests
|
||||
rm -rf gorm/
|
||||
|
||||
|
||||
14
internal/alloc/alloc_test.go
Normal file
14
internal/alloc/alloc_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package alloc_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/alloc"
|
||||
)
|
||||
|
||||
func TestVirtual(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
alloc.Virtual(math.MaxInt+2, math.MaxInt+2)
|
||||
t.Error("want panic")
|
||||
}
|
||||
@@ -39,7 +39,7 @@ type mmappedMemory struct {
|
||||
func (m *mmappedMemory) Reallocate(size uint64) []byte {
|
||||
com := uint64(len(m.buf))
|
||||
res := uint64(cap(m.buf))
|
||||
if com < size && size < res {
|
||||
if com < size && size <= res {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(unix.Getpagesize() - 1)
|
||||
new := (size + rnd) &^ rnd
|
||||
|
||||
@@ -48,7 +48,7 @@ type virtualMemory struct {
|
||||
func (m *virtualMemory) Reallocate(size uint64) []byte {
|
||||
com := uint64(len(m.buf))
|
||||
res := uint64(cap(m.buf))
|
||||
if com < size && size < res {
|
||||
if com < size && size <= res {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(windows.Getpagesize() - 1)
|
||||
new := (size + rnd) &^ rnd
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/tetratelabs/wazero"
|
||||
)
|
||||
|
||||
// notest
|
||||
|
||||
func init() {
|
||||
if bits.UintSize < 64 {
|
||||
return
|
||||
|
||||
@@ -104,3 +104,13 @@ func ErrorCodeString(rc uint32) string {
|
||||
}
|
||||
return "sqlite3: unknown error"
|
||||
}
|
||||
|
||||
type ErrorJoiner []error
|
||||
|
||||
func (j *ErrorJoiner) Join(errs ...error) {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
*j = append(*j, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
internal/util/error_test.go
Normal file
13
internal/util/error_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestErrorJoiner(t *testing.T) {
|
||||
var errs ErrorJoiner
|
||||
errs.Join(NilErr, OOMErr)
|
||||
for i, e := range []error{NilErr, OOMErr} {
|
||||
if e != errs[i] {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func (s *mmapState) new(ctx context.Context, mod api.Module, size int32) *Mapped
|
||||
|
||||
// Allocate page aligned memmory.
|
||||
alloc := mod.ExportedFunction("aligned_alloc")
|
||||
stack := [2]uint64{
|
||||
stack := [...]uint64{
|
||||
uint64(unix.Getpagesize()),
|
||||
uint64(size),
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (s *mmapState) new(ctx context.Context, mod api.Module, size int32) *Mapped
|
||||
// Save the newly allocated region.
|
||||
ptr := uint32(stack[0])
|
||||
buf := View(mod, ptr, uint64(size))
|
||||
addr := uintptr(unsafe.Pointer(&buf[0]))
|
||||
addr := unsafe.Pointer(&buf[0])
|
||||
s.regions = append(s.regions, &MappedRegion{
|
||||
Ptr: ptr,
|
||||
addr: addr,
|
||||
@@ -56,7 +56,7 @@ func (s *mmapState) new(ctx context.Context, mod api.Module, size int32) *Mapped
|
||||
}
|
||||
|
||||
type MappedRegion struct {
|
||||
addr uintptr
|
||||
addr unsafe.Pointer
|
||||
Ptr uint32
|
||||
size int32
|
||||
used bool
|
||||
@@ -76,23 +76,15 @@ func (r *MappedRegion) Unmap() error {
|
||||
// We can't munmap the region, otherwise it could be remaped.
|
||||
// Instead, convert it to a protected, private, anonymous mapping.
|
||||
// If successful, it can be reused for a subsequent mmap.
|
||||
_, err := mmap(r.addr, uintptr(r.size),
|
||||
unix.PROT_NONE, unix.MAP_PRIVATE|unix.MAP_ANON|unix.MAP_FIXED,
|
||||
-1, 0)
|
||||
_, err := unix.MmapPtr(-1, 0, r.addr, uintptr(r.size),
|
||||
unix.PROT_NONE, unix.MAP_PRIVATE|unix.MAP_FIXED|unix.MAP_ANON)
|
||||
r.used = err != nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *MappedRegion) mmap(f *os.File, offset int64, prot int) error {
|
||||
_, err := mmap(r.addr, uintptr(r.size),
|
||||
prot, unix.MAP_SHARED|unix.MAP_FIXED,
|
||||
int(f.Fd()), offset)
|
||||
_, err := unix.MmapPtr(int(f.Fd()), offset, r.addr, uintptr(r.size),
|
||||
prot, unix.MAP_SHARED|unix.MAP_FIXED)
|
||||
r.used = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// We need the low level mmap for MAP_FIXED to work.
|
||||
// Bind the syscall version hoping that it is more stable.
|
||||
|
||||
//go:linkname mmap syscall.mmap
|
||||
func mmap(addr, length uintptr, prot, flag, fd int, pos int64) (*byte, error)
|
||||
|
||||
111
json_test.go
Normal file
111
json_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package sqlite3_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
func Example_json() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE orders (
|
||||
cart_id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
cart BLOB -- stored as JSONB
|
||||
) STRICT;
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
type CartItem struct {
|
||||
ItemID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Quantity int `json:"quantity,omitempty"`
|
||||
Price int `json:"price,omitempty"`
|
||||
}
|
||||
|
||||
type Cart struct {
|
||||
Items []CartItem `json:"items"`
|
||||
}
|
||||
|
||||
// convert to JSONB on insertion
|
||||
stmt, _, err := db.Prepare(`INSERT INTO orders (user_id, cart) VALUES (?, jsonb(?))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if err := stmt.BindInt(1, 123); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stmt.BindJSON(2, Cart{
|
||||
[]CartItem{
|
||||
{ItemID: "111", Name: "T-shirt", Quantity: 1, Price: 250},
|
||||
{ItemID: "222", Name: "Trousers", Quantity: 1, Price: 600},
|
||||
},
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stmt.Exec(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
sl1, _, err := db.Prepare(`
|
||||
SELECT total(json_each.value -> 'price')
|
||||
FROM orders, json_each(cart -> 'items')
|
||||
WHERE cart_id = last_insert_rowid()
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer sl1.Close()
|
||||
|
||||
for sl1.Step() {
|
||||
fmt.Println("total:", sl1.ColumnInt(0))
|
||||
}
|
||||
|
||||
if err := sl1.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
sl2, _, err := db.Prepare(`
|
||||
SELECT json(cart) -- convert to JSON on retrieval
|
||||
FROM orders
|
||||
WHERE cart_id = last_insert_rowid()
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer sl2.Close()
|
||||
|
||||
for sl2.Step() {
|
||||
var cart Cart
|
||||
if err := sl2.ColumnJSON(0, &cart); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, item := range cart.Items {
|
||||
fmt.Printf("id: %s, name: %s, quantity: %d, price: %d\n",
|
||||
item.ItemID, item.Name, item.Quantity, item.Price)
|
||||
}
|
||||
}
|
||||
|
||||
if err := sl2.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// total: 850
|
||||
// id: 111, name: T-shirt, quantity: 1, price: 250
|
||||
// id: 222, name: Trousers, quantity: 1, price: 600
|
||||
}
|
||||
30
registry.go
Normal file
30
registry.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package sqlite3
|
||||
|
||||
import "sync"
|
||||
|
||||
var (
|
||||
// +checklocks:extRegistryMtx
|
||||
extRegistry []func(*Conn) error
|
||||
extRegistryMtx sync.RWMutex
|
||||
)
|
||||
|
||||
// AutoExtension causes the entryPoint function to be invoked
|
||||
// for each new database connection that is created.
|
||||
//
|
||||
// https://sqlite.org/c3ref/auto_extension.html
|
||||
func AutoExtension(entryPoint func(*Conn) error) {
|
||||
extRegistryMtx.Lock()
|
||||
defer extRegistryMtx.Unlock()
|
||||
extRegistry = append(extRegistry, entryPoint)
|
||||
}
|
||||
|
||||
func initExtensions(c *Conn) error {
|
||||
extRegistryMtx.RLock()
|
||||
defer extRegistryMtx.RUnlock()
|
||||
for _, f := range extRegistry {
|
||||
if err := f(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
41
sqlite.go
41
sqlite.go
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
)
|
||||
|
||||
// Configure SQLite Wasm.
|
||||
@@ -44,12 +45,14 @@ var instance struct {
|
||||
}
|
||||
|
||||
func compileSQLite() {
|
||||
if RuntimeConfig == nil {
|
||||
RuntimeConfig = wazero.NewRuntimeConfig()
|
||||
ctx := context.Background()
|
||||
cfg := RuntimeConfig
|
||||
if cfg == nil {
|
||||
cfg = wazero.NewRuntimeConfig()
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
instance.runtime = wazero.NewRuntimeWithConfig(ctx, RuntimeConfig)
|
||||
instance.runtime = wazero.NewRuntimeWithConfig(ctx,
|
||||
cfg.WithCoreFeatures(api.CoreFeaturesV2|experimental.CoreFeaturesThreads))
|
||||
|
||||
env := instance.runtime.NewHostModuleBuilder("env")
|
||||
env = vfs.ExportHostFunctions(env)
|
||||
@@ -82,8 +85,7 @@ type sqlite struct {
|
||||
id [32]*byte
|
||||
mask uint32
|
||||
}
|
||||
stack [8]uint64
|
||||
freer uint32
|
||||
stack [9]uint64
|
||||
}
|
||||
|
||||
func instantiateSQLite() (sqlt *sqlite, err error) {
|
||||
@@ -99,14 +101,7 @@ func instantiateSQLite() (sqlt *sqlite, err error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
global := sqlt.mod.ExportedGlobal("malloc_destructor")
|
||||
if global == nil {
|
||||
return nil, util.BadBinaryErr
|
||||
}
|
||||
|
||||
sqlt.freer = util.ReadUint32(sqlt.mod, uint32(global.Get()))
|
||||
if sqlt.freer == 0 {
|
||||
if sqlt.getfn("sqlite3_progress_handler_go") == nil {
|
||||
return nil, util.BadBinaryErr
|
||||
}
|
||||
return sqlt, nil
|
||||
@@ -193,14 +188,19 @@ func (sqlt *sqlite) free(ptr uint32) {
|
||||
if ptr == 0 {
|
||||
return
|
||||
}
|
||||
sqlt.call("free", uint64(ptr))
|
||||
sqlt.call("sqlite3_free", uint64(ptr))
|
||||
}
|
||||
|
||||
func (sqlt *sqlite) new(size uint64) uint32 {
|
||||
if size > _MAX_ALLOCATION_SIZE {
|
||||
ptr := uint32(sqlt.call("sqlite3_malloc64", size))
|
||||
if ptr == 0 && size != 0 {
|
||||
panic(util.OOMErr)
|
||||
}
|
||||
ptr := uint32(sqlt.call("malloc", size))
|
||||
return ptr
|
||||
}
|
||||
|
||||
func (sqlt *sqlite) realloc(ptr uint32, size uint64) uint32 {
|
||||
ptr = uint32(sqlt.call("sqlite3_realloc64", uint64(ptr), size))
|
||||
if ptr == 0 && size != 0 {
|
||||
panic(util.OOMErr)
|
||||
}
|
||||
@@ -211,7 +211,11 @@ func (sqlt *sqlite) newBytes(b []byte) uint32 {
|
||||
if (*[0]byte)(b) == nil {
|
||||
return 0
|
||||
}
|
||||
ptr := sqlt.new(uint64(len(b)))
|
||||
size := len(b)
|
||||
if size == 0 {
|
||||
size = 1
|
||||
}
|
||||
ptr := sqlt.new(uint64(size))
|
||||
util.WriteBytes(sqlt.mod, ptr, b)
|
||||
return ptr
|
||||
}
|
||||
@@ -303,6 +307,7 @@ func exportCallbacks(env wazero.HostModuleBuilder) wazero.HostModuleBuilder {
|
||||
util.ExportFuncVI(env, "go_rollback_hook", rollbackCallback)
|
||||
util.ExportFuncVIIIIJ(env, "go_update_hook", updateCallback)
|
||||
util.ExportFuncIIIII(env, "go_wal_hook", walCallback)
|
||||
util.ExportFuncIIIII(env, "go_trace", traceCallback)
|
||||
util.ExportFuncIIIIII(env, "go_autovacuum_pages", autoVacuumCallback)
|
||||
util.ExportFuncIIIIIII(env, "go_authorizer", authorizerCallback)
|
||||
util.ExportFuncVIII(env, "go_log", logCallback)
|
||||
|
||||
13
sqlite3/bind.c
Normal file
13
sqlite3/bind.c
Normal file
@@ -0,0 +1,13 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "sqlite3.h"
|
||||
|
||||
int sqlite3_bind_text_go(sqlite3_stmt* stmt, int i, const char* zData,
|
||||
sqlite3_uint64 nData) {
|
||||
return sqlite3_bind_text64(stmt, i, zData, nData, &sqlite3_free, SQLITE_UTF8);
|
||||
}
|
||||
|
||||
int sqlite3_bind_blob_go(sqlite3_stmt* stmt, int i, const char* zData,
|
||||
sqlite3_uint64 nData) {
|
||||
return sqlite3_bind_blob64(stmt, i, zData, nData, &sqlite3_free);
|
||||
}
|
||||
@@ -48,4 +48,4 @@ static_assert(offsetof(union sqlite3_data, i) == 0, "Unexpected offset");
|
||||
static_assert(offsetof(union sqlite3_data, d) == 0, "Unexpected offset");
|
||||
static_assert(offsetof(union sqlite3_data, ptr) == 0, "Unexpected offset");
|
||||
static_assert(offsetof(union sqlite3_data, len) == 4, "Unexpected offset");
|
||||
static_assert(sizeof(union sqlite3_data) == 8, "Unexpected size");
|
||||
static_assert(sizeof(union sqlite3_data) == 8, "Unexpected size");
|
||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
curl -#OL "https://sqlite.org/2024/sqlite-amalgamation-3460000.zip"
|
||||
curl -#OL "https://sqlite.org/2024/sqlite-amalgamation-3460100.zip"
|
||||
unzip -d . sqlite-amalgamation-*.zip
|
||||
mv sqlite-amalgamation-*/sqlite3* .
|
||||
rm -rf sqlite-amalgamation-*
|
||||
@@ -12,25 +12,24 @@ cat *.patch | patch --no-backup-if-mismatch
|
||||
|
||||
mkdir -p ext/
|
||||
cd ext/
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/anycollseq.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/base64.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/decimal.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/ieee754.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/regexp.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/series.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/uint.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/uuid.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/anycollseq.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/base64.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/decimal.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/ieee754.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/regexp.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/series.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/ext/misc/uint.c"
|
||||
cd ~-
|
||||
|
||||
cd ../vfs/tests/mptest/testdata/
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/mptest.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/config01.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/config02.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/crash01.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/crash02.subtest"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/multiwrite01.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/mptest.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/config01.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/config02.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/crash01.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/crash02.subtest"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/mptest/multiwrite01.test"
|
||||
cd ~-
|
||||
|
||||
cd ../vfs/tests/speedtest1/testdata/
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/test/speedtest1.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.1/test/speedtest1.c"
|
||||
cd ~-
|
||||
@@ -10,7 +10,7 @@ int go_commit_hook(void *);
|
||||
void go_rollback_hook(void *);
|
||||
void go_update_hook(void *, int, char const *, char const *, sqlite3_int64);
|
||||
int go_wal_hook(void *, sqlite3 *, const char *, int);
|
||||
|
||||
int go_trace(unsigned, void *, void *, void *);
|
||||
int go_authorizer(void *, int, const char *, const char *, const char *,
|
||||
const char *);
|
||||
|
||||
@@ -47,6 +47,10 @@ int sqlite3_set_authorizer_go(sqlite3 *db, bool enable) {
|
||||
return sqlite3_set_authorizer(db, enable ? go_authorizer : NULL, /*arg=*/db);
|
||||
}
|
||||
|
||||
int sqlite3_trace_go(sqlite3 *db, unsigned mask) {
|
||||
return sqlite3_trace_v2(db, mask, go_trace, /*arg=*/db);
|
||||
}
|
||||
|
||||
int sqlite3_config_log_go(bool enable) {
|
||||
return sqlite3_config(SQLITE_CONFIG_LOG, enable ? go_log : NULL,
|
||||
/*arg=*/NULL);
|
||||
|
||||
@@ -8,18 +8,17 @@
|
||||
#include "ext/regexp.c"
|
||||
#include "ext/series.c"
|
||||
#include "ext/uint.c"
|
||||
#include "ext/uuid.c"
|
||||
// Bindings
|
||||
#include "bind.c"
|
||||
#include "column.c"
|
||||
#include "func.c"
|
||||
#include "hooks.c"
|
||||
#include "pointer.c"
|
||||
#include "result.c"
|
||||
#include "time.c"
|
||||
#include "vfs.c"
|
||||
#include "vtab.c"
|
||||
|
||||
sqlite3_destructor_type malloc_destructor = &free;
|
||||
|
||||
__attribute__((constructor)) void init() {
|
||||
sqlite3_initialize();
|
||||
sqlite3_auto_extension((void (*)(void))sqlite3_base_init);
|
||||
@@ -28,6 +27,5 @@ __attribute__((constructor)) void init() {
|
||||
sqlite3_auto_extension((void (*)(void))sqlite3_regexp_init);
|
||||
sqlite3_auto_extension((void (*)(void))sqlite3_series_init);
|
||||
sqlite3_auto_extension((void (*)(void))sqlite3_uint_init);
|
||||
sqlite3_auto_extension((void (*)(void))sqlite3_uuid_init);
|
||||
sqlite3_auto_extension((void (*)(void))sqlite3_time_init);
|
||||
}
|
||||
13
sqlite3/result.c
Normal file
13
sqlite3/result.c
Normal file
@@ -0,0 +1,13 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "sqlite3.h"
|
||||
|
||||
void sqlite3_result_text_go(sqlite3_context* ctx, const char* zData,
|
||||
sqlite3_uint64 nData) {
|
||||
sqlite3_result_text64(ctx, zData, nData, &sqlite3_free, SQLITE_UTF8);
|
||||
}
|
||||
|
||||
void sqlite3_result_blob_go(sqlite3_context* ctx, const void* zData,
|
||||
sqlite3_uint64 nData) {
|
||||
sqlite3_result_blob64(ctx, zData, nData, &sqlite3_free);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user