mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-12 22:19:14 +00:00
Compare commits
166 Commits
v0.24.0
...
gormlite/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99b097de3b | ||
|
|
4a956e80a2 | ||
|
|
5f4ff03f6f | ||
|
|
5890049488 | ||
|
|
5e73c5d714 | ||
|
|
6d92aa16ef | ||
|
|
191d1337e7 | ||
|
|
b65e894849 | ||
|
|
0b040d3f09 | ||
|
|
1db4366226 | ||
|
|
9e1cbfb5bb | ||
|
|
7f2d70a0f3 | ||
|
|
ea860e407d | ||
|
|
d4561d08f9 | ||
|
|
14c1e490b4 | ||
|
|
23aad5f62f | ||
|
|
e5bd10a1ff | ||
|
|
5cf06c45f7 | ||
|
|
08f9fc758a | ||
|
|
b588d5f991 | ||
|
|
4c24bd0cb6 | ||
|
|
cc353e4848 | ||
|
|
c3ebb04045 | ||
|
|
11e064574c | ||
|
|
770420289a | ||
|
|
62f69011f1 | ||
|
|
4f9e3f900b | ||
|
|
4e90618350 | ||
|
|
54bb94ce58 | ||
|
|
07fec784e1 | ||
|
|
da4638cbff | ||
|
|
085872c2f3 | ||
|
|
de49aa2b06 | ||
|
|
1f3ad0165e | ||
|
|
0bda48d1d9 | ||
|
|
0026bc91aa | ||
|
|
d84ca9d627 | ||
|
|
5d14e01f94 | ||
|
|
342df983d4 | ||
|
|
00476fb1e2 | ||
|
|
8a64ee6eaa | ||
|
|
8f9a8e2752 | ||
|
|
d8880e4cee | ||
|
|
4b154a842c | ||
|
|
758a53e9bf | ||
|
|
1a42b4c590 | ||
|
|
7e4ec1df1c | ||
|
|
2c582a1d66 | ||
|
|
20a67ca669 | ||
|
|
789e2dc136 | ||
|
|
0399f10c06 | ||
|
|
75c6744b5b | ||
|
|
754e806164 | ||
|
|
2640c9fb54 | ||
|
|
9719d4b0e3 | ||
|
|
b21c69dc1f | ||
|
|
b0f8ff44a5 | ||
|
|
f37bca6a80 | ||
|
|
b4e8fcb752 | ||
|
|
14b98a5d05 | ||
|
|
36a62264f9 | ||
|
|
33ea564f38 | ||
|
|
5c55d8692f | ||
|
|
be2f3036b4 | ||
|
|
784f82f42f | ||
|
|
cd6ba43e77 | ||
|
|
d7aef63844 | ||
|
|
64e5046f10 | ||
|
|
0bdce8aa68 | ||
|
|
69a2881a10 | ||
|
|
24ad4445f1 | ||
|
|
c159bbd88f | ||
|
|
c90f8205f7 | ||
|
|
b64b9b0415 | ||
|
|
9142e19d61 | ||
|
|
4a76f2b064 | ||
|
|
c9b364507e | ||
|
|
2204b96ff6 | ||
|
|
b46f480d79 | ||
|
|
040a026925 | ||
|
|
e678040a4e | ||
|
|
f1cc12569c | ||
|
|
721a987e0e | ||
|
|
f3d65142cc | ||
|
|
93f711c77b | ||
|
|
341bd063e8 | ||
|
|
f765882670 | ||
|
|
ff3676ff4a | ||
|
|
54877a53cd | ||
|
|
fccc6c10a7 | ||
|
|
fc21ffcc71 | ||
|
|
687e643d7a | ||
|
|
fc5ced209c | ||
|
|
c1bed07e3a | ||
|
|
a0771f2363 | ||
|
|
6bad547d3d | ||
|
|
c2c1aea578 | ||
|
|
60ab485b29 | ||
|
|
e17a432fde | ||
|
|
c780ef16e2 | ||
|
|
b609930142 | ||
|
|
fd165ce724 | ||
|
|
d3973b23e3 | ||
|
|
320b68e74f | ||
|
|
2c3850e5d1 | ||
|
|
db7aacff9f | ||
|
|
d748d98e39 | ||
|
|
13b8642384 | ||
|
|
29c5c816cb | ||
|
|
b32db76da6 | ||
|
|
383f620a1e | ||
|
|
a3c3515e96 | ||
|
|
e580f080b9 | ||
|
|
9ea7099c24 | ||
|
|
29aa365806 | ||
|
|
bb87a920f7 | ||
|
|
48379336dc | ||
|
|
251a92fa1a | ||
|
|
f5206ea8da | ||
|
|
68ef4593d6 | ||
|
|
79bf171210 | ||
|
|
ad16d329ea | ||
|
|
9706fa9607 | ||
|
|
45494f5fb6 | ||
|
|
1b0bf3495e | ||
|
|
73ac7e06f6 | ||
|
|
a3ce8f9de5 | ||
|
|
2043d5fca4 | ||
|
|
3bd11a0a86 | ||
|
|
39f3fa64eb | ||
|
|
4c19387535 | ||
|
|
e6c9f18934 | ||
|
|
970eb6a2f9 | ||
|
|
fac27b8bab | ||
|
|
9f626b2f52 | ||
|
|
1f5d8bf7df | ||
|
|
41dc46af7e | ||
|
|
e5c285b783 | ||
|
|
6290a14990 | ||
|
|
948641194b | ||
|
|
befed7cf23 | ||
|
|
3547d9ffb0 | ||
|
|
a67165eb09 | ||
|
|
0ba393199a | ||
|
|
9e4258bc46 | ||
|
|
b645721d10 | ||
|
|
6c296231a5 | ||
|
|
c067e3630b | ||
|
|
35a2dbd847 | ||
|
|
b36f73c66d | ||
|
|
d36f19fd91 | ||
|
|
eba71b1f42 | ||
|
|
d78239bfbf | ||
|
|
49852732b2 | ||
|
|
9b90d076cb | ||
|
|
15b94577b1 | ||
|
|
25557244cc | ||
|
|
c2d3bf0cfc | ||
|
|
58a5682084 | ||
|
|
1ed954e96f | ||
|
|
9e7a0a875d | ||
|
|
26adda4529 | ||
|
|
2f6cd8de1d | ||
|
|
e027e055ff | ||
|
|
63fdc141e5 | ||
|
|
0bbd145a49 |
23
.github/workflows/libc.yml
vendored
Normal file
23
.github/workflows/libc.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Benchmark libc
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Benchmark
|
||||
shell: bash
|
||||
run: sqlite3/libc/benchmark.sh
|
||||
21
.github/workflows/repro.sh
vendored
21
.github/workflows/repro.sh
vendored
@@ -1,28 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$OSTYPE" == "linux"* ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-x86_64-linux.tar.gz"
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-arm64-macos.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-arm64-macos.tar.gz"
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-windows.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-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" ] || curl -#L "$BINARYEN" | tar xzC tools &
|
||||
wait
|
||||
|
||||
[ -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
|
||||
sqlite3/tools.sh
|
||||
embed/build.sh
|
||||
embed/bcw2/build.sh
|
||||
|
||||
|
||||
4
.github/workflows/repro.yml
vendored
4
.github/workflows/repro.yml
vendored
@@ -17,13 +17,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: .github/workflows/repro.sh
|
||||
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
- uses: actions/attest-build-provenance@v3
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
with:
|
||||
subject-path: |
|
||||
|
||||
51
.github/workflows/test.yml
vendored
51
.github/workflows/test.yml
vendored
@@ -17,16 +17,21 @@ on:
|
||||
- '**.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Format
|
||||
@@ -93,27 +98,27 @@ jobs:
|
||||
matrix:
|
||||
os:
|
||||
- name: freebsd
|
||||
version: '14.2'
|
||||
version: '14.3'
|
||||
flags: '-test.v'
|
||||
- name: netbsd
|
||||
version: '10.1'
|
||||
flags: '-test.v'
|
||||
- name: freebsd
|
||||
arch: arm64
|
||||
version: '14.2'
|
||||
version: '14.3'
|
||||
flags: '-test.v -test.short'
|
||||
- name: netbsd
|
||||
arch: arm64
|
||||
version: '10.1'
|
||||
flags: '-test.v -test.short'
|
||||
- name: openbsd
|
||||
version: '7.6'
|
||||
version: '7.8'
|
||||
flags: '-test.v -test.short'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
@@ -123,7 +128,7 @@ jobs:
|
||||
run: .github/workflows/build-test.sh
|
||||
|
||||
- name: Test
|
||||
uses: cross-platform-actions/action@v0.27.0
|
||||
uses: cross-platform-actions/action@v0.30.0
|
||||
with:
|
||||
operating_system: ${{ matrix.os.name }}
|
||||
architecture: ${{ matrix.os.arch }}
|
||||
@@ -150,7 +155,7 @@ jobs:
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
@@ -169,8 +174,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: bytecodealliance/actions/wasmtime/setup@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Set path
|
||||
@@ -190,8 +195,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Test 386 (32-bit)
|
||||
@@ -211,20 +216,32 @@ jobs:
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
test-macintel:
|
||||
runs-on: macos-13
|
||||
runs-on: macos-15-intel
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
test-winarm:
|
||||
runs-on: windows-11-arm
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Test
|
||||
|
||||
51
README.md
51
README.md
@@ -30,10 +30,10 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
|
||||
|
||||
- [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
|
||||
wraps the [C SQLite API](https://sqlite.org/cintro.html)
|
||||
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-package)).
|
||||
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-package)).
|
||||
- [`github.com/ncruces/go-sqlite3/driver`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver)
|
||||
provides a [`database/sql`](https://pkg.go.dev/database/sql) driver
|
||||
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package)).
|
||||
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package)).
|
||||
- [`github.com/ncruces/go-sqlite3/embed`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/embed)
|
||||
embeds a build of SQLite into your application.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs)
|
||||
@@ -44,12 +44,19 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
|
||||
### Advanced features
|
||||
|
||||
- [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html)
|
||||
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blobio#example-package))
|
||||
- [nested transactions](https://sqlite.org/lang_savepoint.html)
|
||||
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-Savepoint))
|
||||
- [custom functions](https://sqlite.org/c3ref/create_function.html)
|
||||
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-Conn.CreateFunction))
|
||||
- [virtual tables](https://sqlite.org/vtab.html)
|
||||
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-CreateModule))
|
||||
- [custom VFSes](https://sqlite.org/vfs.html)
|
||||
([examples](vfs/README.md#custom-vfses))
|
||||
- [online backup](https://sqlite.org/backup.html)
|
||||
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#Conn))
|
||||
- [JSON support](https://sqlite.org/json1.html)
|
||||
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package-Json))
|
||||
- [math functions](https://sqlite.org/lang_mathfunc.html)
|
||||
- [full-text search](https://sqlite.org/fts5.html)
|
||||
- [geospatial search](https://sqlite.org/geopoly.html)
|
||||
@@ -57,7 +64,6 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
|
||||
- [statistics functions](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
|
||||
- [encryption at rest](vfs/adiantum/README.md)
|
||||
- [many extensions](ext/README.md)
|
||||
- [custom VFSes](vfs/README.md#custom-vfses)
|
||||
- [and more…](embed/README.md)
|
||||
|
||||
### Caveats
|
||||
@@ -65,31 +71,52 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
|
||||
This module replaces the SQLite [OS Interface](https://sqlite.org/vfs.html)
|
||||
(aka VFS) with a [pure Go](vfs/) implementation,
|
||||
which has advantages and disadvantages.
|
||||
|
||||
Read more about the Go VFS design [here](vfs/README.md).
|
||||
|
||||
Because each database connection executes within a Wasm sandboxed environment,
|
||||
memory usage will be higher than alternatives.
|
||||
|
||||
### Testing
|
||||
|
||||
This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3/wiki/Test-coverage-report).
|
||||
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.
|
||||
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach)
|
||||
thorough testing.
|
||||
|
||||
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) on
|
||||
Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64),
|
||||
Windows (amd64), FreeBSD (amd64/arm64), OpenBSD (amd64), NetBSD (amd64/arm64),
|
||||
DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64).
|
||||
Every commit is tested on:
|
||||
* Linux: amd64, arm64, 386, riscv64, ppc64le, s390x
|
||||
* macOS: amd64, arm64
|
||||
* Windows: amd64, arm64
|
||||
* BSD:
|
||||
* FreeBSD: amd64, arm64
|
||||
* NetBSD: amd64, arm64
|
||||
* DragonFly BSD: amd64
|
||||
* OpenBSD: amd64
|
||||
* illumos: amd64
|
||||
* Solaris: amd64
|
||||
|
||||
Certain operating system and CPU combinations have some limitations. See the [support matrix](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) for a complete overview.
|
||||
|
||||
The Go VFS is tested by running SQLite's
|
||||
[mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c).
|
||||
|
||||
### Performance
|
||||
|
||||
Perfomance of the [`database/sql`](https://pkg.go.dev/database/sql) driver is
|
||||
Performance of the [`database/sql`](https://pkg.go.dev/database/sql) driver is
|
||||
[competitive](https://github.com/cvilsmeier/go-sqlite-bench) with alternatives.
|
||||
|
||||
The Wasm and VFS layers are also tested by running SQLite's
|
||||
The Wasm and VFS layers are also benchmarked by running SQLite's
|
||||
[speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c).
|
||||
|
||||
### Concurrency
|
||||
|
||||
This module behaves similarly to SQLite in [multi-thread](https://sqlite.org/threadsafe.html) mode:
|
||||
it is goroutine-safe, provided that no single database connection, or object derived from it,
|
||||
is used concurrently by multiple goroutines.
|
||||
|
||||
The [`database/sql`](https://pkg.go.dev/database/sql) API is safe to use concurrently,
|
||||
according to its documentation.
|
||||
|
||||
### FAQ, issues, new features
|
||||
|
||||
For questions, please see [Discussions](https://github.com/ncruces/go-sqlite3/discussions/categories/q-a).
|
||||
@@ -106,4 +133,4 @@ and features we're working on, planning to work on, or asking for help with.
|
||||
- [`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite)
|
||||
- [`crawshaw.io/sqlite`](https://pkg.go.dev/crawshaw.io/sqlite)
|
||||
- [`github.com/mattn/go-sqlite3`](https://pkg.go.dev/github.com/mattn/go-sqlite3)
|
||||
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)
|
||||
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)
|
||||
|
||||
9
blob.go
9
blob.go
@@ -31,6 +31,10 @@ var _ io.ReadWriteSeeker = &Blob{}
|
||||
//
|
||||
// https://sqlite.org/c3ref/blob_open.html
|
||||
func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob, error) {
|
||||
if c.interrupt.Err() != nil {
|
||||
return nil, INTERRUPT
|
||||
}
|
||||
|
||||
defer c.arena.mark()()
|
||||
blobPtr := c.arena.new(ptrlen)
|
||||
dbPtr := c.arena.string(db)
|
||||
@@ -42,7 +46,6 @@ func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob,
|
||||
flags = 1
|
||||
}
|
||||
|
||||
c.checkInterrupt(c.handle)
|
||||
rc := res_t(c.call("sqlite3_blob_open", stk_t(c.handle),
|
||||
stk_t(dbPtr), stk_t(tablePtr), stk_t(columnPtr),
|
||||
stk_t(row), stk_t(flags), stk_t(blobPtr)))
|
||||
@@ -253,7 +256,9 @@ func (b *Blob) Seek(offset int64, whence int) (int64, error) {
|
||||
//
|
||||
// https://sqlite.org/c3ref/blob_reopen.html
|
||||
func (b *Blob) Reopen(row int64) error {
|
||||
b.c.checkInterrupt(b.c.handle)
|
||||
if b.c.interrupt.Err() != nil {
|
||||
return INTERRUPT
|
||||
}
|
||||
err := b.c.error(res_t(b.c.call("sqlite3_blob_reopen", stk_t(b.handle), stk_t(row))))
|
||||
b.bytes = int64(int32(b.c.call("sqlite3_blob_bytes", stk_t(b.handle))))
|
||||
b.offset = 0
|
||||
|
||||
20
config.go
20
config.go
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
|
||||
@@ -48,6 +49,15 @@ func (c *Conn) Config(op DBConfig, arg ...bool) (bool, error) {
|
||||
return util.ReadBool(c.mod, argsPtr), c.error(rc)
|
||||
}
|
||||
|
||||
var defaultLogger atomic.Pointer[func(code ExtendedErrorCode, msg string)]
|
||||
|
||||
// ConfigLog sets up the default error logging callback for new connections.
|
||||
//
|
||||
// https://sqlite.org/errlog.html
|
||||
func ConfigLog(cb func(code ExtendedErrorCode, msg string)) {
|
||||
defaultLogger.Store(&cb)
|
||||
}
|
||||
|
||||
// ConfigLog sets up the error logging callback for the connection.
|
||||
//
|
||||
// https://sqlite.org/errlog.html
|
||||
@@ -99,7 +109,7 @@ func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, erro
|
||||
default:
|
||||
return nil, MISUSE
|
||||
|
||||
case FCNTL_RESET_CACHE:
|
||||
case FCNTL_RESET_CACHE, FCNTL_NULL_IO:
|
||||
rc = res_t(c.call("sqlite3_file_control",
|
||||
stk_t(c.handle), stk_t(schemaPtr),
|
||||
stk_t(op), 0))
|
||||
@@ -255,7 +265,7 @@ func traceCallback(ctx context.Context, mod api.Module, evt TraceEvent, pDB, pAr
|
||||
}
|
||||
}
|
||||
if arg1 != nil {
|
||||
_, rc = errorCode(c.trace(evt, arg1, arg2), ERROR)
|
||||
_ = c.trace(evt, arg1, arg2)
|
||||
}
|
||||
}
|
||||
return rc
|
||||
@@ -265,6 +275,10 @@ func traceCallback(ctx context.Context, mod api.Module, evt TraceEvent, pDB, pAr
|
||||
//
|
||||
// https://sqlite.org/c3ref/wal_checkpoint_v2.html
|
||||
func (c *Conn) WALCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt int, err error) {
|
||||
if c.interrupt.Err() != nil {
|
||||
return 0, 0, INTERRUPT
|
||||
}
|
||||
|
||||
defer c.arena.mark()()
|
||||
nLogPtr := c.arena.new(ptrlen)
|
||||
nCkptPtr := c.arena.new(ptrlen)
|
||||
@@ -378,6 +392,6 @@ func (c *Conn) EnableChecksums(schema string) error {
|
||||
}
|
||||
|
||||
// Checkpoint the WAL.
|
||||
_, _, err = c.WALCheckpoint(schema, CHECKPOINT_RESTART)
|
||||
_, _, err = c.WALCheckpoint(schema, CHECKPOINT_FULL)
|
||||
return err
|
||||
}
|
||||
|
||||
118
conn.go
118
conn.go
@@ -25,7 +25,6 @@ type Conn struct {
|
||||
*sqlite
|
||||
|
||||
interrupt context.Context
|
||||
pending *Stmt
|
||||
stmts []*Stmt
|
||||
busy func(context.Context, int) bool
|
||||
log func(xErrorCode, string)
|
||||
@@ -41,6 +40,7 @@ type Conn struct {
|
||||
busylst time.Time
|
||||
arena arena
|
||||
handle ptr_t
|
||||
gosched uint8
|
||||
}
|
||||
|
||||
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE] and [OPEN_URI].
|
||||
@@ -49,7 +49,7 @@ func Open(filename string) (*Conn, error) {
|
||||
}
|
||||
|
||||
// OpenContext is like [Open] but includes a context,
|
||||
// which is used to interrupt the process of opening the connectiton.
|
||||
// which is used to interrupt the process of opening the connection.
|
||||
func OpenContext(ctx context.Context, filename string) (*Conn, error) {
|
||||
return newConn(ctx, filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
|
||||
}
|
||||
@@ -92,6 +92,9 @@ func newConn(ctx context.Context, filename string, flags OpenFlag) (ret *Conn, _
|
||||
}()
|
||||
|
||||
c.ctx = context.WithValue(c.ctx, connKey{}, c)
|
||||
if logger := defaultLogger.Load(); logger != nil {
|
||||
c.ConfigLog(*logger)
|
||||
}
|
||||
c.arena = c.newArena()
|
||||
c.handle, err = c.openDB(filename, flags)
|
||||
if err == nil {
|
||||
@@ -117,7 +120,7 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (ptr_t, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
c.call("sqlite3_progress_handler_go", stk_t(handle), 100)
|
||||
c.call("sqlite3_progress_handler_go", stk_t(handle), 1000)
|
||||
if flags|OPEN_URI != 0 && strings.HasPrefix(filename, "file:") {
|
||||
var pragmas strings.Builder
|
||||
if _, after, ok := strings.Cut(filename, "?"); ok {
|
||||
@@ -129,7 +132,6 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (ptr_t, error) {
|
||||
}
|
||||
}
|
||||
if pragmas.Len() != 0 {
|
||||
c.checkInterrupt(handle)
|
||||
pragmaPtr := c.arena.string(pragmas.String())
|
||||
rc := res_t(c.call("sqlite3_exec", stk_t(handle), stk_t(pragmaPtr), 0, 0, 0))
|
||||
if err := c.sqlite.error(rc, handle, pragmas.String()); err != nil {
|
||||
@@ -163,9 +165,6 @@ func (c *Conn) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.pending.Close()
|
||||
c.pending = nil
|
||||
|
||||
rc := res_t(c.call("sqlite3_close", stk_t(c.handle)))
|
||||
if err := c.error(rc); err != nil {
|
||||
return err
|
||||
@@ -180,11 +179,16 @@ func (c *Conn) Close() error {
|
||||
//
|
||||
// https://sqlite.org/c3ref/exec.html
|
||||
func (c *Conn) Exec(sql string) error {
|
||||
defer c.arena.mark()()
|
||||
sqlPtr := c.arena.string(sql)
|
||||
if c.interrupt.Err() != nil {
|
||||
return INTERRUPT
|
||||
}
|
||||
return c.exec(sql)
|
||||
}
|
||||
|
||||
c.checkInterrupt(c.handle)
|
||||
rc := res_t(c.call("sqlite3_exec", stk_t(c.handle), stk_t(sqlPtr), 0, 0, 0))
|
||||
func (c *Conn) exec(sql string) error {
|
||||
defer c.arena.mark()()
|
||||
textPtr := c.arena.string(sql)
|
||||
rc := res_t(c.call("sqlite3_exec", stk_t(c.handle), stk_t(textPtr), 0, 0, 0))
|
||||
return c.error(rc, sql)
|
||||
}
|
||||
|
||||
@@ -203,20 +207,22 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
|
||||
if len(sql) > _MAX_SQL_LENGTH {
|
||||
return nil, "", TOOBIG
|
||||
}
|
||||
if c.interrupt.Err() != nil {
|
||||
return nil, "", INTERRUPT
|
||||
}
|
||||
|
||||
defer c.arena.mark()()
|
||||
stmtPtr := c.arena.new(ptrlen)
|
||||
tailPtr := c.arena.new(ptrlen)
|
||||
sqlPtr := c.arena.string(sql)
|
||||
textPtr := c.arena.string(sql)
|
||||
|
||||
c.checkInterrupt(c.handle)
|
||||
rc := res_t(c.call("sqlite3_prepare_v3", stk_t(c.handle),
|
||||
stk_t(sqlPtr), stk_t(len(sql)+1), stk_t(flags),
|
||||
stk_t(textPtr), stk_t(len(sql)+1), stk_t(flags),
|
||||
stk_t(stmtPtr), stk_t(tailPtr)))
|
||||
|
||||
stmt = &Stmt{c: c}
|
||||
stmt = &Stmt{c: c, sql: sql}
|
||||
stmt.handle = util.Read32[ptr_t](c.mod, stmtPtr)
|
||||
if sql := sql[util.Read32[ptr_t](c.mod, tailPtr)-sqlPtr:]; sql != "" {
|
||||
if sql := sql[util.Read32[ptr_t](c.mod, tailPtr)-textPtr:]; sql != "" {
|
||||
tail = sql
|
||||
}
|
||||
|
||||
@@ -337,43 +343,17 @@ func (c *Conn) GetInterrupt() context.Context {
|
||||
//
|
||||
// https://sqlite.org/c3ref/interrupt.html
|
||||
func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
|
||||
if ctx == nil {
|
||||
panic("nil Context")
|
||||
}
|
||||
old = c.interrupt
|
||||
c.interrupt = ctx
|
||||
|
||||
if ctx == old || ctx.Done() == old.Done() {
|
||||
return old
|
||||
}
|
||||
|
||||
// A busy SQL statement prevents SQLite from ignoring an interrupt
|
||||
// that comes before any other statements are started.
|
||||
if c.pending == nil {
|
||||
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", stk_t(c.handle), stk_t(loopPtr), math.MaxUint64,
|
||||
stk_t(PREPARE_PERSISTENT), stk_t(stmtPtr), 0)
|
||||
c.pending = &Stmt{c: c}
|
||||
c.pending.handle = util.Read32[ptr_t](c.mod, stmtPtr)
|
||||
}
|
||||
|
||||
if old.Done() != nil && ctx.Err() == nil {
|
||||
c.pending.Reset()
|
||||
}
|
||||
if ctx.Done() != nil {
|
||||
c.pending.Step()
|
||||
}
|
||||
return old
|
||||
}
|
||||
|
||||
func (c *Conn) checkInterrupt(handle ptr_t) {
|
||||
if c.interrupt.Err() != nil {
|
||||
c.call("sqlite3_interrupt", stk_t(handle))
|
||||
}
|
||||
}
|
||||
|
||||
func progressCallback(ctx context.Context, mod api.Module, _ ptr_t) (interrupt int32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok {
|
||||
if c.interrupt.Done() != nil {
|
||||
if c.gosched++; c.gosched%16 == 0 {
|
||||
runtime.Gosched()
|
||||
}
|
||||
if c.interrupt.Err() != nil {
|
||||
@@ -429,11 +409,8 @@ func (c *Conn) BusyHandler(cb func(ctx context.Context, count int) (retry bool))
|
||||
|
||||
func busyCallback(ctx context.Context, mod api.Module, pDB ptr_t, count int32) (retry int32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil {
|
||||
interrupt := c.interrupt
|
||||
if interrupt == nil {
|
||||
interrupt = context.Background()
|
||||
}
|
||||
if interrupt.Err() == nil && c.busy(interrupt, int(count)) {
|
||||
if interrupt := c.interrupt; interrupt.Err() == nil &&
|
||||
c.busy(interrupt, int(count)) {
|
||||
retry = 1
|
||||
}
|
||||
}
|
||||
@@ -443,21 +420,21 @@ func busyCallback(ctx context.Context, mod api.Module, pDB ptr_t, count int32) (
|
||||
// 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) {
|
||||
func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int64, err error) {
|
||||
defer c.arena.mark()()
|
||||
hiPtr := c.arena.new(intlen)
|
||||
curPtr := c.arena.new(intlen)
|
||||
hiPtr := c.arena.new(8)
|
||||
curPtr := c.arena.new(8)
|
||||
|
||||
var i int32
|
||||
if reset {
|
||||
i = 1
|
||||
}
|
||||
|
||||
rc := res_t(c.call("sqlite3_db_status", stk_t(c.handle),
|
||||
rc := res_t(c.call("sqlite3_db_status64", stk_t(c.handle),
|
||||
stk_t(op), stk_t(curPtr), stk_t(hiPtr), stk_t(i)))
|
||||
if err = c.error(rc); err == nil {
|
||||
current = int(util.Read32[int32](c.mod, curPtr))
|
||||
highwater = int(util.Read32[int32](c.mod, hiPtr))
|
||||
current = util.Read64[int64](c.mod, curPtr)
|
||||
highwater = util.Read64[int64](c.mod, hiPtr)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -467,20 +444,27 @@ func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int, err erro
|
||||
// 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 ptr_t
|
||||
declTypePtr := c.arena.new(ptrlen)
|
||||
collSeqPtr := c.arena.new(ptrlen)
|
||||
notNullPtr := c.arena.new(ptrlen)
|
||||
autoIncPtr := c.arena.new(ptrlen)
|
||||
primaryKeyPtr := c.arena.new(ptrlen)
|
||||
var (
|
||||
declTypePtr ptr_t
|
||||
collSeqPtr ptr_t
|
||||
notNullPtr ptr_t
|
||||
primaryKeyPtr ptr_t
|
||||
autoIncPtr ptr_t
|
||||
columnPtr ptr_t
|
||||
schemaPtr ptr_t
|
||||
)
|
||||
if column != "" {
|
||||
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)
|
||||
columnPtr = c.arena.string(column)
|
||||
}
|
||||
if schema != "" {
|
||||
schemaPtr = c.arena.string(schema)
|
||||
}
|
||||
tablePtr := c.arena.string(table)
|
||||
if column != "" {
|
||||
columnPtr = c.arena.string(column)
|
||||
}
|
||||
|
||||
rc := res_t(c.call("sqlite3_table_column_metadata", stk_t(c.handle),
|
||||
stk_t(schemaPtr), stk_t(tablePtr), stk_t(columnPtr),
|
||||
|
||||
39
const.go
39
const.go
@@ -11,10 +11,9 @@ const (
|
||||
_ROW = 100 /* sqlite3_step() has another row ready */
|
||||
_DONE = 101 /* sqlite3_step() has finished executing */
|
||||
|
||||
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
|
||||
_MAX_LENGTH = 1e9
|
||||
_MAX_SQL_LENGTH = 1e9
|
||||
_MAX_FUNCTION_ARG = 100
|
||||
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
|
||||
_MAX_LENGTH = 1e9
|
||||
_MAX_SQL_LENGTH = 1e9
|
||||
|
||||
ptrlen = util.PtrLen
|
||||
intlen = util.IntLen
|
||||
@@ -74,6 +73,9 @@ const (
|
||||
ERROR_MISSING_COLLSEQ ExtendedErrorCode = xErrorCode(ERROR) | (1 << 8)
|
||||
ERROR_RETRY ExtendedErrorCode = xErrorCode(ERROR) | (2 << 8)
|
||||
ERROR_SNAPSHOT ExtendedErrorCode = xErrorCode(ERROR) | (3 << 8)
|
||||
ERROR_RESERVESIZE ExtendedErrorCode = xErrorCode(ERROR) | (4 << 8)
|
||||
ERROR_KEY ExtendedErrorCode = xErrorCode(ERROR) | (5 << 8)
|
||||
ERROR_UNABLE ExtendedErrorCode = xErrorCode(ERROR) | (6 << 8)
|
||||
IOERR_READ ExtendedErrorCode = xErrorCode(IOERR) | (1 << 8)
|
||||
IOERR_SHORT_READ ExtendedErrorCode = xErrorCode(IOERR) | (2 << 8)
|
||||
IOERR_WRITE ExtendedErrorCode = xErrorCode(IOERR) | (3 << 8)
|
||||
@@ -108,6 +110,8 @@ const (
|
||||
IOERR_DATA ExtendedErrorCode = xErrorCode(IOERR) | (32 << 8)
|
||||
IOERR_CORRUPTFS ExtendedErrorCode = xErrorCode(IOERR) | (33 << 8)
|
||||
IOERR_IN_PAGE ExtendedErrorCode = xErrorCode(IOERR) | (34 << 8)
|
||||
IOERR_BADKEY ExtendedErrorCode = xErrorCode(IOERR) | (35 << 8)
|
||||
IOERR_CODEC ExtendedErrorCode = xErrorCode(IOERR) | (36 << 8)
|
||||
LOCKED_SHAREDCACHE ExtendedErrorCode = xErrorCode(LOCKED) | (1 << 8)
|
||||
LOCKED_VTAB ExtendedErrorCode = xErrorCode(LOCKED) | (2 << 8)
|
||||
BUSY_RECOVERY ExtendedErrorCode = xErrorCode(BUSY) | (1 << 8)
|
||||
@@ -186,12 +190,12 @@ const (
|
||||
type FunctionFlag uint32
|
||||
|
||||
const (
|
||||
DETERMINISTIC FunctionFlag = 0x000000800
|
||||
DIRECTONLY FunctionFlag = 0x000080000
|
||||
INNOCUOUS FunctionFlag = 0x000200000
|
||||
SELFORDER1 FunctionFlag = 0x002000000
|
||||
// SUBTYPE FunctionFlag = 0x000100000
|
||||
// RESULT_SUBTYPE FunctionFlag = 0x001000000
|
||||
DETERMINISTIC FunctionFlag = 0x000000800
|
||||
DIRECTONLY FunctionFlag = 0x000080000
|
||||
SUBTYPE FunctionFlag = 0x000100000
|
||||
INNOCUOUS FunctionFlag = 0x000200000
|
||||
RESULT_SUBTYPE FunctionFlag = 0x001000000
|
||||
SELFORDER1 FunctionFlag = 0x002000000
|
||||
)
|
||||
|
||||
// StmtStatus name counter values associated with the [Stmt.Status] method.
|
||||
@@ -230,7 +234,8 @@ const (
|
||||
DBSTATUS_DEFERRED_FKS DBStatus = 10
|
||||
DBSTATUS_CACHE_USED_SHARED DBStatus = 11
|
||||
DBSTATUS_CACHE_SPILL DBStatus = 12
|
||||
// DBSTATUS_MAX DBStatus = 12
|
||||
DBSTATUS_TEMPBUF_SPILL DBStatus = 13
|
||||
// DBSTATUS_MAX DBStatus = 13
|
||||
)
|
||||
|
||||
// DBConfig are the available database connection configuration options.
|
||||
@@ -281,6 +286,7 @@ const (
|
||||
FCNTL_DATA_VERSION FcntlOpcode = 35
|
||||
FCNTL_RESERVE_BYTES FcntlOpcode = 38
|
||||
FCNTL_RESET_CACHE FcntlOpcode = 42
|
||||
FCNTL_NULL_IO FcntlOpcode = 43
|
||||
)
|
||||
|
||||
// LimitCategory are the available run-time limit categories.
|
||||
@@ -362,13 +368,14 @@ const (
|
||||
// CheckpointMode are all the checkpoint mode values.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_checkpoint_full.html
|
||||
type CheckpointMode uint32
|
||||
type CheckpointMode int32
|
||||
|
||||
const (
|
||||
CHECKPOINT_PASSIVE CheckpointMode = 0 /* Do as much as possible w/o blocking */
|
||||
CHECKPOINT_FULL CheckpointMode = 1 /* Wait for writers, then checkpoint */
|
||||
CHECKPOINT_RESTART CheckpointMode = 2 /* Like FULL but wait for readers */
|
||||
CHECKPOINT_TRUNCATE CheckpointMode = 3 /* Like RESTART but also truncate WAL */
|
||||
CHECKPOINT_NOOP CheckpointMode = -1 /* Do no work at all */
|
||||
CHECKPOINT_PASSIVE CheckpointMode = 0 /* Do as much as possible w/o blocking */
|
||||
CHECKPOINT_FULL CheckpointMode = 1 /* Wait for writers, then checkpoint */
|
||||
CHECKPOINT_RESTART CheckpointMode = 2 /* Like FULL but wait for readers */
|
||||
CHECKPOINT_TRUNCATE CheckpointMode = 3 /* Like RESTART but also truncate WAL */
|
||||
)
|
||||
|
||||
// TxnState are the allowed return values from [Conn.TxnState].
|
||||
|
||||
35
context.go
35
context.go
@@ -1,7 +1,6 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
@@ -89,20 +88,26 @@ func (ctx Context) ResultText(value string) {
|
||||
}
|
||||
|
||||
// ResultRawText sets the text result of the function to a []byte.
|
||||
// Returning a nil slice is the same as calling [Context.ResultNull].
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultRawText(value []byte) {
|
||||
if len(value) == 0 {
|
||||
ctx.ResultText("")
|
||||
return
|
||||
}
|
||||
ptr := ctx.c.newBytes(value)
|
||||
ctx.c.call("sqlite3_result_text_go",
|
||||
stk_t(ctx.handle), stk_t(ptr), stk_t(len(value)))
|
||||
}
|
||||
|
||||
// ResultBlob sets the result of the function to a []byte.
|
||||
// Returning a nil slice is the same as calling [Context.ResultNull].
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultBlob(value []byte) {
|
||||
if len(value) == 0 {
|
||||
ctx.ResultZeroBlob(0)
|
||||
return
|
||||
}
|
||||
ptr := ctx.c.newBytes(value)
|
||||
ctx.c.call("sqlite3_result_blob_go",
|
||||
stk_t(ctx.handle), stk_t(ptr), stk_t(len(value)))
|
||||
@@ -167,18 +172,6 @@ func (ctx Context) ResultPointer(ptr any) {
|
||||
stk_t(ctx.handle), stk_t(valPtr))
|
||||
}
|
||||
|
||||
// ResultJSON sets the result of the function to the JSON encoding of value.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultJSON(value any) {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
ctx.ResultRawText(data)
|
||||
}
|
||||
|
||||
// ResultValue sets the result of the function to a copy of [Value].
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
@@ -205,19 +198,27 @@ func (ctx Context) ResultError(err error) {
|
||||
return
|
||||
}
|
||||
|
||||
msg, code := errorCode(err, _OK)
|
||||
msg, code := errorCode(err, ERROR)
|
||||
if msg != "" {
|
||||
defer ctx.c.arena.mark()()
|
||||
ptr := ctx.c.arena.string(msg)
|
||||
ctx.c.call("sqlite3_result_error",
|
||||
stk_t(ctx.handle), stk_t(ptr), stk_t(len(msg)))
|
||||
}
|
||||
if code != _OK {
|
||||
if code != res_t(ERROR) {
|
||||
ctx.c.call("sqlite3_result_error_code",
|
||||
stk_t(ctx.handle), stk_t(code))
|
||||
}
|
||||
}
|
||||
|
||||
// ResultSubtype sets the subtype of the result of the function.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_subtype.html
|
||||
func (ctx Context) ResultSubtype(t uint) {
|
||||
ctx.c.call("sqlite3_result_subtype",
|
||||
stk_t(ctx.handle), stk_t(uint32(t)))
|
||||
}
|
||||
|
||||
// VTabNoChange may return true if a column is being fetched as part
|
||||
// of an update during which the column value will not change.
|
||||
//
|
||||
|
||||
327
driver/driver.go
327
driver/driver.go
@@ -20,22 +20,45 @@
|
||||
// - a [serializable] transaction is always "immediate";
|
||||
// - a [read-only] transaction is always "deferred".
|
||||
//
|
||||
// # Datatypes In SQLite
|
||||
//
|
||||
// SQLite is dynamically typed.
|
||||
// Columns can mostly hold any value regardless of their declared type.
|
||||
// SQLite supports most [driver.Value] types out of the box,
|
||||
// but bool and [time.Time] require special care.
|
||||
//
|
||||
// Booleans can be stored on any column type and scanned back to a *bool.
|
||||
// However, if scanned to a *any, booleans may either become an
|
||||
// int64, string or bool, depending on the declared type of the column.
|
||||
// If you use BOOLEAN for your column type,
|
||||
// 1 and 0 will always scan as true and false.
|
||||
//
|
||||
// # Working with time
|
||||
//
|
||||
// Time values can similarly be stored on any column type.
|
||||
// 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";
|
||||
// Special 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.
|
||||
//
|
||||
// If you encode as RFC 3339 (the default),
|
||||
// consider using the TIME [collating sequence] to produce a time-ordered sequence.
|
||||
// You can also set "_timefmt" to an arbitrary [sqlite3.TimeFormat] or [time.Layout].
|
||||
//
|
||||
// To scan values in other formats, [sqlite3.TimeFormat.Scanner] may be helpful.
|
||||
// To bind values in other formats, [sqlite3.TimeFormat.Encode] them before binding.
|
||||
// If you encode as RFC 3339 (the default),
|
||||
// consider using the TIME [collating sequence] to produce time-ordered sequences.
|
||||
//
|
||||
// If you encode as RFC 3339 (the default),
|
||||
// time values will scan back to a *time.Time unless your column type is TEXT.
|
||||
// Otherwise, if scanned to a *any, time values may either become an
|
||||
// int64, float64 or string, depending on the time format and declared type of the column.
|
||||
// If you use DATE, TIME, DATETIME, or TIMESTAMP for your column type,
|
||||
// "_timefmt" will be used to decode values.
|
||||
//
|
||||
// To scan values in custom formats, [sqlite3.TimeFormat.Scanner] may be helpful.
|
||||
// To bind values in custom 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].
|
||||
@@ -48,7 +71,7 @@
|
||||
// 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, or nil,
|
||||
// depending on the column type and what whoever wrote the value.
|
||||
// depending on the column type and whoever wrote the value.
|
||||
// [sqlite3.TimeFormat.Decode] may help.
|
||||
//
|
||||
// # Setting PRAGMAs
|
||||
@@ -218,8 +241,9 @@ func (n *connector) Connect(ctx context.Context) (ret driver.Conn, err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
old := c.Conn.SetInterrupt(ctx)
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
if old := c.Conn.SetInterrupt(ctx); old != ctx {
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
}
|
||||
|
||||
if !n.pragmas {
|
||||
err = c.Conn.BusyTimeout(time.Minute)
|
||||
@@ -339,8 +363,9 @@ func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, e
|
||||
c.txReset = `; PRAGMA query_only=` + string(c.readOnly)
|
||||
}
|
||||
|
||||
old := c.Conn.SetInterrupt(ctx)
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
if old := c.Conn.SetInterrupt(ctx); old != ctx {
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
}
|
||||
|
||||
err := c.Conn.Exec(txBegin)
|
||||
if err != nil {
|
||||
@@ -358,13 +383,12 @@ func (c *conn) Commit() error {
|
||||
}
|
||||
|
||||
func (c *conn) Rollback() error {
|
||||
err := c.Conn.Exec(`ROLLBACK` + c.txReset)
|
||||
if errors.Is(err, sqlite3.INTERRUPT) {
|
||||
old := c.Conn.SetInterrupt(context.Background())
|
||||
// ROLLBACK even if interrupted.
|
||||
ctx := context.Background()
|
||||
if old := c.Conn.SetInterrupt(ctx); old != ctx {
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
err = c.Conn.Exec(`ROLLBACK` + c.txReset)
|
||||
}
|
||||
return err
|
||||
return c.Conn.Exec(`ROLLBACK` + c.txReset)
|
||||
}
|
||||
|
||||
func (c *conn) Prepare(query string) (driver.Stmt, error) {
|
||||
@@ -373,8 +397,9 @@ func (c *conn) Prepare(query string) (driver.Stmt, error) {
|
||||
}
|
||||
|
||||
func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
|
||||
old := c.Conn.SetInterrupt(ctx)
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
if old := c.Conn.SetInterrupt(ctx); old != ctx {
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
}
|
||||
|
||||
s, tail, err := c.Conn.Prepare(query)
|
||||
if err != nil {
|
||||
@@ -399,8 +424,9 @@ func (c *conn) ExecContext(ctx context.Context, query string, args []driver.Name
|
||||
return resultRowsAffected(0), nil
|
||||
}
|
||||
|
||||
old := c.Conn.SetInterrupt(ctx)
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
if old := c.Conn.SetInterrupt(ctx); old != ctx {
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
}
|
||||
|
||||
err := c.Conn.Exec(query)
|
||||
if err != nil {
|
||||
@@ -416,6 +442,22 @@ func (c *conn) CheckNamedValue(arg *driver.NamedValue) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: for Litestream use only; may be removed at any time.
|
||||
func (c *conn) FileControlPersistWAL(schema string, mode int) (int, error) {
|
||||
// notest
|
||||
arg := make([]any, 1)
|
||||
if mode >= 0 {
|
||||
arg[0] = mode > 0
|
||||
} else {
|
||||
arg = arg[:0]
|
||||
}
|
||||
res, err := c.Conn.FileControl(schema, sqlite3.FCNTL_PERSIST_WAL, arg...)
|
||||
if res == true {
|
||||
return 1, err
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
type stmt struct {
|
||||
*sqlite3.Stmt
|
||||
tmWrite sqlite3.TimeFormat
|
||||
@@ -463,8 +505,10 @@ func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (drive
|
||||
return nil, err
|
||||
}
|
||||
|
||||
old := s.Stmt.Conn().SetInterrupt(ctx)
|
||||
defer s.Stmt.Conn().SetInterrupt(old)
|
||||
c := s.Stmt.Conn()
|
||||
if old := c.SetInterrupt(ctx); old != ctx {
|
||||
defer c.SetInterrupt(old)
|
||||
}
|
||||
|
||||
err = errors.Join(
|
||||
s.Stmt.Exec(),
|
||||
@@ -473,7 +517,7 @@ func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (drive
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newResult(s.Stmt.Conn()), nil
|
||||
return newResult(c), nil
|
||||
}
|
||||
|
||||
func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
||||
@@ -518,8 +562,8 @@ func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
|
||||
err = s.Stmt.BindTime(id, a, s.tmWrite)
|
||||
case util.JSON:
|
||||
err = s.Stmt.BindJSON(id, a.Value)
|
||||
case util.PointerUnwrap:
|
||||
err = s.Stmt.BindPointer(id, util.UnwrapPointer(a))
|
||||
case util.Pointer:
|
||||
err = s.Stmt.BindPointer(id, a.Value)
|
||||
case nil:
|
||||
err = s.Stmt.BindNull(id)
|
||||
default:
|
||||
@@ -537,7 +581,7 @@ func (s *stmt) CheckNamedValue(arg *driver.NamedValue) error {
|
||||
switch arg.Value.(type) {
|
||||
case bool, int, int64, float64, string, []byte,
|
||||
time.Time, sqlite3.ZeroBlob,
|
||||
util.JSON, util.PointerUnwrap,
|
||||
util.JSON, util.Pointer,
|
||||
nil:
|
||||
return nil
|
||||
default:
|
||||
@@ -576,32 +620,65 @@ func (r resultRowsAffected) RowsAffected() (int64, error) {
|
||||
return int64(r), nil
|
||||
}
|
||||
|
||||
type scantype byte
|
||||
|
||||
const (
|
||||
_ANY scantype = iota
|
||||
_INT
|
||||
_REAL
|
||||
_TEXT
|
||||
_BLOB
|
||||
_NULL
|
||||
_BOOL
|
||||
_TIME
|
||||
_NOT_NULL
|
||||
)
|
||||
|
||||
var (
|
||||
_ [0]struct{} = [scantype(sqlite3.INTEGER) - _INT]struct{}{}
|
||||
_ [0]struct{} = [scantype(sqlite3.FLOAT) - _REAL]struct{}{}
|
||||
_ [0]struct{} = [scantype(sqlite3.TEXT) - _TEXT]struct{}{}
|
||||
_ [0]struct{} = [scantype(sqlite3.BLOB) - _BLOB]struct{}{}
|
||||
_ [0]struct{} = [scantype(sqlite3.NULL) - _NULL]struct{}{}
|
||||
_ [0]struct{} = [_NOT_NULL & (_NOT_NULL - 1)]struct{}{}
|
||||
)
|
||||
|
||||
func scanFromDecl(decl string) scantype {
|
||||
// These types are only used before we have rows,
|
||||
// and otherwise as type hints.
|
||||
// The first few ensure STRICT tables are strictly typed.
|
||||
// The other two are type hints for booleans and time.
|
||||
switch decl {
|
||||
case "INT", "INTEGER":
|
||||
return _INT
|
||||
case "REAL":
|
||||
return _REAL
|
||||
case "TEXT":
|
||||
return _TEXT
|
||||
case "BLOB":
|
||||
return _BLOB
|
||||
case "BOOLEAN":
|
||||
return _BOOL
|
||||
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
|
||||
return _TIME
|
||||
}
|
||||
return _ANY
|
||||
}
|
||||
|
||||
type rows struct {
|
||||
ctx context.Context
|
||||
*stmt
|
||||
names []string
|
||||
types []string
|
||||
nulls []bool
|
||||
scans []scantype
|
||||
dest []driver.Value
|
||||
}
|
||||
|
||||
type scantype byte
|
||||
|
||||
const (
|
||||
_ANY scantype = iota
|
||||
_INT scantype = scantype(sqlite3.INTEGER)
|
||||
_REAL scantype = scantype(sqlite3.FLOAT)
|
||||
_TEXT scantype = scantype(sqlite3.TEXT)
|
||||
_BLOB scantype = scantype(sqlite3.BLOB)
|
||||
_NULL scantype = scantype(sqlite3.NULL)
|
||||
_BOOL scantype = iota
|
||||
_TIME
|
||||
)
|
||||
|
||||
var (
|
||||
// Ensure these interfaces are implemented:
|
||||
_ driver.RowsColumnTypeDatabaseTypeName = &rows{}
|
||||
_ driver.RowsColumnTypeNullable = &rows{}
|
||||
// _ driver.RowsColumnScanner = &rows{}
|
||||
)
|
||||
|
||||
func (r *rows) Close() error {
|
||||
@@ -622,79 +699,63 @@ func (r *rows) Columns() []string {
|
||||
return r.names
|
||||
}
|
||||
|
||||
func (r *rows) scanType(index int) scantype {
|
||||
if r.scans == nil {
|
||||
count := len(r.names)
|
||||
scans := make([]scantype, count)
|
||||
for i := range scans {
|
||||
scans[i] = scanFromDecl(strings.ToUpper(r.Stmt.ColumnDeclType(i)))
|
||||
}
|
||||
r.scans = scans
|
||||
}
|
||||
return r.scans[index] &^ _NOT_NULL
|
||||
}
|
||||
|
||||
func (r *rows) loadColumnMetadata() {
|
||||
if r.nulls == nil {
|
||||
count := r.Stmt.ColumnCount()
|
||||
nulls := make([]bool, count)
|
||||
if r.types == nil {
|
||||
c := r.Stmt.Conn()
|
||||
count := len(r.names)
|
||||
types := make([]string, count)
|
||||
scans := make([]scantype, count)
|
||||
for i := range nulls {
|
||||
for i := range types {
|
||||
var notnull bool
|
||||
if col := r.Stmt.ColumnOriginName(i); col != "" {
|
||||
types[i], _, nulls[i], _, _, _ = r.Stmt.Conn().TableColumnMetadata(
|
||||
types[i], _, notnull, _, _, _ = c.TableColumnMetadata(
|
||||
r.Stmt.ColumnDatabaseName(i),
|
||||
r.Stmt.ColumnTableName(i),
|
||||
col)
|
||||
types[i] = strings.ToUpper(types[i])
|
||||
// These types are only used before we have rows,
|
||||
// and otherwise as type hints.
|
||||
// The first few ensure STRICT tables are strictly typed.
|
||||
// The other two are type hints for booleans and time.
|
||||
switch types[i] {
|
||||
case "INT", "INTEGER":
|
||||
scans[i] = _INT
|
||||
case "REAL":
|
||||
scans[i] = _REAL
|
||||
case "TEXT":
|
||||
scans[i] = _TEXT
|
||||
case "BLOB":
|
||||
scans[i] = _BLOB
|
||||
case "BOOLEAN":
|
||||
scans[i] = _BOOL
|
||||
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
|
||||
scans[i] = _TIME
|
||||
scans[i] = scanFromDecl(types[i])
|
||||
if notnull {
|
||||
scans[i] |= _NOT_NULL
|
||||
}
|
||||
}
|
||||
}
|
||||
r.nulls = nulls
|
||||
r.types = types
|
||||
r.scans = scans
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rows) declType(index int) string {
|
||||
if r.types == nil {
|
||||
count := r.Stmt.ColumnCount()
|
||||
types := make([]string, count)
|
||||
for i := range types {
|
||||
types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
|
||||
}
|
||||
r.types = types
|
||||
}
|
||||
return r.types[index]
|
||||
}
|
||||
|
||||
func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
|
||||
r.loadColumnMetadata()
|
||||
decltype := r.types[index]
|
||||
if len := len(decltype); len > 0 && decltype[len-1] == ')' {
|
||||
if i := strings.LastIndexByte(decltype, '('); i >= 0 {
|
||||
decltype = decltype[:i]
|
||||
decl := r.types[index]
|
||||
if len := len(decl); len > 0 && decl[len-1] == ')' {
|
||||
if i := strings.LastIndexByte(decl, '('); i >= 0 {
|
||||
decl = decl[:i]
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(decltype)
|
||||
return strings.TrimSpace(decl)
|
||||
}
|
||||
|
||||
func (r *rows) ColumnTypeNullable(index int) (nullable, ok bool) {
|
||||
r.loadColumnMetadata()
|
||||
if r.nulls[index] {
|
||||
return false, true
|
||||
}
|
||||
return true, false
|
||||
nullable = r.scans[index]&^_NOT_NULL == 0
|
||||
return nullable, !nullable
|
||||
}
|
||||
|
||||
func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
|
||||
r.loadColumnMetadata()
|
||||
scan := r.scans[index]
|
||||
scan := r.scans[index] &^ _NOT_NULL
|
||||
|
||||
if r.Stmt.Busy() {
|
||||
// SQLite is dynamically typed and we now have a row.
|
||||
@@ -706,7 +767,7 @@ func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
|
||||
switch {
|
||||
case scan == _TIME && val != _BLOB && val != _NULL:
|
||||
t := r.Stmt.ColumnTime(index, r.tmRead)
|
||||
useValType = t == time.Time{}
|
||||
useValType = t.IsZero()
|
||||
case scan == _BOOL && val == _INT:
|
||||
i := r.Stmt.ColumnInt64(index)
|
||||
useValType = i != 0 && i != 1
|
||||
@@ -737,8 +798,11 @@ func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
|
||||
}
|
||||
|
||||
func (r *rows) Next(dest []driver.Value) error {
|
||||
old := r.Stmt.Conn().SetInterrupt(r.ctx)
|
||||
defer r.Stmt.Conn().SetInterrupt(old)
|
||||
r.dest = nil
|
||||
c := r.Stmt.Conn()
|
||||
if old := c.SetInterrupt(r.ctx); old != r.ctx {
|
||||
defer c.SetInterrupt(old)
|
||||
}
|
||||
|
||||
if !r.Stmt.Step() {
|
||||
if err := r.Stmt.Err(); err != nil {
|
||||
@@ -748,36 +812,69 @@ func (r *rows) Next(dest []driver.Value) error {
|
||||
}
|
||||
|
||||
data := unsafe.Slice((*any)(unsafe.SliceData(dest)), len(dest))
|
||||
err := r.Stmt.Columns(data...)
|
||||
if err := r.Stmt.ColumnsRaw(data...); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range dest {
|
||||
if t, ok := r.decodeTime(i, dest[i]); ok {
|
||||
dest[i] = t
|
||||
scan := r.scanType(i)
|
||||
if v, ok := dest[i].([]byte); ok {
|
||||
if len(v) == cap(v) { // a BLOB
|
||||
continue
|
||||
}
|
||||
if scan != _TEXT {
|
||||
switch r.tmWrite {
|
||||
case "", time.RFC3339, time.RFC3339Nano:
|
||||
t, ok := maybeTime(v)
|
||||
if ok {
|
||||
dest[i] = t
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
dest[i] = string(v)
|
||||
}
|
||||
switch scan {
|
||||
case _TIME:
|
||||
t, err := r.tmRead.Decode(dest[i])
|
||||
if err == nil {
|
||||
dest[i] = t
|
||||
}
|
||||
case _BOOL:
|
||||
switch dest[i] {
|
||||
case int64(0):
|
||||
dest[i] = false
|
||||
case int64(1):
|
||||
dest[i] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
r.dest = dest
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rows) ScanColumn(dest any, index int) (err error) {
|
||||
// notest // Go 1.26
|
||||
var tm *time.Time
|
||||
var ok *bool
|
||||
switch d := dest.(type) {
|
||||
case *time.Time:
|
||||
tm = d
|
||||
case *sql.NullTime:
|
||||
tm = &d.Time
|
||||
ok = &d.Valid
|
||||
case *sql.Null[time.Time]:
|
||||
tm = &d.V
|
||||
ok = &d.Valid
|
||||
default:
|
||||
return driver.ErrSkip
|
||||
}
|
||||
value := r.dest[index]
|
||||
*tm, err = r.tmRead.Decode(value)
|
||||
if ok != nil {
|
||||
*ok = err == nil
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rows) decodeTime(i int, v any) (_ time.Time, ok bool) {
|
||||
switch v := v.(type) {
|
||||
case int64, float64:
|
||||
// could be a time value
|
||||
case string:
|
||||
if r.tmWrite != "" && r.tmWrite != time.RFC3339 && r.tmWrite != time.RFC3339Nano {
|
||||
break
|
||||
}
|
||||
t, ok := maybeTime(v)
|
||||
if ok {
|
||||
return t, true
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
switch r.declType(i) {
|
||||
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
|
||||
// could be a time value
|
||||
default:
|
||||
return
|
||||
}
|
||||
t, err := r.tmRead.Decode(v)
|
||||
return t, err == nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"math"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -33,7 +34,7 @@ func Test_Open_error(t *testing.T) {
|
||||
func Test_Open_dir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sql.Open("sqlite3", ".")
|
||||
db, err := Open(".")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -43,18 +44,18 @@ func Test_Open_dir(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
}
|
||||
if !errors.Is(err, sqlite3.CANTOPEN) {
|
||||
t.Errorf("got %v, want sqlite3.CANTOPEN", err)
|
||||
if !errors.Is(err, sqlite3.CANTOPEN_ISDIR) {
|
||||
t.Errorf("got %v, want sqlite3.CANTOPEN_ISDIR", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Open_pragma(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
dsn := memdb.TestDB(t, url.Values{
|
||||
"_pragma": {"busy_timeout(1000)"},
|
||||
})
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -72,11 +73,11 @@ func Test_Open_pragma(t *testing.T) {
|
||||
|
||||
func Test_Open_pragma_invalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
dsn := memdb.TestDB(t, url.Values{
|
||||
"_pragma": {"busy_timeout 1000"},
|
||||
})
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -100,12 +101,12 @@ func Test_Open_pragma_invalid(t *testing.T) {
|
||||
|
||||
func Test_Open_txLock(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
dsn := memdb.TestDB(t, url.Values{
|
||||
"_txlock": {"exclusive"},
|
||||
"_pragma": {"busy_timeout(1000)"},
|
||||
})
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -136,11 +137,11 @@ func Test_Open_txLock(t *testing.T) {
|
||||
|
||||
func Test_Open_txLock_invalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
dsn := memdb.TestDB(t, url.Values{
|
||||
"_txlock": {"xclusive"},
|
||||
})
|
||||
|
||||
_, err := sql.Open("sqlite3", tmp+"_txlock=xclusive")
|
||||
_, err := Open(dsn)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
}
|
||||
@@ -151,31 +152,28 @@ func Test_Open_txLock_invalid(t *testing.T) {
|
||||
|
||||
func Test_BeginTx(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
dsn := memdb.TestDB(t, url.Values{
|
||||
"_txlock": {"exclusive"},
|
||||
"_pragma": {"busy_timeout(0)"},
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||
_, err = db.BeginTx(t.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||
if err.Error() != string(util.IsolationErr) {
|
||||
t.Error("want isolationErr")
|
||||
}
|
||||
|
||||
tx1, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
||||
tx1, err := db.BeginTx(t.Context(), &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tx2, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
||||
tx2, err := db.BeginTx(t.Context(), &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -199,11 +197,69 @@ func Test_BeginTx(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_nested_context(t *testing.T) {
|
||||
t.Parallel()
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
outer, err := tx.Query(`SELECT value FROM generate_series(0)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer outer.Close()
|
||||
|
||||
want := func(rows *sql.Rows, want int) {
|
||||
t.Helper()
|
||||
|
||||
var got int
|
||||
rows.Next()
|
||||
if err := rows.Scan(&got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
want(outer, 0)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
inner, err := tx.QueryContext(ctx, `SELECT value FROM generate_series(0)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer inner.Close()
|
||||
|
||||
want(inner, 0)
|
||||
cancel()
|
||||
|
||||
var terr interface{ Temporary() bool }
|
||||
if inner.Next() || !errors.Is(inner.Err(), context.Canceled) &&
|
||||
(!errors.As(inner.Err(), &terr) || !terr.Temporary()) {
|
||||
t.Fatalf("got %v, want cancellation", inner.Err())
|
||||
}
|
||||
|
||||
want(outer, 1)
|
||||
}
|
||||
|
||||
func Test_Prepare(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -242,24 +298,21 @@ func Test_Prepare(t *testing.T) {
|
||||
|
||||
func Test_QueryRow_named(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
conn, err := db.Conn(ctx)
|
||||
conn, err := db.Conn(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
stmt, err := conn.PrepareContext(ctx, `SELECT ?, ?5, :AAA, @AAA, $AAA`)
|
||||
stmt, err := conn.PrepareContext(t.Context(), `SELECT ?, ?5, :AAA, @AAA, $AAA`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -295,9 +348,9 @@ func Test_QueryRow_named(t *testing.T) {
|
||||
|
||||
func Test_QueryRow_blob_null(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -332,11 +385,11 @@ func Test_time(t *testing.T) {
|
||||
|
||||
for _, fmt := range []string{"auto", "sqlite", "rfc3339", time.ANSIC} {
|
||||
t.Run(fmt, func(t *testing.T) {
|
||||
tmp := memdb.TestDB(t, url.Values{
|
||||
dsn := memdb.TestDB(t, url.Values{
|
||||
"_timefmt": {fmt},
|
||||
})
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -379,9 +432,9 @@ func Test_ColumnType_ScanType(t *testing.T) {
|
||||
)
|
||||
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -467,3 +520,58 @@ func Test_ColumnType_ScanType(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_rows_ScanColumn(t *testing.T) {
|
||||
t.Parallel()
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var tm time.Time
|
||||
err = db.QueryRow(`SELECT NULL`).Scan(&tm)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
// Go 1.26
|
||||
err = db.QueryRow(`SELECT datetime()`).Scan(&tm)
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "sql: Scan error") {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var nt sql.NullTime
|
||||
err = db.QueryRow(`SELECT NULL`).Scan(&nt)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// Go 1.26
|
||||
err = db.QueryRow(`SELECT datetime()`).Scan(&nt)
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "sql: Scan error") {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_loop(b *testing.B) {
|
||||
db, err := Open(":memory:")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var version string
|
||||
err = db.QueryRow(`SELECT sqlite_version();`).Scan(&version)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
_, err := db.ExecContext(b.Context(),
|
||||
`WITH RECURSIVE c(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM c WHERE x < 1000000) SELECT x FROM c;`)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk
|
||||
|
||||
package driver_test
|
||||
|
||||
// Adapted from: https://go.dev/doc/tutorial/database-access
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
@@ -27,7 +23,7 @@ func Example_customTime() {
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE data (
|
||||
id INTEGER PRIMARY KEY,
|
||||
date_time TEXT
|
||||
date_time ANY
|
||||
) STRICT;
|
||||
`)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package driver
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"bytes"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Convert a string in [time.RFC3339Nano] format into a [time.Time]
|
||||
// if it roundtrips back to the same string.
|
||||
// This way times can be persisted to, and recovered from, the database,
|
||||
// but if a string is needed, [database/sql] will recover the same string.
|
||||
func maybeTime(text string) (_ time.Time, _ bool) {
|
||||
func maybeTime(text []byte) (_ time.Time, _ bool) {
|
||||
// Weed out (some) values that can't possibly be
|
||||
// [time.RFC3339Nano] timestamps.
|
||||
if len(text) < len("2006-01-02T15:04:05Z") {
|
||||
@@ -21,8 +24,8 @@ func maybeTime(text string) (_ time.Time, _ bool) {
|
||||
|
||||
// Slow path.
|
||||
var buf [len(time.RFC3339Nano)]byte
|
||||
date, err := time.Parse(time.RFC3339Nano, text)
|
||||
if err == nil && text == string(date.AppendFormat(buf[:0], time.RFC3339Nano)) {
|
||||
date, err := time.Parse(time.RFC3339Nano, string(text))
|
||||
if err == nil && bytes.Equal(text, date.AppendFormat(buf[:0], time.RFC3339Nano)) {
|
||||
return date, true
|
||||
}
|
||||
return
|
||||
|
||||
@@ -22,7 +22,7 @@ func Fuzz_stringOrTime_1(f *testing.F) {
|
||||
f.Add("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
|
||||
|
||||
f.Fuzz(func(t *testing.T, str string) {
|
||||
v, ok := maybeTime(str)
|
||||
v, ok := maybeTime([]byte(str))
|
||||
if ok {
|
||||
// Make sure times round-trip to the same string:
|
||||
// https://pkg.go.dev/database/sql#Rows.Scan
|
||||
@@ -51,7 +51,7 @@ func Fuzz_stringOrTime_2(f *testing.F) {
|
||||
f.Add(int64(-763421161058), int64(222_222_222)) // twosday, year 22222BC
|
||||
|
||||
checkTime := func(t testing.TB, date time.Time) {
|
||||
v, ok := maybeTime(date.Format(time.RFC3339Nano))
|
||||
v, ok := maybeTime(date.AppendFormat(nil, time.RFC3339Nano))
|
||||
if ok {
|
||||
// Make sure times round-trip to the same time:
|
||||
if !v.Equal(date) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"slices"
|
||||
"testing"
|
||||
@@ -56,7 +55,7 @@ func Fuzz_notWhitespace(f *testing.F) {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
c, err := db.Conn(context.Background())
|
||||
c, err := db.Conn(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Embeddable Wasm build of SQLite
|
||||
|
||||
This folder includes an embeddable Wasm build of SQLite 3.49.1 for use with
|
||||
This folder includes an embeddable Wasm build of SQLite 3.50.4 for use with
|
||||
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
|
||||
|
||||
The following optional features are compiled in:
|
||||
|
||||
Binary file not shown.
@@ -53,7 +53,7 @@ func Test_bcw2(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != "3.50.0" {
|
||||
if version != "3.51.0" {
|
||||
t.Error(version)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,18 @@ ROOT=../../
|
||||
BINARYEN="$ROOT/tools/binaryen/bin"
|
||||
WASI_SDK="$ROOT/tools/wasi-sdk/bin"
|
||||
|
||||
trap 'rm -rf build/ sqlite/ bcw2.tmp' EXIT
|
||||
trap 'rm -rf sqlite/ build/ bcw2.tmp' EXIT
|
||||
|
||||
mkdir -p sqlite/
|
||||
mkdir -p build/ext/
|
||||
cp "$ROOT"/sqlite3/*.[ch] build/
|
||||
cp "$ROOT"/sqlite3/*.patch build/
|
||||
cd sqlite/
|
||||
|
||||
# https://sqlite.org/src/info/c09656c62155a6e8
|
||||
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=c09656c6 | tar xz
|
||||
# https://sqlite.org/src/info/0e862bc9ed7aa9ae
|
||||
curl -#L https://github.com/sqlite/sqlite/archive/0b99392.tar.gz | tar xz --strip-components=1
|
||||
# curl -#L https://sqlite.org/src/tarball/sqlite.tar.gz?r=0e862bc9ed | tar xz --strip-components=1
|
||||
|
||||
cd sqlite
|
||||
cat ../repro.patch | patch -p0 --no-backup-if-mismatch
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c "OPTS=-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT -DSQLITE_ENABLE_ORDERED_SET_AGGREGATES"
|
||||
else
|
||||
@@ -44,13 +45,14 @@ 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" \
|
||||
-o bcw2.wasm build/main.c \
|
||||
-I"$ROOT/sqlite3/libc" -I"build" \
|
||||
-mexec-model=reactor \
|
||||
-msimd128 -mmutable-globals -mmultivalue \
|
||||
-mbulk-memory -mreference-types \
|
||||
-mnontrapping-fptoint -msign-ext \
|
||||
-fno-stack-protector -fno-stack-clash-protection \
|
||||
-mmutable-globals -mnontrapping-fptoint \
|
||||
-msimd128 -mbulk-memory -msign-ext \
|
||||
-mreference-types -mmultivalue \
|
||||
-mno-extended-const \
|
||||
-fno-stack-protector \
|
||||
-Wl,--stack-first \
|
||||
-Wl,--import-undefined \
|
||||
-Wl,--initial-memory=327680 \
|
||||
@@ -60,8 +62,9 @@ cd ~-
|
||||
$(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
|
||||
"$BINARYEN/wasm-opt" -g bcw2.tmp -o bcw2.wasm \
|
||||
--low-memory-unused --gufa --generate-global-effects --converge -O3 \
|
||||
--enable-mutable-globals --enable-nontrapping-float-to-int \
|
||||
--enable-simd --enable-bulk-memory --enable-sign-ext \
|
||||
--enable-reference-types --enable-multivalue \
|
||||
--strip --strip-producers
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
module github.com/ncruces/go-sqlite3/embed/bcw2
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.0
|
||||
|
||||
require github.com/ncruces/go-sqlite3 v0.23.1
|
||||
require github.com/ncruces/go-sqlite3 v0.30.1
|
||||
|
||||
require (
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/ncruces/sort v0.1.5 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
github.com/ncruces/sort v0.1.6 // indirect
|
||||
github.com/tetratelabs/wazero v1.10.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
github.com/ncruces/go-sqlite3 v0.23.1 h1:zGAd76q+Tr18z/xKGatUlzBQdjR3J+rexfANUcjAgkY=
|
||||
github.com/ncruces/go-sqlite3 v0.23.1/go.mod h1:Xg3FyAZl25HcBSFmcbymdfoTqD7jRnBUmv1jSrbIjdE=
|
||||
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
|
||||
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
|
||||
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.5 h1:fiFWXXAqKI8QckPf/6hu/bGFwcEPrirIOFaJqWujs4k=
|
||||
github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
github.com/ncruces/sort v0.1.6 h1:TrsJfGRH1AoWoaeB4/+gCohot9+cA6u/INaH5agIhNk=
|
||||
github.com/ncruces/sort v0.1.6/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
|
||||
github.com/tetratelabs/wazero v1.10.0 h1:CXP3zneLDl6J4Zy8N/J+d5JsWKfrjE6GtvVK1fpnDlk=
|
||||
github.com/tetratelabs/wazero v1.10.0/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# https://sqlite.org/src/vpatch?from=67809715977a5bad&to=3f57584710d61174
|
||||
--- tool/mkpragmatab.tcl
|
||||
+++ tool/mkpragmatab.tcl
|
||||
@@ -526,14 +526,17 @@
|
||||
puts $fd [format {#define PragFlg_%-10s 0x%02x /* %s */} \
|
||||
$f $fv $flagMeaning($f)]
|
||||
set fv [expr {$fv*2}]
|
||||
}
|
||||
|
||||
-# Sort the column lists so that longer column lists occur first
|
||||
+# Sort the column lists so that longer column lists occur first.
|
||||
+# In the event of a tie, sort column lists lexicographically.
|
||||
#
|
||||
proc colscmp {a b} {
|
||||
- return [expr {[llength $b] - [llength $a]}]
|
||||
+ set rc [expr {[llength $b] - [llength $a]}]
|
||||
+ if {$rc} {return $rc}
|
||||
+ return [string compare $a $b]
|
||||
}
|
||||
set cols_list [lsort -command colscmp $cols_list]
|
||||
|
||||
# Generate the array of column names used by pragmas that act like
|
||||
# queries.
|
||||
@@ -12,12 +12,13 @@ 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" \
|
||||
-I"$ROOT/sqlite3/libc" -I"$ROOT/sqlite3" \
|
||||
-mexec-model=reactor \
|
||||
-msimd128 -mmutable-globals -mmultivalue \
|
||||
-mbulk-memory -mreference-types \
|
||||
-mnontrapping-fptoint -msign-ext \
|
||||
-fno-stack-protector -fno-stack-clash-protection \
|
||||
-mmutable-globals -mnontrapping-fptoint \
|
||||
-msimd128 -mbulk-memory -msign-ext \
|
||||
-mreference-types -mmultivalue \
|
||||
-mno-extended-const \
|
||||
-fno-stack-protector \
|
||||
-Wl,--stack-first \
|
||||
-Wl,--import-undefined \
|
||||
-Wl,--initial-memory=327680 \
|
||||
@@ -26,8 +27,9 @@ trap 'rm -f sqlite3.tmp' EXIT
|
||||
$(awk '{print "-Wl,--export="$0}' exports.txt)
|
||||
|
||||
"$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 \
|
||||
--enable-simd --enable-mutable-globals --enable-multivalue \
|
||||
--enable-bulk-memory --enable-reference-types \
|
||||
--enable-nontrapping-float-to-int --enable-sign-ext
|
||||
"$BINARYEN/wasm-opt" -g sqlite3.tmp -o sqlite3.wasm \
|
||||
--low-memory-unused --gufa --generate-global-effects --converge -O3 \
|
||||
--enable-mutable-globals --enable-nontrapping-float-to-int \
|
||||
--enable-simd --enable-bulk-memory --enable-sign-ext \
|
||||
--enable-reference-types --enable-multivalue \
|
||||
--strip --strip-producers
|
||||
@@ -59,13 +59,14 @@ sqlite3_db_filename
|
||||
sqlite3_db_name
|
||||
sqlite3_db_readonly
|
||||
sqlite3_db_release_memory
|
||||
sqlite3_db_status
|
||||
sqlite3_db_status64
|
||||
sqlite3_declare_vtab
|
||||
sqlite3_errcode
|
||||
sqlite3_errmsg
|
||||
sqlite3_error_offset
|
||||
sqlite3_errstr
|
||||
sqlite3_exec
|
||||
sqlite3_exec_go
|
||||
sqlite3_expanded_sql
|
||||
sqlite3_file_control
|
||||
sqlite3_filename_database
|
||||
@@ -97,6 +98,7 @@ sqlite3_result_error_toobig
|
||||
sqlite3_result_int64
|
||||
sqlite3_result_null
|
||||
sqlite3_result_pointer_go
|
||||
sqlite3_result_subtype
|
||||
sqlite3_result_text_go
|
||||
sqlite3_result_value
|
||||
sqlite3_result_zeroblob64
|
||||
@@ -125,6 +127,7 @@ sqlite3_value_int64
|
||||
sqlite3_value_nochange
|
||||
sqlite3_value_numeric_type
|
||||
sqlite3_value_pointer_go
|
||||
sqlite3_value_subtype
|
||||
sqlite3_value_text
|
||||
sqlite3_value_type
|
||||
sqlite3_vtab_collation
|
||||
|
||||
@@ -17,5 +17,7 @@ import (
|
||||
var binary string
|
||||
|
||||
func init() {
|
||||
sqlite3.Binary = unsafe.Slice(unsafe.StringData(binary), len(binary))
|
||||
if sqlite3.Binary == nil {
|
||||
sqlite3.Binary = unsafe.Slice(unsafe.StringData(binary), len(binary))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func Test_init(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != "3.49.1" {
|
||||
if version != "3.51.0" {
|
||||
t.Error(version)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
54
error.go
54
error.go
@@ -2,7 +2,6 @@ package sqlite3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
@@ -12,7 +11,7 @@ import (
|
||||
//
|
||||
// https://sqlite.org/c3ref/errcode.html
|
||||
type Error struct {
|
||||
str string
|
||||
sys error
|
||||
msg string
|
||||
sql string
|
||||
code res_t
|
||||
@@ -29,28 +28,34 @@ func (e *Error) Code() ErrorCode {
|
||||
//
|
||||
// https://sqlite.org/rescode.html
|
||||
func (e *Error) ExtendedCode() ExtendedErrorCode {
|
||||
return ExtendedErrorCode(e.code)
|
||||
return xErrorCode(e.code)
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *Error) Error() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("sqlite3: ")
|
||||
|
||||
if e.str != "" {
|
||||
b.WriteString(e.str)
|
||||
} else {
|
||||
b.WriteString(strconv.Itoa(int(e.code)))
|
||||
}
|
||||
b.WriteString(util.ErrorCodeString(e.code))
|
||||
|
||||
if e.msg != "" {
|
||||
b.WriteString(": ")
|
||||
b.WriteString(e.msg)
|
||||
}
|
||||
if e.sys != nil {
|
||||
b.WriteString(": ")
|
||||
b.WriteString(e.sys.Error())
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying operating system error
|
||||
// that caused the I/O error or failure to open a file.
|
||||
//
|
||||
// https://sqlite.org/c3ref/system_errno.html
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.sys
|
||||
}
|
||||
|
||||
// Is tests whether this error matches a given [ErrorCode] or [ExtendedErrorCode].
|
||||
//
|
||||
// It makes it possible to do:
|
||||
@@ -83,7 +88,7 @@ func (e *Error) As(err any) bool {
|
||||
|
||||
// Temporary returns true for [BUSY] errors.
|
||||
func (e *Error) Temporary() bool {
|
||||
return e.Code() == BUSY
|
||||
return e.Code() == BUSY || e.Code() == INTERRUPT
|
||||
}
|
||||
|
||||
// Timeout returns true for [BUSY_TIMEOUT] errors.
|
||||
@@ -98,22 +103,31 @@ func (e *Error) SQL() string {
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e ErrorCode) Error() string {
|
||||
return util.ErrorCodeString(uint32(e))
|
||||
return util.ErrorCodeString(e)
|
||||
}
|
||||
|
||||
// As converts this error to an [ExtendedErrorCode].
|
||||
func (e ErrorCode) As(err any) bool {
|
||||
c, ok := err.(*xErrorCode)
|
||||
if ok {
|
||||
*c = xErrorCode(e)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// Temporary returns true for [BUSY] errors.
|
||||
func (e ErrorCode) Temporary() bool {
|
||||
return e == BUSY
|
||||
return e == BUSY || e == INTERRUPT
|
||||
}
|
||||
|
||||
// ExtendedCode returns the extended error code for this error.
|
||||
func (e ErrorCode) ExtendedCode() ExtendedErrorCode {
|
||||
return ExtendedErrorCode(e)
|
||||
return xErrorCode(e)
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e ExtendedErrorCode) Error() string {
|
||||
return util.ErrorCodeString(uint32(e))
|
||||
return util.ErrorCodeString(e)
|
||||
}
|
||||
|
||||
// Is tests whether this error matches a given [ErrorCode].
|
||||
@@ -133,7 +147,7 @@ func (e ExtendedErrorCode) As(err any) bool {
|
||||
|
||||
// Temporary returns true for [BUSY] errors.
|
||||
func (e ExtendedErrorCode) Temporary() bool {
|
||||
return ErrorCode(e) == BUSY
|
||||
return ErrorCode(e) == BUSY || ErrorCode(e) == INTERRUPT
|
||||
}
|
||||
|
||||
// Timeout returns true for [BUSY_TIMEOUT] errors.
|
||||
@@ -158,14 +172,10 @@ func errorCode(err error, def ErrorCode) (msg string, code res_t) {
|
||||
return code.msg, res_t(code.code)
|
||||
}
|
||||
|
||||
var ecode ErrorCode
|
||||
var xcode xErrorCode
|
||||
switch {
|
||||
case errors.As(err, &xcode):
|
||||
if errors.As(err, &xcode) {
|
||||
code = res_t(xcode)
|
||||
case errors.As(err, &ecode):
|
||||
code = res_t(ecode)
|
||||
default:
|
||||
} else {
|
||||
code = res_t(def)
|
||||
}
|
||||
return err.Error(), code
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestError(t *testing.T) {
|
||||
if !errors.Is(err, xErrorCode(0x8080)) {
|
||||
t.Errorf("want true")
|
||||
}
|
||||
if s := err.Error(); s != "sqlite3: 32896" {
|
||||
if s := err.Error(); s != "sqlite3: unknown error" {
|
||||
t.Errorf("got %q", s)
|
||||
}
|
||||
if ok := errors.As(err.ExtendedCode(), &ecode); !ok || ecode != ErrorCode(0x80) {
|
||||
@@ -83,7 +83,7 @@ func TestError_Temporary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
{
|
||||
err := ExtendedErrorCode(tt.code)
|
||||
err := xErrorCode(tt.code)
|
||||
if got := err.Temporary(); got != tt.want {
|
||||
t.Errorf("ExtendedErrorCode.Temporary(%d) = %v, want %v", tt.code, got, tt.want)
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func TestError_Timeout(t *testing.T) {
|
||||
}
|
||||
}
|
||||
{
|
||||
err := ExtendedErrorCode(tt.code)
|
||||
err := xErrorCode(tt.code)
|
||||
if got := err.Timeout(); got != tt.want {
|
||||
t.Errorf("Error.Timeout(%d) = %v, want %v", tt.code, got, tt.want)
|
||||
}
|
||||
@@ -156,12 +156,12 @@ func Test_ExtendedErrorCode_Error(t *testing.T) {
|
||||
defer db.Close()
|
||||
|
||||
// Test all extended error codes.
|
||||
for i := 0; i == int(ExtendedErrorCode(i)); i++ {
|
||||
for i := 0; i == int(xErrorCode(i)); i++ {
|
||||
want := "sqlite3: "
|
||||
ptr := ptr_t(db.call("sqlite3_errstr", stk_t(i)))
|
||||
want += util.ReadString(db.mod, ptr, _MAX_NAME)
|
||||
|
||||
got := ExtendedErrorCode(i).Error()
|
||||
got := xErrorCode(i).Error()
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q, with %d", got, want, i)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ you can load into your database connections.
|
||||
- [`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.
|
||||
provides [statistics](https://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)
|
||||
@@ -38,11 +38,11 @@ you can load into your database connections.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
|
||||
maps multidimensional data to one dimension.
|
||||
|
||||
### Pakages
|
||||
### Packages
|
||||
|
||||
These packages may also be useful to work with SQLite:
|
||||
|
||||
- [`github.com/ncruces/decimal`](https://pkg.go.dev/github.com/ncruces/decimal)
|
||||
decimal arithmetic.
|
||||
- [`github.com/ncruces/julianday`](https://pkg.go.dev/github.com/ncruces/julianday)
|
||||
Julian day math.
|
||||
Julian day math.
|
||||
|
||||
@@ -59,7 +59,8 @@ func (c *cursor) Next() error {
|
||||
}
|
||||
|
||||
func (c *cursor) RowID() (int64, error) {
|
||||
return int64(c.rowID), nil
|
||||
// One-based RowID for consistency with carray and other tables.
|
||||
return int64(c.rowID) + 1, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
|
||||
|
||||
@@ -88,9 +88,9 @@ func Example() {
|
||||
|
||||
func Test_cursor_Column(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, array.Register)
|
||||
db, err := driver.Open(dsn, array.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package blobio_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
@@ -34,7 +35,8 @@ func Example() {
|
||||
const message = "Hello BLOB!"
|
||||
|
||||
// Create the BLOB.
|
||||
r, err := db.Exec(`INSERT INTO test VALUES (?)`, sqlite3.ZeroBlob(len(message)))
|
||||
r, err := db.Exec(`INSERT INTO test VALUES (:data)`,
|
||||
sql.Named("data", sqlite3.ZeroBlob(len(message))))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -45,15 +47,19 @@ func Example() {
|
||||
}
|
||||
|
||||
// Write the BLOB.
|
||||
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', ?, 0, ?)`,
|
||||
id, message)
|
||||
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', :rowid, :offset, :message)`,
|
||||
sql.Named("rowid", id),
|
||||
sql.Named("offset", 0),
|
||||
sql.Named("message", message))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Read the BLOB.
|
||||
_, err = db.Exec(`SELECT readblob('main', 'test', 'col', ?, 0, ?)`,
|
||||
id, sqlite3.Pointer(os.Stdout))
|
||||
_, err = db.Exec(`SELECT readblob('main', 'test', 'col', :rowid, :offset, :writer)`,
|
||||
sql.Named("rowid", id),
|
||||
sql.Named("offset", 0),
|
||||
sql.Named("writer", sqlite3.Pointer(os.Stdout)))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -64,7 +70,7 @@ func Example() {
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(blobio.Register)
|
||||
sqlite3.AutoExtension(array.Register)
|
||||
m.Run()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func Test_readblob(t *testing.T) {
|
||||
@@ -138,18 +144,16 @@ func Test_readblob(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnText(0)
|
||||
if got != tt.want1 {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnText(0); got != tt.want1 {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnText(0)
|
||||
if got != tt.want2 {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnText(0); got != tt.want2 {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
|
||||
err = stmt.Err()
|
||||
|
||||
@@ -268,13 +268,13 @@ func (b *bloom) Open() (sqlite3.VTabCursor, error) {
|
||||
|
||||
type cursor struct {
|
||||
*bloom
|
||||
arg *sqlite3.Value
|
||||
arg sqlite3.Value
|
||||
eof bool
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
c.eof = false
|
||||
c.arg = &arg[0]
|
||||
c.arg = arg[0]
|
||||
blob := arg[0].RawBlob()
|
||||
|
||||
f, err := c.db.OpenBlob(c.schema, c.storage, "data", 1, false)
|
||||
@@ -312,7 +312,7 @@ func (c *cursor) Column(ctx sqlite3.Context, n int) error {
|
||||
case 0:
|
||||
ctx.ResultBool(true)
|
||||
case 1:
|
||||
ctx.ResultValue(*c.arg)
|
||||
ctx.ResultValue(c.arg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(bloom.Register)
|
||||
m.Run()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
|
||||
@@ -154,6 +154,7 @@ func (c *closure) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
|
||||
idx.IdxFlags = sqlite3.INDEX_SCAN_HEX
|
||||
idx.EstimatedCost = cost
|
||||
idx.IdxNum = plan
|
||||
return nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(closure.Register)
|
||||
m.Run()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func Example() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package csv_test
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
@@ -56,7 +57,7 @@ func Example() {
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(csv.Register)
|
||||
m.Run()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
@@ -146,20 +147,21 @@ func TestAffinity(t *testing.T) {
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnText(0); got != "1" {
|
||||
t.Errorf("got %q want 1", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else 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() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else 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)
|
||||
}
|
||||
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnText(0); got != "e" {
|
||||
t.Errorf("got %q want e", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ func Register(db *sqlite3.Conn) error {
|
||||
return RegisterFS(db, nil)
|
||||
}
|
||||
|
||||
// Register registers SQL functions readfile, lsmode,
|
||||
// RegisterFS 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) error {
|
||||
@@ -42,7 +42,7 @@ func lsmode(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultText(fs.FileMode(arg[0].Int()).String())
|
||||
}
|
||||
|
||||
func readfile(fsys fs.FS) func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
func readfile(fsys fs.FS) sqlite3.ScalarFunction {
|
||||
return func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
var err error
|
||||
var data []byte
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
|
||||
func Test_lsmode(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, fileio.Register)
|
||||
db, err := driver.Open(dsn, fileio.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -53,9 +53,9 @@ func Test_readfile(t *testing.T) {
|
||||
|
||||
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
|
||||
t.Run("", func(t *testing.T) {
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, func(c *sqlite3.Conn) error {
|
||||
db, err := driver.Open(dsn, func(c *sqlite3.Conn) error {
|
||||
fileio.RegisterFS(c, fsys)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -63,12 +63,12 @@ func (d fsdir) Open() (sqlite3.VTabCursor, error) {
|
||||
|
||||
type cursor struct {
|
||||
fsdir
|
||||
base string
|
||||
resume func() (entry, bool)
|
||||
cancel func()
|
||||
curr entry
|
||||
eof bool
|
||||
rowID int64
|
||||
base string
|
||||
next func() (entry, bool)
|
||||
stop func()
|
||||
curr entry
|
||||
eof bool
|
||||
rowID int64
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
@@ -78,8 +78,8 @@ type entry struct {
|
||||
}
|
||||
|
||||
func (c *cursor) Close() error {
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
if c.stop != nil {
|
||||
c.stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
c.base = base
|
||||
}
|
||||
|
||||
c.resume, c.cancel = iter.Pull(func(yield func(entry) bool) {
|
||||
c.next, c.stop = 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
|
||||
@@ -121,7 +121,7 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
}
|
||||
|
||||
func (c *cursor) Next() error {
|
||||
curr, ok := c.resume()
|
||||
curr, ok := c.next()
|
||||
c.curr = curr
|
||||
c.eof = !ok
|
||||
c.rowID++
|
||||
|
||||
@@ -21,9 +21,9 @@ func Test_fsdir(t *testing.T) {
|
||||
|
||||
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
|
||||
t.Run("", func(t *testing.T) {
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, func(c *sqlite3.Conn) error {
|
||||
db, err := driver.Open(dsn, func(c *sqlite3.Conn) error {
|
||||
fileio.RegisterFS(c, fsys)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
|
||||
func Test_writefile(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
db, err := driver.Open(dsn, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
_ "crypto/md5"
|
||||
_ "crypto/sha1"
|
||||
_ "crypto/sha256"
|
||||
_ "crypto/sha3"
|
||||
_ "crypto/sha512"
|
||||
"testing"
|
||||
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
_ "golang.org/x/crypto/blake2s"
|
||||
_ "golang.org/x/crypto/md4"
|
||||
_ "golang.org/x/crypto/ripemd160"
|
||||
_ "golang.org/x/crypto/sha3"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -55,7 +55,7 @@ func TestRegister(t *testing.T) {
|
||||
{"blake2b('', 256)", "0E5751C026E543B2E8AB2EB06099DAA1D1E5DF47778F7787FAAB45CDF12FE3A8"},
|
||||
}
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
db, err := driver.Open(dsn, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
113
ext/ipaddr/ipaddr.go
Normal file
113
ext/ipaddr/ipaddr.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// Package ipaddr provides functions to manipulate IPs and CIDRs.
|
||||
//
|
||||
// It provides the following functions:
|
||||
// - ipcontains(prefix, ip)
|
||||
// - ipoverlaps(prefix1, prefix2)
|
||||
// - ipfamily(ip/prefix)
|
||||
// - iphost(ip/prefix)
|
||||
// - ipmasklen(prefix)
|
||||
// - ipnetwork(prefix)
|
||||
package ipaddr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Register IP/CIDR functions for a database connection.
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
return errors.Join(
|
||||
db.CreateFunction("ipcontains", 2, flags, contains),
|
||||
db.CreateFunction("ipoverlaps", 2, flags, overlaps),
|
||||
db.CreateFunction("ipfamily", 1, flags, family),
|
||||
db.CreateFunction("iphost", 1, flags, host),
|
||||
db.CreateFunction("ipmasklen", 1, flags, masklen),
|
||||
db.CreateFunction("ipnetwork", 1, flags, network))
|
||||
}
|
||||
|
||||
func contains(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
prefix, err := netip.ParsePrefix(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
addr, err := netip.ParseAddr(arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
ctx.ResultBool(prefix.Contains(addr))
|
||||
}
|
||||
|
||||
func overlaps(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
prefix1, err := netip.ParsePrefix(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
prefix2, err := netip.ParsePrefix(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
ctx.ResultBool(prefix1.Overlaps(prefix2))
|
||||
}
|
||||
|
||||
func family(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
addr, err := addr(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
switch {
|
||||
case addr.Is4():
|
||||
ctx.ResultInt(4)
|
||||
case addr.Is6():
|
||||
ctx.ResultInt(6)
|
||||
}
|
||||
}
|
||||
|
||||
func host(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
addr, err := addr(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
buf, _ := addr.MarshalText()
|
||||
ctx.ResultRawText(buf)
|
||||
}
|
||||
|
||||
func masklen(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
prefix, err := netip.ParsePrefix(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
ctx.ResultInt(prefix.Bits())
|
||||
}
|
||||
|
||||
func network(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
prefix, err := netip.ParsePrefix(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
buf, _ := prefix.Masked().MarshalText()
|
||||
ctx.ResultRawText(buf)
|
||||
}
|
||||
|
||||
func addr(text string) (netip.Addr, error) {
|
||||
addr, err := netip.ParseAddr(text)
|
||||
if err != nil {
|
||||
if prefix, err := netip.ParsePrefix(text); err == nil {
|
||||
return prefix.Addr(), nil
|
||||
}
|
||||
if addrpt, err := netip.ParseAddrPort(text); err == nil {
|
||||
return addrpt.Addr(), nil
|
||||
}
|
||||
}
|
||||
return addr, err
|
||||
}
|
||||
88
ext/ipaddr/ipaddr_test.go
Normal file
88
ext/ipaddr/ipaddr_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package ipaddr_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/ipaddr"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(dsn, ipaddr.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var got string
|
||||
|
||||
err = db.QueryRow(`SELECT ipfamily('::1')`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "6" {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT ipfamily('[::1]:80')`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "6" {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT ipfamily('192.168.1.5/24')`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "4" {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT iphost('192.168.1.5/24')`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "192.168.1.5" {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT ipmasklen('192.168.1.5/24')`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "24" {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT ipnetwork('192.168.1.5/24')`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "192.168.1.0/24" {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT ipcontains('192.168.1.0/24', '192.168.1.5')`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "1" {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT ipoverlaps('192.168.1.0/24', '192.168.1.5/32')`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "1" {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
}
|
||||
@@ -67,9 +67,9 @@ func Example() {
|
||||
|
||||
func Test_lines(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, lines.Register)
|
||||
db, err := driver.Open(dsn, lines.Register)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -98,9 +98,9 @@ func Test_lines(t *testing.T) {
|
||||
|
||||
func Test_lines_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, lines.Register)
|
||||
db, err := driver.Open(dsn, lines.Register)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -123,9 +123,9 @@ func Test_lines_error(t *testing.T) {
|
||||
|
||||
func Test_lines_read(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, lines.Register)
|
||||
db, err := driver.Open(dsn, lines.Register)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -155,9 +155,9 @@ func Test_lines_read(t *testing.T) {
|
||||
|
||||
func Test_lines_test(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, lines.Register)
|
||||
db, err := driver.Open(dsn, lines.Register)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package pivot_test
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -85,7 +86,7 @@ func Example() {
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(pivot.Register)
|
||||
m.Run()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
@@ -140,10 +141,10 @@ func TestRegister(t *testing.T) {
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnInt(0); got != 3 {
|
||||
t.Errorf("got %d, want 3", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnInt(0); got != 3 {
|
||||
t.Errorf("got %d, want 3", got)
|
||||
}
|
||||
|
||||
err = db.Exec(`ALTER TABLE v_x RENAME TO v_y`)
|
||||
|
||||
@@ -111,22 +111,24 @@ loop2:
|
||||
return glob.String()
|
||||
}
|
||||
|
||||
func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
|
||||
func load(ctx sqlite3.Context, arg []sqlite3.Value, i int) (*regexp.Regexp, error) {
|
||||
re, ok := ctx.GetAuxData(i).(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
re, ok = arg[i].Pointer().(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(arg[i].Text())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
re = r
|
||||
}
|
||||
re = r
|
||||
ctx.SetAuxData(0, r)
|
||||
ctx.SetAuxData(i, re)
|
||||
}
|
||||
return re, nil
|
||||
}
|
||||
|
||||
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
_ = arg[1] // bounds check
|
||||
re, err := load(ctx, 0, arg[0].Text())
|
||||
re, err := load(ctx, arg, 0)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
@@ -136,18 +138,17 @@ func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
func regexLike(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
re, err := load(ctx, arg, 1)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
|
||||
text := arg[0].RawText()
|
||||
ctx.ResultBool(re.Match(text))
|
||||
}
|
||||
|
||||
func regexCount(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
re, err := load(ctx, arg, 1)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
@@ -162,7 +163,7 @@ func regexCount(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
re, err := load(ctx, arg, 1)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
@@ -187,7 +188,7 @@ func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
func regexInstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
re, err := load(ctx, arg, 1)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
@@ -215,16 +216,14 @@ func regexInstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
func regexReplace(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
_ = arg[2] // bounds check
|
||||
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
re, err := load(ctx, arg, 1)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
|
||||
text := arg[0].RawText()
|
||||
repl := arg[2].RawText()
|
||||
text := arg[0].RawText()
|
||||
var pos, n int
|
||||
if len(arg) > 3 {
|
||||
pos = arg[3].Int()
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"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"
|
||||
@@ -14,9 +15,9 @@ import (
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
db, err := driver.Open(dsn, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -37,7 +38,7 @@ func TestRegister(t *testing.T) {
|
||||
{`regexp_instr('Hello', '.', 6)`, ""},
|
||||
{`regexp_substr('Hello', 'el.')`, "ell"},
|
||||
{`regexp_replace('Hello', 'llo', 'll')`, "Hell"},
|
||||
// https://www.postgresql.org/docs/current/functions-matching.html
|
||||
// https://postgresql.org/docs/current/functions-matching.html
|
||||
{`regexp_count('ABCABCAXYaxy', 'A.')`, "3"},
|
||||
{`regexp_count('ABCABCAXYaxy', '(?i)A.', 1)`, "4"},
|
||||
{`regexp_instr('number of your street, town zip, FR', '[^,]+', 1, 2)`, "23"},
|
||||
@@ -79,9 +80,9 @@ func TestRegister(t *testing.T) {
|
||||
|
||||
func TestRegister_errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
db, err := driver.Open(dsn, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -104,6 +105,27 @@ func TestRegister_errors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_pointer(t *testing.T) {
|
||||
t.Parallel()
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(dsn, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var got int
|
||||
err = db.QueryRow(`SELECT regexp_count('ABCABCAXYaxy', ?, 1)`,
|
||||
sqlite3.Pointer(regexp.MustCompile(`(?i)A.`))).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != 4 {
|
||||
t.Errorf("got %d, want %d", got, 4)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
re string
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
package serdes
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/util/vfsutil"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
)
|
||||
|
||||
@@ -14,16 +13,16 @@ func init() {
|
||||
vfs.Register(vfsName, sliceVFS{})
|
||||
}
|
||||
|
||||
var fileToOpen = make(chan *sliceFile, 1)
|
||||
var fileToOpen = make(chan *[]byte, 1)
|
||||
|
||||
// Serialize backs up a database into a byte slice.
|
||||
//
|
||||
// https://sqlite.org/c3ref/serialize.html
|
||||
func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) {
|
||||
var file sliceFile
|
||||
var file []byte
|
||||
fileToOpen <- &file
|
||||
err := db.Backup(schema, "file:serdes.db?vfs="+vfsName)
|
||||
return file.data, err
|
||||
err := db.Backup(schema, "file:serdes.db?nolock=1&vfs="+vfsName)
|
||||
return file, err
|
||||
}
|
||||
|
||||
// Deserialize restores a database from a byte slice,
|
||||
@@ -41,8 +40,8 @@ func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) {
|
||||
// ["memdb"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb
|
||||
// ["reader"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs
|
||||
func Deserialize(db *sqlite3.Conn, schema string, data []byte) error {
|
||||
fileToOpen <- &sliceFile{data}
|
||||
return db.Restore(schema, "file:serdes.db?vfs="+vfsName)
|
||||
fileToOpen <- &data
|
||||
return db.Restore(schema, "file:serdes.db?immutable=1&vfs="+vfsName)
|
||||
}
|
||||
|
||||
type sliceVFS struct{}
|
||||
@@ -53,14 +52,14 @@ func (sliceVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, e
|
||||
}
|
||||
select {
|
||||
case file := <-fileToOpen:
|
||||
return file, flags | vfs.OPEN_MEMORY, nil
|
||||
return (*vfsutil.SliceFile)(file), flags | vfs.OPEN_MEMORY, nil
|
||||
default:
|
||||
return nil, flags, sqlite3.MISUSE
|
||||
}
|
||||
}
|
||||
|
||||
func (sliceVFS) Delete(name string, dirSync bool) error {
|
||||
// notest // OPEN_MEMORY
|
||||
// notest // no journals to delete
|
||||
return sqlite3.IOERR_DELETE
|
||||
}
|
||||
|
||||
@@ -71,70 +70,3 @@ func (sliceVFS) Access(name string, flag vfs.AccessFlag) (bool, error) {
|
||||
func (sliceVFS) FullPathname(name string) (string, error) {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
type sliceFile struct{ data []byte }
|
||||
|
||||
func (f *sliceFile) ReadAt(b []byte, off int64) (n int, err error) {
|
||||
if d := f.data; off < int64(len(d)) {
|
||||
n = copy(b, d[off:])
|
||||
}
|
||||
if n == 0 {
|
||||
err = io.EOF
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *sliceFile) WriteAt(b []byte, off int64) (n int, err error) {
|
||||
if d := f.data; off > int64(len(d)) {
|
||||
f.data = append(d, make([]byte, off-int64(len(d)))...)
|
||||
}
|
||||
d := append(f.data[:off], b...)
|
||||
if len(d) > len(f.data) {
|
||||
f.data = d
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (f *sliceFile) Size() (int64, error) {
|
||||
return int64(len(f.data)), nil
|
||||
}
|
||||
|
||||
func (f *sliceFile) Truncate(size int64) error {
|
||||
if d := f.data; size < int64(len(d)) {
|
||||
f.data = d[:size]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *sliceFile) SizeHint(size int64) error {
|
||||
if d := f.data; size > int64(len(d)) {
|
||||
f.data = append(d, make([]byte, size-int64(len(d)))...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*sliceFile) Close() error { return nil }
|
||||
|
||||
func (*sliceFile) Sync(flag vfs.SyncFlag) error { return nil }
|
||||
|
||||
func (*sliceFile) Lock(lock vfs.LockLevel) error { return nil }
|
||||
|
||||
func (*sliceFile) Unlock(lock vfs.LockLevel) error { return nil }
|
||||
|
||||
func (*sliceFile) CheckReservedLock() (bool, error) {
|
||||
// notest // OPEN_MEMORY
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (*sliceFile) SectorSize() int {
|
||||
// notest // IOCAP_POWERSAFE_OVERWRITE
|
||||
return 0
|
||||
}
|
||||
|
||||
func (*sliceFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
|
||||
return vfs.IOCAP_ATOMIC |
|
||||
vfs.IOCAP_SAFE_APPEND |
|
||||
vfs.IOCAP_SEQUENTIAL |
|
||||
vfs.IOCAP_POWERSAFE_OVERWRITE |
|
||||
vfs.IOCAP_SUBPAGE_READ
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package serdes_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -11,7 +12,30 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/ext/serdes"
|
||||
)
|
||||
|
||||
func TestDeserialize(t *testing.T) {
|
||||
//go:embed testdata/wal.db
|
||||
var walDB []byte
|
||||
|
||||
func Test_wal(t *testing.T) {
|
||||
db, err := sqlite3.Open("testdata/wal.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
data, err := serdes.Serialize(db, "main")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
compareDBs(t, data, walDB)
|
||||
|
||||
err = serdes.Deserialize(db, "temp", walDB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_northwind(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping in short mode")
|
||||
}
|
||||
@@ -37,10 +61,14 @@ func TestDeserialize(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(input) != len(output) {
|
||||
compareDBs(t, input, output)
|
||||
}
|
||||
|
||||
func compareDBs(t *testing.T, a, b []byte) {
|
||||
if len(a) != len(b) {
|
||||
t.Fatal("lengths are different")
|
||||
}
|
||||
for i := range input {
|
||||
for i := range a {
|
||||
// These may be different.
|
||||
switch {
|
||||
case 24 <= i && i < 28:
|
||||
@@ -53,14 +81,14 @@ func TestDeserialize(t *testing.T) {
|
||||
// SQLite version that wrote the file.
|
||||
continue
|
||||
}
|
||||
if input[i] != output[i] {
|
||||
t.Errorf("difference at %d: %d %d", i, input[i], output[i])
|
||||
if a[i] != b[i] {
|
||||
t.Errorf("difference at %d: %d %d", i, a[i], b[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func httpGet() ([]byte, error) {
|
||||
res, err := http.Get("https://raw.githubusercontent.com/jpwhite3/northwind-SQLite3/refs/heads/main/dist/northwind.db")
|
||||
res, err := http.Get("https://github.com/jpwhite3/northwind-SQLite3/raw/refs/heads/main/dist/northwind.db")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
BIN
ext/serdes/testdata/wal.db
vendored
Normal file
BIN
ext/serdes/testdata/wal.db
vendored
Normal file
Binary file not shown.
@@ -159,8 +159,6 @@ func (c *cursor) Close() error {
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
c.arg = arg
|
||||
c.rowID = 0
|
||||
err := errors.Join(
|
||||
c.stmt.Reset(),
|
||||
c.stmt.ClearBindings())
|
||||
@@ -187,6 +185,8 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.arg = append(c.arg[:0], arg...)
|
||||
c.rowID = 0
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package statement_test
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
@@ -50,7 +51,7 @@ func Example() {
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(statement.Register)
|
||||
m.Run()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
@@ -91,7 +92,9 @@ func TestRegister(t *testing.T) {
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else {
|
||||
x := stmt.ColumnInt(0)
|
||||
y := stmt.ColumnInt(1)
|
||||
hypot := stmt.ColumnInt(2)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ANSI SQL Aggregate Functions
|
||||
|
||||
https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
|
||||
https://oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
|
||||
|
||||
## Built in aggregates
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const (
|
||||
some
|
||||
)
|
||||
|
||||
func newBoolean(kind int) func() sqlite3.AggregateFunction {
|
||||
func newBoolean(kind int) sqlite3.AggregateConstructor {
|
||||
return func() sqlite3.AggregateFunction { return &boolean{kind: kind} }
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ func TestRegister_boolean(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else {
|
||||
if got := stmt.ColumnBool(0); got != true {
|
||||
t.Errorf("got %v, want true", got)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ type mode struct {
|
||||
|
||||
func (m mode) Value(ctx sqlite3.Context) {
|
||||
var (
|
||||
max = 0
|
||||
typ = sqlite3.NULL
|
||||
max uint
|
||||
i64 int64
|
||||
f64 float64
|
||||
str string
|
||||
@@ -32,7 +32,6 @@ func (m mode) Value(ctx sqlite3.Context) {
|
||||
i64 = k
|
||||
}
|
||||
}
|
||||
f64 = float64(i64)
|
||||
for k, v := range m.reals {
|
||||
if v > max || v == max && k < f64 {
|
||||
typ = sqlite3.FLOAT
|
||||
@@ -66,33 +65,45 @@ func (m mode) Value(ctx sqlite3.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *mode) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
func (m *mode) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
switch arg[0].Type() {
|
||||
case sqlite3.INTEGER:
|
||||
b.ints.add(arg[0].Int64())
|
||||
if m.reals == nil {
|
||||
m.ints.add(arg[0].Int64())
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case sqlite3.FLOAT:
|
||||
b.reals.add(arg[0].Float())
|
||||
m.reals.add(arg[0].Float())
|
||||
for k, v := range m.ints {
|
||||
m.reals[float64(k)] += v
|
||||
}
|
||||
m.ints = nil
|
||||
case sqlite3.TEXT:
|
||||
b.texts.add(arg[0].Text())
|
||||
m.texts.add(arg[0].Text())
|
||||
case sqlite3.BLOB:
|
||||
b.blobs.add(string(arg[0].RawBlob()))
|
||||
m.blobs.add(string(arg[0].RawBlob()))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *mode) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
func (m *mode) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
switch arg[0].Type() {
|
||||
case sqlite3.INTEGER:
|
||||
b.ints.del(arg[0].Int64())
|
||||
if m.reals == nil {
|
||||
m.ints.del(arg[0].Int64())
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case sqlite3.FLOAT:
|
||||
b.reals.del(arg[0].Float())
|
||||
m.reals.del(arg[0].Float())
|
||||
case sqlite3.TEXT:
|
||||
b.texts.del(arg[0].Text())
|
||||
m.texts.del(arg[0].Text())
|
||||
case sqlite3.BLOB:
|
||||
b.blobs.del(string(arg[0].RawBlob()))
|
||||
m.blobs.del(string(arg[0].RawBlob()))
|
||||
}
|
||||
}
|
||||
|
||||
type counter[T comparable] map[T]int
|
||||
type counter[T comparable] map[T]uint
|
||||
|
||||
func (c *counter[T]) add(k T) {
|
||||
if (*c) == nil {
|
||||
@@ -102,11 +113,9 @@ func (c *counter[T]) add(k T) {
|
||||
}
|
||||
|
||||
func (c counter[T]) del(k T) {
|
||||
switch n := c[k]; n {
|
||||
default:
|
||||
c[k] = n - 1
|
||||
case 1:
|
||||
if n := c[k]; n == 1 {
|
||||
delete(c, k)
|
||||
case 0:
|
||||
} else {
|
||||
c[k] = n - 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ func TestRegister_mode(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnInt(0); got != 3 {
|
||||
t.Errorf("got %v, want 3", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnInt(0); got != 3 {
|
||||
t.Errorf("got %v, want 3", got)
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
@@ -32,10 +32,10 @@ func TestRegister_mode(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnInt(0); got != 1 {
|
||||
t.Errorf("got %v, want 1", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnInt(0); got != 1 {
|
||||
t.Errorf("got %v, want 1", got)
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
@@ -43,10 +43,10 @@ func TestRegister_mode(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 2.5 {
|
||||
t.Errorf("got %v, want 2.5", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnFloat(0); got != 2.5 {
|
||||
t.Errorf("got %v, want 2.5", got)
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
@@ -54,21 +54,22 @@ func TestRegister_mode(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnText(0); got != "red" {
|
||||
t.Errorf("got %q, want red", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnText(0); got != "red" {
|
||||
t.Errorf("got %q, want red", got)
|
||||
}
|
||||
|
||||
stmt.Close()
|
||||
|
||||
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES (X'cafebabe'), ('green'), ('blue'), (X'cafebabe'))`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnText(0); got != "\xca\xfe\xba\xbe" {
|
||||
t.Errorf("got %q, want cafebabe", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnText(0); got != "\xca\xfe\xba\xbe" {
|
||||
t.Errorf("got %q, want cafebabe", got)
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
@@ -82,4 +83,20 @@ func TestRegister_mode(t *testing.T) {
|
||||
for stmt.Step() {
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES (?), (?), (?), (?), (?))`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stmt.BindInt(1, 1)
|
||||
stmt.BindInt(2, 1)
|
||||
stmt.BindInt(3, 2)
|
||||
stmt.BindFloat(4, 2)
|
||||
stmt.BindFloat(5, 2)
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnInt(0); got != 2 {
|
||||
t.Errorf("got %v, want 2", got)
|
||||
}
|
||||
stmt.Close()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const (
|
||||
percentile_disc
|
||||
)
|
||||
|
||||
func newPercentile(kind int) func() sqlite3.AggregateFunction {
|
||||
func newPercentile(kind int) sqlite3.AggregateConstructor {
|
||||
return func() sqlite3.AggregateFunction { return &percentile{kind: kind} }
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ func TestRegister_percentile(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else {
|
||||
if got := stmt.ColumnFloat(0); got != 10 {
|
||||
t.Errorf("got %v, want 10", got)
|
||||
}
|
||||
@@ -65,30 +67,30 @@ func TestRegister_percentile(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 5.5 {
|
||||
t.Errorf("got %v, want 5.5", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnFloat(0); got != 5.5 {
|
||||
t.Errorf("got %v, want 5.5", got)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 7 {
|
||||
t.Errorf("got %v, want 7", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnFloat(0); got != 7 {
|
||||
t.Errorf("got %v, want 7", got)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 10 {
|
||||
t.Errorf("got %v, want 10", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnFloat(0); got != 10 {
|
||||
t.Errorf("got %v, want 10", got)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 14.5 {
|
||||
t.Errorf("got %v, want 14.5", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnFloat(0); got != 14.5 {
|
||||
t.Errorf("got %v, want 14.5", got)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 16 {
|
||||
t.Errorf("got %v, want 16", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnFloat(0); got != 16 {
|
||||
t.Errorf("got %v, want 16", got)
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
@@ -103,7 +105,9 @@ func TestRegister_percentile(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else {
|
||||
if got := stmt.ColumnFloat(0); got != 4 {
|
||||
t.Errorf("got %v, want 4", got)
|
||||
}
|
||||
@@ -134,7 +138,9 @@ func TestRegister_percentile(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else {
|
||||
if got := stmt.ColumnType(0); got != sqlite3.NULL {
|
||||
t.Error("want NULL")
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
//
|
||||
// [Built-in Aggregate Functions]: https://sqlite.org/lang_aggfunc.html
|
||||
// [Built-in Window Functions]: https://sqlite.org/windowfunctions.html#builtins
|
||||
// [ANSI SQL Aggregate Functions]: https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
|
||||
// [ANSI SQL Aggregate Functions]: https://oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
|
||||
package stats
|
||||
|
||||
import (
|
||||
@@ -58,8 +58,11 @@ import (
|
||||
|
||||
// Register registers statistics functions.
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
const order = sqlite3.SELFORDER1 | flags
|
||||
const (
|
||||
flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
json = sqlite3.RESULT_SUBTYPE | flags
|
||||
order = sqlite3.SELFORDER1 | flags
|
||||
)
|
||||
return errors.Join(
|
||||
db.CreateWindowFunction("var_pop", 1, flags, newVariance(var_pop)),
|
||||
db.CreateWindowFunction("var_samp", 1, flags, newVariance(var_samp)),
|
||||
@@ -81,7 +84,7 @@ func Register(db *sqlite3.Conn) error {
|
||||
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("regr_json", 2, json, newCovariance(regr_json)),
|
||||
db.CreateWindowFunction("median", 1, order, newPercentile(median)),
|
||||
db.CreateWindowFunction("percentile", 2, order, newPercentile(percentile_100)),
|
||||
db.CreateWindowFunction("percentile_cont", 2, order, newPercentile(percentile_cont)),
|
||||
@@ -130,7 +133,7 @@ func special(kind int, n int64) (null, zero bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func newVariance(kind int) func() sqlite3.AggregateFunction {
|
||||
func newVariance(kind int) sqlite3.AggregateConstructor {
|
||||
return func() sqlite3.AggregateFunction { return &variance{kind: kind} }
|
||||
}
|
||||
|
||||
@@ -178,7 +181,7 @@ func (fn *variance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
}
|
||||
|
||||
func newCovariance(kind int) func() sqlite3.AggregateFunction {
|
||||
func newCovariance(kind int) sqlite3.AggregateConstructor {
|
||||
return func() sqlite3.AggregateFunction { return &covariance{kind: kind} }
|
||||
}
|
||||
|
||||
@@ -227,6 +230,7 @@ func (fn *covariance) Value(ctx sqlite3.Context) {
|
||||
case regr_json:
|
||||
var buf [128]byte
|
||||
ctx.ResultRawText(fn.regr_json(buf[:0]))
|
||||
ctx.ResultSubtype('J')
|
||||
return
|
||||
}
|
||||
ctx.ResultFloat(r)
|
||||
@@ -254,7 +258,7 @@ func (fn *covariance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
}
|
||||
|
||||
func newMoments(kind int) func() sqlite3.AggregateFunction {
|
||||
func newMoments(kind int) sqlite3.AggregateConstructor {
|
||||
return func() sqlite3.AggregateFunction { return &momentfn{kind: kind} }
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package stats_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(stats.Register)
|
||||
m.Run()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRegister_variance(t *testing.T) {
|
||||
@@ -33,10 +34,10 @@ func TestRegister_variance(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnType(0); got != sqlite3.NULL {
|
||||
t.Errorf("got %v, want NULL", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else if got := stmt.ColumnType(0); got != sqlite3.NULL {
|
||||
t.Errorf("got %v, want NULL", got)
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
@@ -56,7 +57,9 @@ func TestRegister_variance(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else {
|
||||
if got := stmt.ColumnFloat(0); got != 40 {
|
||||
t.Errorf("got %v, want 40", got)
|
||||
}
|
||||
@@ -130,7 +133,9 @@ func TestRegister_covariance(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
} else {
|
||||
if got := stmt.ColumnInt(0); got != 0 {
|
||||
t.Errorf("got %v, want 0", got)
|
||||
}
|
||||
@@ -155,49 +160,50 @@ func TestRegister_covariance(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 0.9881049293224639 {
|
||||
t.Errorf("got %v, want 0.9881049293224639", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(1); got != 21.25 {
|
||||
t.Errorf("got %v, want 21.25", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(2); got != 17 {
|
||||
t.Errorf("got %v, want 17", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(3); got != 4.2 {
|
||||
t.Errorf("got %v, want 4.2", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(4); got != 75 {
|
||||
t.Errorf("got %v, want 75", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(5); got != 14.8 {
|
||||
t.Errorf("got %v, want 14.8", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(6); got != 500 {
|
||||
t.Errorf("got %v, want 500", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(7); got != 85 {
|
||||
t.Errorf("got %v, want 85", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(8); got != 0.17 {
|
||||
t.Errorf("got %v, want 0.17", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(9); got != -8.55 {
|
||||
t.Errorf("got %v, want -8.55", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(10); got != 0.9763513513513513 {
|
||||
t.Errorf("got %v, want 0.9763513513513513", got)
|
||||
}
|
||||
if got := stmt.ColumnInt(11); got != 5 {
|
||||
t.Errorf("got %v, want 5", got)
|
||||
}
|
||||
var a map[string]float64
|
||||
if err := stmt.ColumnJSON(12, &a); err != nil {
|
||||
t.Error(err)
|
||||
} else if got := a["count"]; got != 5 {
|
||||
t.Errorf("got %v, want 5", got)
|
||||
}
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
}
|
||||
if got := stmt.ColumnFloat(0); got != 0.9881049293224639 {
|
||||
t.Errorf("got %v, want 0.9881049293224639", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(1); got != 21.25 {
|
||||
t.Errorf("got %v, want 21.25", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(2); got != 17 {
|
||||
t.Errorf("got %v, want 17", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(3); got != 4.2 {
|
||||
t.Errorf("got %v, want 4.2", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(4); got != 75 {
|
||||
t.Errorf("got %v, want 75", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(5); got != 14.8 {
|
||||
t.Errorf("got %v, want 14.8", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(6); got != 500 {
|
||||
t.Errorf("got %v, want 500", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(7); got != 85 {
|
||||
t.Errorf("got %v, want 85", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(8); got != 0.17 {
|
||||
t.Errorf("got %v, want 0.17", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(9); got != -8.55 {
|
||||
t.Errorf("got %v, want -8.55", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(10); got != 0.9763513513513513 {
|
||||
t.Errorf("got %v, want 0.9763513513513513", got)
|
||||
}
|
||||
if got := stmt.ColumnInt(11); got != 5 {
|
||||
t.Errorf("got %v, want 5", got)
|
||||
}
|
||||
var a map[string]float64
|
||||
if err := stmt.ColumnJSON(12, &a); err != nil {
|
||||
t.Error(err)
|
||||
} else if got := a["count"]; got != 5 {
|
||||
t.Errorf("got %v, want 5", got)
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
@@ -247,7 +253,9 @@ func Benchmark_average(b *testing.B) {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
if !stmt.Step() {
|
||||
b.Fatal(stmt.Err())
|
||||
} else {
|
||||
want := float64(b.N) / 2
|
||||
if got := stmt.ColumnFloat(0); got != want {
|
||||
b.Errorf("got %v, want %v", got, want)
|
||||
@@ -281,7 +289,9 @@ func Benchmark_variance(b *testing.B) {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
if stmt.Step() && b.N > 100 {
|
||||
if !stmt.Step() {
|
||||
b.Fatal(stmt.Err())
|
||||
} else if b.N > 100 {
|
||||
want := float64(b.N*b.N) / 12
|
||||
if got := stmt.ColumnFloat(0); want > (got-want)*float64(b.N) {
|
||||
b.Errorf("got %v, want %v", got, want)
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
// Package unicode provides an alternative to the SQLite ICU extension.
|
||||
//
|
||||
// Like the [ICU extension], it provides Unicode aware:
|
||||
// - upper() and lower() functions,
|
||||
// - LIKE and REGEXP operators,
|
||||
// - collation sequences.
|
||||
// - upper() and lower() functions
|
||||
// - LIKE and REGEXP operators
|
||||
// - collation sequences
|
||||
//
|
||||
// The implementation is not 100% compatible with the [ICU extension]:
|
||||
// - upper() and lower() use [strings.ToUpper], [strings.ToLower] and [cases];
|
||||
// - the LIKE operator follows [strings.EqualFold] rules;
|
||||
// - the REGEXP operator uses Go [regexp/syntax];
|
||||
// - collation sequences use [collate].
|
||||
// Like PostgreSQL, it also provides:
|
||||
// - initcap()
|
||||
// - casefold()
|
||||
// - normalize()
|
||||
// - unaccent()
|
||||
//
|
||||
// It also provides (approximately) from PostgreSQL:
|
||||
// - casefold(),
|
||||
// - initcap(),
|
||||
// - normalize(),
|
||||
// - unaccent().
|
||||
// The implementations are not 100% compatible:
|
||||
// - upper(), lower(), initcap() casefold() use [strings.ToUpper], [strings.ToLower], [strings.Title] and [cases]
|
||||
// - normalize(), unaccent() use [transform] and [unicode.Mn]
|
||||
// - the LIKE operator follows [strings.EqualFold] rules
|
||||
// - the REGEXP operator uses Go [regexp/syntax]
|
||||
// - collation sequences use [collate]
|
||||
//
|
||||
// Expect subtle differences (e.g.) in the handling of Turkish case folding.
|
||||
//
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -41,7 +43,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Set RegisterLike to false to not register a Unicode aware LIKE operator.
|
||||
// RegisterLike must be set to false to not register a Unicode aware LIKE operator.
|
||||
// Overriding the built-in LIKE operator disables the [LIKE optimization].
|
||||
//
|
||||
// [LIKE optimization]: https://sqlite.org/optoverview.html#the_like_optimization
|
||||
@@ -113,9 +115,8 @@ func upper(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
c := cases.Upper(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
cs = c
|
||||
cs = cases.Upper(t)
|
||||
ctx.SetAuxData(1, cs)
|
||||
}
|
||||
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
|
||||
}
|
||||
@@ -132,9 +133,8 @@ func lower(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
c := cases.Lower(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
cs = c
|
||||
cs = cases.Lower(t)
|
||||
ctx.SetAuxData(1, cs)
|
||||
}
|
||||
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
|
||||
}
|
||||
@@ -151,9 +151,8 @@ func initcap(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
c := cases.Title(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
cs = c
|
||||
cs = cases.Title(t)
|
||||
ctx.SetAuxData(1, cs)
|
||||
}
|
||||
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
|
||||
}
|
||||
@@ -162,8 +161,16 @@ func casefold(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultRawText(cases.Fold().Bytes(arg[0].RawText()))
|
||||
}
|
||||
|
||||
var unaccentPool = sync.Pool{
|
||||
New: func() any {
|
||||
return transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||
},
|
||||
}
|
||||
|
||||
func unaccent(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
unaccent := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||
unaccent := unaccentPool.Get().(transform.Transformer)
|
||||
defer unaccentPool.Put(unaccent)
|
||||
|
||||
res, _, err := transform.Bytes(unaccent, arg[0].RawText())
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
@@ -200,13 +207,16 @@ func normalize(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, ok := ctx.GetAuxData(0).(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
re, ok = arg[0].Pointer().(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
re = r
|
||||
}
|
||||
re = r
|
||||
ctx.SetAuxData(0, r)
|
||||
ctx.SetAuxData(0, re)
|
||||
}
|
||||
ctx.ResultBool(re.Match(arg[1].RawText()))
|
||||
}
|
||||
|
||||
@@ -26,11 +26,10 @@ func TestRegister(t *testing.T) {
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
return stmt.ColumnText(0)
|
||||
if !stmt.Step() {
|
||||
t.Fatal(stmt.Err())
|
||||
}
|
||||
t.Fatal(stmt.Err())
|
||||
return ""
|
||||
return stmt.ColumnText(0)
|
||||
}
|
||||
|
||||
Register(db)
|
||||
|
||||
@@ -17,17 +17,18 @@ import (
|
||||
|
||||
// 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.
|
||||
// - uuid([ version [, domain/namespace, [ id/data ]]]):
|
||||
// to generate a UUID as a string
|
||||
// - uuid_str(u):
|
||||
// to convert a UUID into a well-formed UUID string
|
||||
// - uuid_blob(u):
|
||||
// to convert a UUID into a 16-byte blob
|
||||
// - uuid_extract_version(u):
|
||||
// to extract the version of a RFC 4122 UUID
|
||||
// - uuid_extract_timestamp(u):
|
||||
// to extract the timestamp of a version 1/2/6/7 UUID
|
||||
// - gen_random_uuid(u):
|
||||
// to generate a version 4 (random) UUID
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
return errors.Join(
|
||||
@@ -38,7 +39,8 @@ func Register(db *sqlite3.Conn) error {
|
||||
db.CreateFunction("uuid_str", 1, flags, toString),
|
||||
db.CreateFunction("uuid_blob", 1, flags, toBlob),
|
||||
db.CreateFunction("uuid_extract_version", 1, flags, version),
|
||||
db.CreateFunction("uuid_extract_timestamp", 1, flags, timestamp))
|
||||
db.CreateFunction("uuid_extract_timestamp", 1, flags, timestamp),
|
||||
db.CreateFunction("gen_random_uuid", 0, sqlite3.INNOCUOUS, generate))
|
||||
}
|
||||
|
||||
func generate(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
@@ -192,7 +194,7 @@ func timestamp(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
switch u.Version() {
|
||||
case 1, 2, 6, 7:
|
||||
ctx.ResultTime(
|
||||
time.Unix(u.Time().UnixTime()),
|
||||
time.Unix(u.Time().UnixTime()).UTC(),
|
||||
sqlite3.TimeFormatDefault)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
|
||||
func Test_generate(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
db, err := driver.Open(dsn, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -153,9 +153,9 @@ func Test_generate(t *testing.T) {
|
||||
|
||||
func Test_convert(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, Register)
|
||||
db, err := driver.Open(dsn, Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ func Register(db *sqlite3.Conn) error {
|
||||
}
|
||||
|
||||
func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
var x [63]int64
|
||||
if len(arg) > len(x) {
|
||||
ctx.ResultError(util.ErrorString("zorder: too many parameters"))
|
||||
var x [24]int64
|
||||
if n := len(arg); n < 2 || n > 24 {
|
||||
ctx.ResultError(util.ErrorString("zorder: needs between 2 and 24 dimensions"))
|
||||
return
|
||||
}
|
||||
for i := range arg {
|
||||
@@ -29,17 +29,15 @@ func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
var z int64
|
||||
if len(arg) > 0 {
|
||||
for i := range x {
|
||||
j := i % len(arg)
|
||||
z |= (x[j] & 1) << i
|
||||
x[j] >>= 1
|
||||
}
|
||||
for i := range 63 {
|
||||
j := i % len(arg)
|
||||
z |= (x[j] & 1) << i
|
||||
x[j] >>= 1
|
||||
}
|
||||
|
||||
for i := range arg {
|
||||
if x[i] != 0 {
|
||||
ctx.ResultError(util.ErrorString("zorder: parameter too large"))
|
||||
ctx.ResultError(util.ErrorString("zorder: argument out of range"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -51,6 +49,19 @@ func unzorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
n := arg[1].Int64()
|
||||
z := arg[0].Int64()
|
||||
|
||||
if n < 2 || n > 24 {
|
||||
ctx.ResultError(util.ErrorString("unzorder: needs between 2 and 24 dimensions"))
|
||||
return
|
||||
}
|
||||
if i < 0 || i >= n {
|
||||
ctx.ResultError(util.ErrorString("unzorder: index out of range"))
|
||||
return
|
||||
}
|
||||
if z < 0 {
|
||||
ctx.ResultError(util.ErrorString("unzorder: argument out of range"))
|
||||
return
|
||||
}
|
||||
|
||||
var k int
|
||||
var x int64
|
||||
for j := i; j < 63; j += n {
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func TestRegister_zorder(t *testing.T) {
|
||||
func Test_zorder(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, zorder.Register)
|
||||
db, err := driver.Open(dsn, zorder.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -57,11 +57,11 @@ func TestRegister_zorder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_unzorder(t *testing.T) {
|
||||
func Test_unzorder(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, zorder.Register)
|
||||
db, err := driver.Open(dsn, zorder.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -85,11 +85,11 @@ func TestRegister_unzorder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_error(t *testing.T) {
|
||||
func Test_zorder_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(tmp, zorder.Register)
|
||||
db, err := driver.Open(dsn, zorder.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func TestRegister_error(t *testing.T) {
|
||||
|
||||
var buf strings.Builder
|
||||
buf.WriteString("SELECT zorder(0")
|
||||
for i := 1; i < 80; i++ {
|
||||
for i := 1; i < 25; i++ {
|
||||
buf.WriteByte(',')
|
||||
buf.WriteString(strconv.Itoa(0))
|
||||
}
|
||||
@@ -113,3 +113,30 @@ func TestRegister_error(t *testing.T) {
|
||||
t.Error("want error")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unzorder_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
db, err := driver.Open(dsn, zorder.Register)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var got int64
|
||||
err = db.QueryRow(`SELECT unzorder(-1, 2, 0)`).Scan(&got)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT unzorder(0, 2, 2)`).Scan(&got)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT unzorder(0, 25, 2)`).Scan(&got)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
}
|
||||
|
||||
149
func.go
149
func.go
@@ -3,7 +3,9 @@ package sqlite3
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"iter"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
|
||||
@@ -45,7 +47,7 @@ func (c Conn) AnyCollationNeeded() error {
|
||||
// CreateCollation defines a new collating sequence.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_collation.html
|
||||
func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
|
||||
func (c *Conn) CreateCollation(name string, fn CollatingFunction) error {
|
||||
var funcPtr ptr_t
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
@@ -57,6 +59,10 @@ func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
|
||||
return c.error(rc)
|
||||
}
|
||||
|
||||
// CollatingFunction is the type of a collation callback.
|
||||
// Implementations must not retain a or b.
|
||||
type CollatingFunction func(a, b []byte) int
|
||||
|
||||
// CreateFunction defines a new scalar SQL function.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
@@ -77,34 +83,67 @@ func (c *Conn) CreateFunction(name string, nArg int, flag FunctionFlag, fn Scala
|
||||
// Implementations must not retain arg.
|
||||
type ScalarFunction func(ctx Context, arg ...Value)
|
||||
|
||||
// CreateWindowFunction defines a new aggregate or aggregate window SQL function.
|
||||
// If fn returns a [WindowFunction], then an aggregate window function is created.
|
||||
// If fn returns an [io.Closer], it will be called to free resources.
|
||||
// CreateAggregateFunction defines a new aggregate SQL function.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn func() AggregateFunction) error {
|
||||
func (c *Conn) CreateAggregateFunction(name string, nArg int, flag FunctionFlag, fn AggregateSeqFunction) error {
|
||||
var funcPtr ptr_t
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
call := "sqlite3_create_aggregate_function_go"
|
||||
if fn != nil {
|
||||
agg := fn()
|
||||
if c, ok := agg.(io.Closer); ok {
|
||||
if err := c.Close(); err != nil {
|
||||
return err
|
||||
funcPtr = util.AddHandle(c.ctx, AggregateConstructor(func() AggregateFunction {
|
||||
var a aggregateFunc
|
||||
coro := func(yieldCoro func(struct{}) bool) {
|
||||
seq := func(yieldSeq func([]Value) bool) {
|
||||
for yieldSeq(a.arg) {
|
||||
if !yieldCoro(struct{}{}) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
fn(&a.ctx, seq)
|
||||
}
|
||||
}
|
||||
if _, ok := agg.(WindowFunction); ok {
|
||||
call = "sqlite3_create_window_function_go"
|
||||
}
|
||||
funcPtr = util.AddHandle(c.ctx, fn)
|
||||
a.next, a.stop = iter.Pull(coro)
|
||||
return &a
|
||||
}))
|
||||
}
|
||||
rc := res_t(c.call(call,
|
||||
rc := res_t(c.call("sqlite3_create_aggregate_function_go",
|
||||
stk_t(c.handle), stk_t(namePtr), stk_t(nArg),
|
||||
stk_t(flag), stk_t(funcPtr)))
|
||||
return c.error(rc)
|
||||
}
|
||||
|
||||
// AggregateSeqFunction is the type of an aggregate SQL function.
|
||||
// Implementations must not retain the slices yielded by seq.
|
||||
type AggregateSeqFunction func(ctx *Context, seq iter.Seq[[]Value])
|
||||
|
||||
// CreateWindowFunction defines a new aggregate or aggregate window SQL function.
|
||||
// If fn returns a [WindowFunction], an aggregate window function is created.
|
||||
// If fn returns an [io.Closer], it will be called to free resources.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn AggregateConstructor) error {
|
||||
var funcPtr ptr_t
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
if fn != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, AggregateConstructor(func() AggregateFunction {
|
||||
agg := fn()
|
||||
if win, ok := agg.(WindowFunction); ok {
|
||||
return win
|
||||
}
|
||||
return agg
|
||||
}))
|
||||
}
|
||||
rc := res_t(c.call("sqlite3_create_window_function_go",
|
||||
stk_t(c.handle), stk_t(namePtr), stk_t(nArg),
|
||||
stk_t(flag), stk_t(funcPtr)))
|
||||
return c.error(rc)
|
||||
}
|
||||
|
||||
// AggregateConstructor is a an [AggregateFunction] constructor.
|
||||
type AggregateConstructor func() AggregateFunction
|
||||
|
||||
// AggregateFunction is the interface an aggregate function should implement.
|
||||
//
|
||||
// https://sqlite.org/appfunc.html
|
||||
@@ -153,26 +192,24 @@ func collationCallback(ctx context.Context, mod api.Module, pArg, pDB ptr_t, eTe
|
||||
}
|
||||
|
||||
func compareCallback(ctx context.Context, mod api.Module, pApp ptr_t, nKey1 int32, pKey1 ptr_t, nKey2 int32, pKey2 ptr_t) uint32 {
|
||||
fn := util.GetHandle(ctx, pApp).(func(a, b []byte) int)
|
||||
fn := util.GetHandle(ctx, pApp).(CollatingFunction)
|
||||
return uint32(fn(util.View(mod, pKey1, int64(nKey1)), util.View(mod, pKey2, int64(nKey2))))
|
||||
}
|
||||
|
||||
func funcCallback(ctx context.Context, mod api.Module, pCtx, pApp ptr_t, nArg int32, pArg ptr_t) {
|
||||
args := getFuncArgs()
|
||||
defer putFuncArgs(args)
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
args := callbackArgs(db, nArg, pArg)
|
||||
defer returnArgs(args)
|
||||
fn := util.GetHandle(db.ctx, pApp).(ScalarFunction)
|
||||
callbackArgs(db, args[:nArg], pArg)
|
||||
fn(Context{db, pCtx}, args[:nArg]...)
|
||||
fn(Context{db, pCtx}, *args...)
|
||||
}
|
||||
|
||||
func stepCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp ptr_t, nArg int32, pArg ptr_t) {
|
||||
args := getFuncArgs()
|
||||
defer putFuncArgs(args)
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
callbackArgs(db, args[:nArg], pArg)
|
||||
args := callbackArgs(db, nArg, pArg)
|
||||
defer returnArgs(args)
|
||||
fn, _ := callbackAggregate(db, pAgg, pApp)
|
||||
fn.Step(Context{db, pCtx}, args[:nArg]...)
|
||||
fn.Step(Context{db, pCtx}, *args...)
|
||||
}
|
||||
|
||||
func valueCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp ptr_t, final int32) {
|
||||
@@ -196,12 +233,11 @@ func valueCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp ptr_t,
|
||||
}
|
||||
|
||||
func inverseCallback(ctx context.Context, mod api.Module, pCtx, pAgg ptr_t, nArg int32, pArg ptr_t) {
|
||||
args := getFuncArgs()
|
||||
defer putFuncArgs(args)
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
callbackArgs(db, args[:nArg], pArg)
|
||||
args := callbackArgs(db, nArg, pArg)
|
||||
defer returnArgs(args)
|
||||
fn := util.GetHandle(db.ctx, pAgg).(WindowFunction)
|
||||
fn.Inverse(Context{db, pCtx}, args[:nArg]...)
|
||||
fn.Inverse(Context{db, pCtx}, *args...)
|
||||
}
|
||||
|
||||
func callbackAggregate(db *Conn, pAgg, pApp ptr_t) (AggregateFunction, ptr_t) {
|
||||
@@ -211,7 +247,7 @@ func callbackAggregate(db *Conn, pAgg, pApp ptr_t) (AggregateFunction, ptr_t) {
|
||||
}
|
||||
|
||||
// We need to create the aggregate.
|
||||
fn := util.GetHandle(db.ctx, pApp).(func() AggregateFunction)()
|
||||
fn := util.GetHandle(db.ctx, pApp).(AggregateConstructor)()
|
||||
if pAgg != 0 {
|
||||
handle := util.AddHandle(db.ctx, fn)
|
||||
util.Write32(db.mod, pAgg, handle)
|
||||
@@ -220,25 +256,54 @@ func callbackAggregate(db *Conn, pAgg, pApp ptr_t) (AggregateFunction, ptr_t) {
|
||||
return fn, 0
|
||||
}
|
||||
|
||||
func callbackArgs(db *Conn, arg []Value, pArg ptr_t) {
|
||||
for i := range arg {
|
||||
arg[i] = Value{
|
||||
var (
|
||||
valueArgsPool sync.Pool
|
||||
valueArgsLen atomic.Int32
|
||||
)
|
||||
|
||||
func callbackArgs(db *Conn, nArg int32, pArg ptr_t) *[]Value {
|
||||
arg, ok := valueArgsPool.Get().(*[]Value)
|
||||
if !ok || cap(*arg) < int(nArg) {
|
||||
max := valueArgsLen.Or(nArg) | nArg
|
||||
lst := make([]Value, max)
|
||||
arg = &lst
|
||||
}
|
||||
lst := (*arg)[:nArg]
|
||||
for i := range lst {
|
||||
lst[i] = Value{
|
||||
c: db,
|
||||
handle: util.Read32[ptr_t](db.mod, pArg+ptr_t(i)*ptrlen),
|
||||
}
|
||||
}
|
||||
*arg = lst
|
||||
return arg
|
||||
}
|
||||
|
||||
var funcArgsPool sync.Pool
|
||||
|
||||
func putFuncArgs(p *[_MAX_FUNCTION_ARG]Value) {
|
||||
funcArgsPool.Put(p)
|
||||
func returnArgs(p *[]Value) {
|
||||
valueArgsPool.Put(p)
|
||||
}
|
||||
|
||||
func getFuncArgs() *[_MAX_FUNCTION_ARG]Value {
|
||||
if p := funcArgsPool.Get(); p == nil {
|
||||
return new([_MAX_FUNCTION_ARG]Value)
|
||||
} else {
|
||||
return p.(*[_MAX_FUNCTION_ARG]Value)
|
||||
type aggregateFunc struct {
|
||||
next func() (struct{}, bool)
|
||||
stop func()
|
||||
ctx Context
|
||||
arg []Value
|
||||
}
|
||||
|
||||
func (a *aggregateFunc) Step(ctx Context, arg ...Value) {
|
||||
a.ctx = ctx
|
||||
a.arg = append(a.arg[:0], arg...)
|
||||
if _, more := a.next(); !more {
|
||||
a.stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *aggregateFunc) Value(ctx Context) {
|
||||
a.ctx = ctx
|
||||
a.stop()
|
||||
}
|
||||
|
||||
func (a *aggregateFunc) Close() error {
|
||||
a.stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
57
func_seq_test.go
Normal file
57
func_seq_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package sqlite3_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"log"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
func ExampleConn_CreateAggregateFunction() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO test VALUES (1), (2), (3)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.CreateAggregateFunction("seq_avg", 1, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS,
|
||||
func(ctx *sqlite3.Context, seq iter.Seq[[]sqlite3.Value]) {
|
||||
count := 0
|
||||
total := 0.0
|
||||
for arg := range seq {
|
||||
total += arg[0].Float()
|
||||
count++
|
||||
}
|
||||
ctx.ResultFloat(total / float64(count))
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT seq_avg(col) FROM test`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for stmt.Step() {
|
||||
fmt.Println(stmt.ColumnFloat(0))
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// 2
|
||||
}
|
||||
17
go.mod
17
go.mod
@@ -1,23 +1,22 @@
|
||||
module github.com/ncruces/go-sqlite3
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/ncruces/julianday v1.0.0
|
||||
github.com/ncruces/sort v0.1.5
|
||||
github.com/tetratelabs/wazero v1.9.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/sys v0.30.0
|
||||
github.com/ncruces/sort v0.1.6
|
||||
github.com/ncruces/wbt v0.2.0
|
||||
github.com/tetratelabs/wazero v1.10.0
|
||||
golang.org/x/sys v0.38.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dchest/siphash v1.2.3 // ext/bloom
|
||||
github.com/google/uuid v1.6.0 // ext/uuid
|
||||
github.com/psanford/httpreadat v0.1.0 // example
|
||||
golang.org/x/sync v0.11.0 // test
|
||||
golang.org/x/text v0.22.0 // ext/unicode
|
||||
golang.org/x/crypto v0.43.0 // vfs/adiantum vfs/xts
|
||||
golang.org/x/sync v0.17.0 // test
|
||||
golang.org/x/text v0.30.0 // ext/unicode
|
||||
lukechampine.com/adiantum v1.1.1 // vfs/adiantum
|
||||
)
|
||||
|
||||
|
||||
26
go.sum
26
go.sum
@@ -4,19 +4,21 @@ 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.5 h1:fiFWXXAqKI8QckPf/6hu/bGFwcEPrirIOFaJqWujs4k=
|
||||
github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
|
||||
github.com/ncruces/sort v0.1.6 h1:TrsJfGRH1AoWoaeB4/+gCohot9+cA6u/INaH5agIhNk=
|
||||
github.com/ncruces/sort v0.1.6/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
|
||||
github.com/ncruces/wbt v0.2.0 h1:Q9zlKOBSZc7Yy/R2cGa35g6RKUUE3BjNIW3tfGC4F04=
|
||||
github.com/ncruces/wbt v0.2.0/go.mod h1:DtF92amvMxH69EmBFUSFWRDAlo6hOEfoNQnClxj9C/c=
|
||||
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.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
github.com/tetratelabs/wazero v1.10.0 h1:CXP3zneLDl6J4Zy8N/J+d5JsWKfrjE6GtvVK1fpnDlk=
|
||||
github.com/tetratelabs/wazero v1.10.0/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
|
||||
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=
|
||||
|
||||
@@ -209,8 +209,12 @@ func (d *ddl) renameTable(dst, src string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func compileConstraintRegexp(name string) *regexp.Regexp {
|
||||
return regexp.MustCompile("^(?i:CONSTRAINT)\\s+[\"`]?" + regexp.QuoteMeta(name) + "[\"`\\s]")
|
||||
}
|
||||
|
||||
func (d *ddl) addConstraint(name string, sql string) {
|
||||
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
|
||||
reg := compileConstraintRegexp(name)
|
||||
|
||||
for i := 0; i < len(d.fields); i++ {
|
||||
if reg.MatchString(d.fields[i]) {
|
||||
@@ -223,7 +227,7 @@ func (d *ddl) addConstraint(name string, sql string) {
|
||||
}
|
||||
|
||||
func (d *ddl) removeConstraint(name string) bool {
|
||||
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
|
||||
reg := compileConstraintRegexp(name)
|
||||
|
||||
for i := 0; i < len(d.fields); i++ {
|
||||
if reg.MatchString(d.fields[i]) {
|
||||
@@ -236,7 +240,7 @@ func (d *ddl) removeConstraint(name string) bool {
|
||||
|
||||
//lint:ignore U1000 ignore unused code.
|
||||
func (d *ddl) hasConstraint(name string) bool {
|
||||
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
|
||||
reg := compileConstraintRegexp(name)
|
||||
|
||||
for _, f := range d.fields {
|
||||
if reg.MatchString(f) {
|
||||
|
||||
@@ -95,7 +95,7 @@ func parseAllColumns(in string) ([]string, error) {
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected token: %s", string(s[i]))
|
||||
case parseAllColumnsState_State_End:
|
||||
break
|
||||
continue // avoid SA4011
|
||||
}
|
||||
}
|
||||
if state != parseAllColumnsState_State_End {
|
||||
|
||||
@@ -313,6 +313,41 @@ func TestRemoveConstraint(t *testing.T) {
|
||||
success: true,
|
||||
expect: []string{"`id` integer NOT NULL"},
|
||||
},
|
||||
{
|
||||
name: "lowercase",
|
||||
fields: []string{"`id` integer NOT NULL", "constraint `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
|
||||
cName: "fk_users_notes",
|
||||
success: true,
|
||||
expect: []string{"`id` integer NOT NULL"},
|
||||
},
|
||||
{
|
||||
name: "mixed_case",
|
||||
fields: []string{"`id` integer NOT NULL", "cOnsTraiNT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
|
||||
cName: "fk_users_notes",
|
||||
success: true,
|
||||
expect: []string{"`id` integer NOT NULL"},
|
||||
},
|
||||
{
|
||||
name: "newline",
|
||||
fields: []string{"`id` integer NOT NULL", "CONSTRAINT `fk_users_notes`\nFOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
|
||||
cName: "fk_users_notes",
|
||||
success: true,
|
||||
expect: []string{"`id` integer NOT NULL"},
|
||||
},
|
||||
{
|
||||
name: "lots_of_newlines",
|
||||
fields: []string{"`id` integer NOT NULL", "constraint \n fk_users_notes \n FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
|
||||
cName: "fk_users_notes",
|
||||
success: true,
|
||||
expect: []string{"`id` integer NOT NULL"},
|
||||
},
|
||||
{
|
||||
name: "no_backtick",
|
||||
fields: []string{"`id` integer NOT NULL", "CONSTRAINT fk_users_notes FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
|
||||
cName: "fk_users_notes",
|
||||
success: true,
|
||||
expect: []string{"`id` integer NOT NULL"},
|
||||
},
|
||||
{
|
||||
name: "check",
|
||||
fields: []string{"CONSTRAINT `name_checker` CHECK (`name` <> 'thetadev')", "`id` integer NOT NULL"},
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
module github.com/ncruces/go-sqlite3/gormlite
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/ncruces/go-sqlite3 v0.23.1
|
||||
gorm.io/gorm v1.25.12
|
||||
github.com/ncruces/go-sqlite3 v0.30.1
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
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.9.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.10.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.30.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.23.1 h1:zGAd76q+Tr18z/xKGatUlzBQdjR3J+rexfANUcjAgkY=
|
||||
github.com/ncruces/go-sqlite3 v0.23.1/go.mod h1:Xg3FyAZl25HcBSFmcbymdfoTqD7jRnBUmv1jSrbIjdE=
|
||||
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
|
||||
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
|
||||
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.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
github.com/tetratelabs/wazero v1.10.0 h1:CXP3zneLDl6J4Zy8N/J+d5JsWKfrjE6GtvVK1fpnDlk=
|
||||
github.com/tetratelabs/wazero v1.10.0/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
|
||||
@@ -339,7 +339,7 @@ func (m _Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) {
|
||||
indexes := make([]gorm.Index, 0)
|
||||
err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
rst := make([]*_Index, 0)
|
||||
if err := m.DB.Debug().Raw("SELECT * FROM PRAGMA_index_list(?)", stmt.Table).Scan(&rst).Error; err != nil { // alias `PRAGMA index_list(?)`
|
||||
if err := m.DB.Raw("SELECT * FROM PRAGMA_index_list(?)", stmt.Table).Scan(&rst).Error; err != nil { // alias `PRAGMA index_list(?)`
|
||||
return err
|
||||
}
|
||||
for _, index := range rst {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package gormlite provides a GORM driver for SQLite.
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
@@ -52,7 +51,9 @@ func (dialector _Dialector) Initialize(db *gorm.DB) (err error) {
|
||||
})
|
||||
|
||||
for k, v := range dialector.ClauseBuilders() {
|
||||
db.ClauseBuilders[k] = v
|
||||
if _, ok := db.ClauseBuilders[k]; !ok {
|
||||
db.ClauseBuilders[k] = v
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
)
|
||||
|
||||
func TestDialector(t *testing.T) {
|
||||
tmp := memdb.TestDB(t)
|
||||
dsn := memdb.TestDB(t)
|
||||
|
||||
// Custom connection with a custom function called "my_custom_function".
|
||||
db, err := driver.Open(tmp, func(conn *sqlite3.Conn) error {
|
||||
db, err := driver.Open(dsn, 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(tmp),
|
||||
dialector: Open(dsn),
|
||||
openSuccess: true,
|
||||
query: "SELECT 1",
|
||||
querySuccess: true,
|
||||
},
|
||||
{
|
||||
description: "Custom function",
|
||||
dialector: Open(tmp),
|
||||
dialector: Open(dsn),
|
||||
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.12 --filter=blob:none https://github.com/go-gorm/gorm.git
|
||||
git clone --branch v1.31.1 --filter=blob:none https://github.com/go-gorm/gorm.git
|
||||
mv gorm/tests tests
|
||||
rm -rf gorm/
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ diff --git a/tests/.gitignore b/tests/.gitignore
|
||||
diff --git a/tests/tests_test.go b/tests/tests_test.go
|
||||
--- a/tests/tests_test.go
|
||||
+++ b/tests/tests_test.go
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
+ _ "github.com/ncruces/go-sqlite3/embed"
|
||||
+ sqlite "github.com/ncruces/go-sqlite3/gormlite"
|
||||
+
|
||||
"gorm.io/driver/gaussdb"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
- "gorm.io/driver/sqlite"
|
||||
|
||||
@@ -3,13 +3,12 @@ set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/ddlmod.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/ddlmod_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/ddlmod_parse_all_columns.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/ddlmod_parse_all_columns_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/error_translator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/migrator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/sqlite.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/sqlite_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/sqlite_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod_parse_all_columns.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod_parse_all_columns_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/error_translator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/migrator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/sqlite.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/sqlite_test.go"
|
||||
curl -#L "https://github.com/glebarez/sqlite/raw/v1.11.0/sqlite_error_translator_test.go" > error_translator_test.go
|
||||
@@ -9,24 +9,31 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func NewMemory(_, max uint64) experimental.LinearMemory {
|
||||
func NewMemory(cap, max uint64) experimental.LinearMemory {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(unix.Getpagesize() - 1)
|
||||
max = (max + rnd) &^ rnd
|
||||
res := (max + rnd) &^ rnd
|
||||
|
||||
if max > math.MaxInt {
|
||||
// This ensures int(max) overflows to a negative value,
|
||||
if res > math.MaxInt {
|
||||
// This ensures int(res) overflows to a negative value,
|
||||
// and unix.Mmap returns EINVAL.
|
||||
max = math.MaxUint64
|
||||
res = math.MaxUint64
|
||||
}
|
||||
|
||||
// Reserve max bytes of address space, to ensure we won't need to move it.
|
||||
com := res
|
||||
prot := unix.PROT_READ | unix.PROT_WRITE
|
||||
if cap < max { // Commit memory only if cap=max.
|
||||
com = 0
|
||||
prot = unix.PROT_NONE
|
||||
}
|
||||
|
||||
// Reserve res bytes of address space, to ensure we won't need to move it.
|
||||
// A protected, private, anonymous mapping should not commit memory.
|
||||
b, err := unix.Mmap(-1, 0, int(max), unix.PROT_NONE, unix.MAP_PRIVATE|unix.MAP_ANON)
|
||||
b, err := unix.Mmap(-1, 0, int(res), prot, unix.MAP_PRIVATE|unix.MAP_ANON)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &mmappedMemory{buf: b[:0]}
|
||||
return &mmappedMemory{buf: b[:com]}
|
||||
}
|
||||
|
||||
// The slice covers the entire mmapped memory:
|
||||
@@ -40,9 +47,11 @@ func (m *mmappedMemory) Reallocate(size uint64) []byte {
|
||||
com := uint64(len(m.buf))
|
||||
res := uint64(cap(m.buf))
|
||||
if com < size && size <= res {
|
||||
// Round up to the page size.
|
||||
// Grow geometrically, round up to the page size.
|
||||
rnd := uint64(unix.Getpagesize() - 1)
|
||||
new := (size + rnd) &^ rnd
|
||||
new := com + com>>3
|
||||
new = min(max(size, new), res)
|
||||
new = (new + rnd) &^ rnd
|
||||
|
||||
// Commit additional memory up to new bytes.
|
||||
err := unix.Mprotect(m.buf[com:new], unix.PROT_READ|unix.PROT_WRITE)
|
||||
@@ -50,8 +59,7 @@ func (m *mmappedMemory) Reallocate(size uint64) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update committed memory.
|
||||
m.buf = m.buf[:new]
|
||||
m.buf = m.buf[:new] // Update committed memory.
|
||||
}
|
||||
// Limit returned capacity because bytes beyond
|
||||
// len(m.buf) have not yet been committed.
|
||||
|
||||
@@ -9,20 +9,26 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func NewMemory(_, max uint64) experimental.LinearMemory {
|
||||
func NewMemory(cap, max uint64) experimental.LinearMemory {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(windows.Getpagesize() - 1)
|
||||
max = (max + rnd) &^ rnd
|
||||
res := (max + rnd) &^ rnd
|
||||
|
||||
if max > math.MaxInt {
|
||||
// This ensures uintptr(max) overflows to a large value,
|
||||
if res > math.MaxInt {
|
||||
// This ensures uintptr(res) overflows to a large value,
|
||||
// and windows.VirtualAlloc returns an error.
|
||||
max = math.MaxUint64
|
||||
res = math.MaxUint64
|
||||
}
|
||||
|
||||
// Reserve max bytes of address space, to ensure we won't need to move it.
|
||||
// This does not commit memory.
|
||||
r, err := windows.VirtualAlloc(0, uintptr(max), windows.MEM_RESERVE, windows.PAGE_READWRITE)
|
||||
com := res
|
||||
kind := windows.MEM_COMMIT
|
||||
if cap < max { // Commit memory only if cap=max.
|
||||
com = 0
|
||||
kind = windows.MEM_RESERVE
|
||||
}
|
||||
|
||||
// Reserve res bytes of address space, to ensure we won't need to move it.
|
||||
r, err := windows.VirtualAlloc(0, uintptr(res), uint32(kind), windows.PAGE_READWRITE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -30,8 +36,9 @@ func NewMemory(_, max uint64) experimental.LinearMemory {
|
||||
mem := virtualMemory{addr: r}
|
||||
// SliceHeader, although deprecated, avoids a go vet warning.
|
||||
sh := (*reflect.SliceHeader)(unsafe.Pointer(&mem.buf))
|
||||
sh.Cap = int(max)
|
||||
sh.Data = r
|
||||
sh.Len = int(com)
|
||||
sh.Cap = int(res)
|
||||
return &mem
|
||||
}
|
||||
|
||||
@@ -47,9 +54,11 @@ func (m *virtualMemory) Reallocate(size uint64) []byte {
|
||||
com := uint64(len(m.buf))
|
||||
res := uint64(cap(m.buf))
|
||||
if com < size && size <= res {
|
||||
// Round up to the page size.
|
||||
// Grow geometrically, round up to the page size.
|
||||
rnd := uint64(windows.Getpagesize() - 1)
|
||||
new := (size + rnd) &^ rnd
|
||||
new := com + com>>3
|
||||
new = min(max(size, new), res)
|
||||
new = (new + rnd) &^ rnd
|
||||
|
||||
// Commit additional memory up to new bytes.
|
||||
_, err := windows.VirtualAlloc(m.addr, uintptr(new), windows.MEM_COMMIT, windows.PAGE_READWRITE)
|
||||
@@ -57,8 +66,7 @@ func (m *virtualMemory) Reallocate(size uint64) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update committed memory.
|
||||
m.buf = m.buf[:new]
|
||||
m.buf = m.buf[:new] // Update committed memory.
|
||||
}
|
||||
// Limit returned capacity because bytes beyond
|
||||
// len(m.buf) have not yet been committed.
|
||||
|
||||
@@ -39,6 +39,9 @@ const (
|
||||
ERROR_MISSING_COLLSEQ = ERROR | (1 << 8)
|
||||
ERROR_RETRY = ERROR | (2 << 8)
|
||||
ERROR_SNAPSHOT = ERROR | (3 << 8)
|
||||
ERROR_RESERVESIZE = ERROR | (4 << 8)
|
||||
ERROR_KEY = ERROR | (5 << 8)
|
||||
ERROR_UNABLE = ERROR | (6 << 8)
|
||||
IOERR_READ = IOERR | (1 << 8)
|
||||
IOERR_SHORT_READ = IOERR | (2 << 8)
|
||||
IOERR_WRITE = IOERR | (3 << 8)
|
||||
@@ -73,6 +76,8 @@ const (
|
||||
IOERR_DATA = IOERR | (32 << 8)
|
||||
IOERR_CORRUPTFS = IOERR | (33 << 8)
|
||||
IOERR_IN_PAGE = IOERR | (34 << 8)
|
||||
IOERR_BADKEY = IOERR | (35 << 8)
|
||||
IOERR_CODEC = IOERR | (36 << 8)
|
||||
LOCKED_SHAREDCACHE = LOCKED | (1 << 8)
|
||||
LOCKED_VTAB = LOCKED | (2 << 8)
|
||||
BUSY_RECOVERY = BUSY | (1 << 8)
|
||||
|
||||
@@ -33,8 +33,12 @@ func AssertErr() ErrorString {
|
||||
return ErrorString(msg)
|
||||
}
|
||||
|
||||
func ErrorCodeString(rc uint32) string {
|
||||
switch rc {
|
||||
type errorCode interface {
|
||||
~uint8 | ~uint16 | ~uint32 | ~int32
|
||||
}
|
||||
|
||||
func ErrorCodeString[T errorCode](rc T) string {
|
||||
switch uint32(rc) {
|
||||
case ABORT_ROLLBACK:
|
||||
return "sqlite3: abort due to ROLLBACK"
|
||||
case ROW:
|
||||
@@ -42,7 +46,7 @@ func ErrorCodeString(rc uint32) string {
|
||||
case DONE:
|
||||
return "sqlite3: no more rows available"
|
||||
}
|
||||
switch rc & 0xff {
|
||||
switch uint8(rc) {
|
||||
case OK:
|
||||
return "sqlite3: not an error"
|
||||
case ERROR:
|
||||
@@ -75,7 +79,7 @@ func ErrorCodeString(rc uint32) string {
|
||||
return "sqlite3: unable to open database file"
|
||||
case PROTOCOL:
|
||||
return "sqlite3: locking protocol"
|
||||
case FORMAT:
|
||||
case EMPTY:
|
||||
break
|
||||
case SCHEMA:
|
||||
return "sqlite3: database schema has changed"
|
||||
@@ -91,7 +95,7 @@ func ErrorCodeString(rc uint32) string {
|
||||
break
|
||||
case AUTH:
|
||||
return "sqlite3: authorization denied"
|
||||
case EMPTY:
|
||||
case FORMAT:
|
||||
break
|
||||
case RANGE:
|
||||
return "sqlite3: column index out of range"
|
||||
|
||||
@@ -20,20 +20,6 @@ func ExportFuncVI[T0 i32](mod wazero.HostModuleBuilder, name string, fn func(con
|
||||
Export(name)
|
||||
}
|
||||
|
||||
type funcVII[T0, T1 i32] func(context.Context, api.Module, T0, T1)
|
||||
|
||||
func (fn funcVII[T0, T1]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
_ = stack[1] // prevent bounds check on every slice access
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]))
|
||||
}
|
||||
|
||||
func ExportFuncVII[T0, T1 i32](mod wazero.HostModuleBuilder, name string, fn func(context.Context, api.Module, T0, T1)) {
|
||||
mod.NewFunctionBuilder().
|
||||
WithGoModuleFunction(funcVII[T0, T1](fn),
|
||||
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, nil).
|
||||
Export(name)
|
||||
}
|
||||
|
||||
type funcVIII[T0, T1, T2 i32] func(context.Context, api.Module, T0, T1, T2)
|
||||
|
||||
func (fn funcVIII[T0, T1, T2]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !goexperiment.jsonv2
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
|
||||
52
internal/util/json_v2.go
Normal file
52
internal/util/json_v2.go
Normal file
@@ -0,0 +1,52 @@
|
||||
//go:build goexperiment.jsonv2
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json/v2"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type JSON struct{ Value any }
|
||||
|
||||
func (j JSON) Scan(value any) error {
|
||||
var buf []byte
|
||||
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
buf = v
|
||||
case string:
|
||||
buf = unsafe.Slice(unsafe.StringData(v), len(v))
|
||||
case int64:
|
||||
buf = strconv.AppendInt(nil, v, 10)
|
||||
case float64:
|
||||
buf = AppendNumber(nil, v)
|
||||
case time.Time:
|
||||
buf = append(buf, '"')
|
||||
buf = v.AppendFormat(buf, time.RFC3339Nano)
|
||||
buf = append(buf, '"')
|
||||
case nil:
|
||||
buf = []byte("null")
|
||||
default:
|
||||
panic(AssertErr())
|
||||
}
|
||||
|
||||
return json.Unmarshal(buf, j.Value)
|
||||
}
|
||||
|
||||
func AppendNumber(dst []byte, f float64) []byte {
|
||||
switch {
|
||||
case math.IsNaN(f):
|
||||
dst = append(dst, "null"...)
|
||||
case math.IsInf(f, 1):
|
||||
dst = append(dst, "9.0e999"...)
|
||||
case math.IsInf(f, -1):
|
||||
dst = append(dst, "-9.0e999"...)
|
||||
default:
|
||||
return strconv.AppendFloat(dst, f, 'g', -1, 64)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
@@ -135,11 +135,10 @@ func ReadString(mod api.Module, ptr Ptr_t, maxlen int64) string {
|
||||
panic(RangeErr)
|
||||
}
|
||||
}
|
||||
if i := bytes.IndexByte(buf, 0); i < 0 {
|
||||
panic(NoNulErr)
|
||||
} else {
|
||||
if i := bytes.IndexByte(buf, 0); i >= 0 {
|
||||
return string(buf[:i])
|
||||
}
|
||||
panic(NoNulErr)
|
||||
}
|
||||
|
||||
func WriteBytes(mod api.Module, ptr Ptr_t, b []byte) {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
@@ -16,14 +14,21 @@ type MappedRegion struct {
|
||||
addr uintptr
|
||||
}
|
||||
|
||||
func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32) (*MappedRegion, error) {
|
||||
h, err := windows.CreateFileMapping(windows.Handle(f.Fd()), nil, windows.PAGE_READWRITE, 0, 0, nil)
|
||||
func MapRegion(f *os.File, offset int64, size int32) (*MappedRegion, error) {
|
||||
maxSize := offset + int64(size)
|
||||
h, err := windows.CreateFileMapping(
|
||||
windows.Handle(f.Fd()), nil, windows.PAGE_READWRITE,
|
||||
uint32(maxSize>>32), uint32(maxSize), nil)
|
||||
if h == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
const allocationGranularity = 64 * 1024
|
||||
align := offset % allocationGranularity
|
||||
offset -= align
|
||||
|
||||
a, err := windows.MapViewOfFile(h, windows.FILE_MAP_WRITE,
|
||||
uint32(offset>>32), uint32(offset), uintptr(size))
|
||||
uint32(offset>>32), uint32(offset), uintptr(size)+uintptr(align))
|
||||
if a == 0 {
|
||||
windows.CloseHandle(h)
|
||||
return nil, err
|
||||
@@ -32,9 +37,9 @@ func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, si
|
||||
ret := &MappedRegion{Handle: h, addr: a}
|
||||
// SliceHeader, although deprecated, avoids a go vet warning.
|
||||
sh := (*reflect.SliceHeader)(unsafe.Pointer(&ret.Data))
|
||||
sh.Data = a + uintptr(align)
|
||||
sh.Len = int(size)
|
||||
sh.Cap = int(size)
|
||||
sh.Data = a
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ type ConnKey struct{}
|
||||
|
||||
type moduleKey struct{}
|
||||
type moduleState struct {
|
||||
sysError error
|
||||
mmapState
|
||||
handleState
|
||||
}
|
||||
@@ -23,3 +24,20 @@ func NewContext(ctx context.Context) context.Context {
|
||||
ctx = context.WithValue(ctx, moduleKey{}, state)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func GetSystemError(ctx context.Context) error {
|
||||
// Test needed to simplify testing.
|
||||
s, ok := ctx.Value(moduleKey{}).(*moduleState)
|
||||
if ok {
|
||||
return s.sysError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetSystemError(ctx context.Context, err error) {
|
||||
// Test needed to simplify testing.
|
||||
s, ok := ctx.Value(moduleKey{}).(*moduleState)
|
||||
if ok {
|
||||
s.sysError = err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
package util
|
||||
|
||||
type Pointer[T any] struct{ Value T }
|
||||
|
||||
func (p Pointer[T]) unwrap() any { return p.Value }
|
||||
|
||||
type PointerUnwrap interface{ unwrap() any }
|
||||
|
||||
func UnwrapPointer(p PointerUnwrap) any {
|
||||
return p.unwrap()
|
||||
}
|
||||
type Pointer struct{ Value any }
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnwrapPointer(t *testing.T) {
|
||||
p := Pointer[float64]{Value: math.Pi}
|
||||
if got := UnwrapPointer(p); got != math.Pi {
|
||||
t.Errorf("want π, got %v", got)
|
||||
}
|
||||
}
|
||||
83
json.go
83
json.go
@@ -1,6 +1,13 @@
|
||||
//go:build !goexperiment.jsonv2
|
||||
|
||||
package sqlite3
|
||||
|
||||
import "github.com/ncruces/go-sqlite3/internal/util"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// JSON returns a value that can be used as an argument to
|
||||
// [database/sql.DB.Exec], [database/sql.Row.Scan] and similar methods to
|
||||
@@ -10,3 +17,77 @@ import "github.com/ncruces/go-sqlite3/internal/util"
|
||||
func JSON(value any) any {
|
||||
return util.JSON{Value: value}
|
||||
}
|
||||
|
||||
// ResultJSON sets the result of the function to the JSON encoding of value.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultJSON(value any) {
|
||||
err := json.NewEncoder(callbackWriter(func(p []byte) (int, error) {
|
||||
ctx.ResultRawText(p[:len(p)-1]) // remove the newline
|
||||
return 0, nil
|
||||
})).Encode(value)
|
||||
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
}
|
||||
|
||||
// BindJSON binds the JSON encoding of value to the prepared statement.
|
||||
// The leftmost SQL parameter has an index of 1.
|
||||
//
|
||||
// https://sqlite.org/c3ref/bind_blob.html
|
||||
func (s *Stmt) BindJSON(param int, value any) error {
|
||||
return json.NewEncoder(callbackWriter(func(p []byte) (int, error) {
|
||||
return 0, s.BindRawText(param, p[:len(p)-1]) // remove the newline
|
||||
})).Encode(value)
|
||||
}
|
||||
|
||||
// ColumnJSON parses the JSON-encoded value of the result column
|
||||
// and stores it in the value pointed to by ptr.
|
||||
// The leftmost column of the result set has the index 0.
|
||||
//
|
||||
// https://sqlite.org/c3ref/column_blob.html
|
||||
func (s *Stmt) ColumnJSON(col int, ptr any) error {
|
||||
var data []byte
|
||||
switch s.ColumnType(col) {
|
||||
case NULL:
|
||||
data = []byte("null")
|
||||
case TEXT:
|
||||
data = s.ColumnRawText(col)
|
||||
case BLOB:
|
||||
data = s.ColumnRawBlob(col)
|
||||
case INTEGER:
|
||||
data = strconv.AppendInt(nil, s.ColumnInt64(col), 10)
|
||||
case FLOAT:
|
||||
data = util.AppendNumber(nil, s.ColumnFloat(col))
|
||||
default:
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
return json.Unmarshal(data, ptr)
|
||||
}
|
||||
|
||||
// JSON parses a JSON-encoded value
|
||||
// and stores the result in the value pointed to by ptr.
|
||||
func (v Value) JSON(ptr any) error {
|
||||
var data []byte
|
||||
switch v.Type() {
|
||||
case NULL:
|
||||
data = []byte("null")
|
||||
case TEXT:
|
||||
data = v.RawText()
|
||||
case BLOB:
|
||||
data = v.RawBlob()
|
||||
case INTEGER:
|
||||
data = strconv.AppendInt(nil, v.Int64(), 10)
|
||||
case FLOAT:
|
||||
data = util.AppendNumber(nil, v.Float())
|
||||
default:
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
return json.Unmarshal(data, ptr)
|
||||
}
|
||||
|
||||
type callbackWriter func(p []byte) (int, error)
|
||||
|
||||
func (fn callbackWriter) Write(p []byte) (int, error) { return fn(p) }
|
||||
|
||||
113
json_v2.go
Normal file
113
json_v2.go
Normal file
@@ -0,0 +1,113 @@
|
||||
//go:build goexperiment.jsonv2
|
||||
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"encoding/json/v2"
|
||||
"strconv"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// JSON returns a value that can be used as an argument to
|
||||
// [database/sql.DB.Exec], [database/sql.Row.Scan] and similar methods to
|
||||
// store value as JSON, or decode JSON into value.
|
||||
// JSON should NOT be used with [Stmt.BindJSON], [Stmt.ColumnJSON],
|
||||
// [Value.JSON], or [Context.ResultJSON].
|
||||
func JSON(value any) any {
|
||||
return util.JSON{Value: value}
|
||||
}
|
||||
|
||||
// ResultJSON sets the result of the function to the JSON encoding of value.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultJSON(value any) {
|
||||
w := bytesWriter{sqlite: ctx.c.sqlite}
|
||||
if err := json.MarshalWrite(&w, value); err != nil {
|
||||
ctx.c.free(w.ptr)
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
ctx.c.call("sqlite3_result_text_go",
|
||||
stk_t(ctx.handle), stk_t(w.ptr), stk_t(len(w.buf)))
|
||||
}
|
||||
|
||||
// BindJSON binds the JSON encoding of value to the prepared statement.
|
||||
// The leftmost SQL parameter has an index of 1.
|
||||
//
|
||||
// https://sqlite.org/c3ref/bind_blob.html
|
||||
func (s *Stmt) BindJSON(param int, value any) error {
|
||||
w := bytesWriter{sqlite: s.c.sqlite}
|
||||
if err := json.MarshalWrite(&w, value); err != nil {
|
||||
s.c.free(w.ptr)
|
||||
return err
|
||||
}
|
||||
rc := res_t(s.c.call("sqlite3_bind_text_go",
|
||||
stk_t(s.handle), stk_t(param),
|
||||
stk_t(w.ptr), stk_t(len(w.buf))))
|
||||
return s.c.error(rc)
|
||||
}
|
||||
|
||||
// ColumnJSON parses the JSON-encoded value of the result column
|
||||
// and stores it in the value pointed to by ptr.
|
||||
// The leftmost column of the result set has the index 0.
|
||||
//
|
||||
// https://sqlite.org/c3ref/column_blob.html
|
||||
func (s *Stmt) ColumnJSON(col int, ptr any) error {
|
||||
var data []byte
|
||||
switch s.ColumnType(col) {
|
||||
case NULL:
|
||||
data = []byte("null")
|
||||
case TEXT:
|
||||
data = s.ColumnRawText(col)
|
||||
case BLOB:
|
||||
data = s.ColumnRawBlob(col)
|
||||
case INTEGER:
|
||||
data = strconv.AppendInt(nil, s.ColumnInt64(col), 10)
|
||||
case FLOAT:
|
||||
data = util.AppendNumber(nil, s.ColumnFloat(col))
|
||||
default:
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
return json.Unmarshal(data, ptr)
|
||||
}
|
||||
|
||||
// JSON parses a JSON-encoded value
|
||||
// and stores the result in the value pointed to by ptr.
|
||||
func (v Value) JSON(ptr any) error {
|
||||
var data []byte
|
||||
switch v.Type() {
|
||||
case NULL:
|
||||
data = []byte("null")
|
||||
case TEXT:
|
||||
data = v.RawText()
|
||||
case BLOB:
|
||||
data = v.RawBlob()
|
||||
case INTEGER:
|
||||
data = strconv.AppendInt(nil, v.Int64(), 10)
|
||||
case FLOAT:
|
||||
data = util.AppendNumber(nil, v.Float())
|
||||
default:
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
return json.Unmarshal(data, ptr)
|
||||
}
|
||||
|
||||
type bytesWriter struct {
|
||||
*sqlite
|
||||
buf []byte
|
||||
ptr ptr_t
|
||||
}
|
||||
|
||||
func (b *bytesWriter) Write(p []byte) (n int, err error) {
|
||||
if len(p) > cap(b.buf)-len(b.buf) {
|
||||
want := int64(len(b.buf)) + int64(len(p))
|
||||
grow := int64(cap(b.buf))
|
||||
grow += grow >> 1
|
||||
want = max(want, grow)
|
||||
b.ptr = b.realloc(b.ptr, want)
|
||||
b.buf = util.View(b.mod, b.ptr, want)[:len(b.buf)]
|
||||
}
|
||||
b.buf = append(b.buf, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user