mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-12 22:19:14 +00:00
Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc3a993c3e | ||
|
|
0c09dd89c2 | ||
|
|
31c5000875 | ||
|
|
8175407754 | ||
|
|
abfad02d95 | ||
|
|
f7c3fb8062 | ||
|
|
c3633dda35 | ||
|
|
f2d894194d | ||
|
|
e08c7b3adf | ||
|
|
66601dd3cb | ||
|
|
58b66b75f1 | ||
|
|
e0c6086aa9 | ||
|
|
9bc39c5b91 | ||
|
|
12193cedea | ||
|
|
71d95bf9d5 | ||
|
|
7e23100ff7 | ||
|
|
e32d8401fb | ||
|
|
503db60927 | ||
|
|
1227fa7a04 | ||
|
|
e455b5f729 | ||
|
|
2bb1c8c795 | ||
|
|
844fab4167 | ||
|
|
5ed4a6cb9d | ||
|
|
37f2145588 | ||
|
|
e17b3ef2c8 | ||
|
|
a75b8887db | ||
|
|
9f456fecb9 | ||
|
|
36bbd674c2 | ||
|
|
7f5ea54009 | ||
|
|
5f1d5727cd | ||
|
|
6fb259e2b9 | ||
|
|
301f6bc2bd | ||
|
|
9e112c54b0 | ||
|
|
270efcb4af | ||
|
|
8252198dd2 | ||
|
|
dff825ae81 | ||
|
|
aae732e530 | ||
|
|
2e3ba3949e | ||
|
|
a44690035f | ||
|
|
7e12105b22 | ||
|
|
987db177ad | ||
|
|
1469cb9f1a | ||
|
|
4ede2c7216 | ||
|
|
cf14f190b2 | ||
|
|
6ca92b035d | ||
|
|
2912adf226 | ||
|
|
b3f83a4392 | ||
|
|
1223c4fc80 | ||
|
|
e9d6509577 | ||
|
|
81dd786af7 | ||
|
|
a2253558ef | ||
|
|
466d14a9e0 | ||
|
|
ada7b3a906 | ||
|
|
eba73a87d9 | ||
|
|
38da27b5d1 | ||
|
|
23737a61ba | ||
|
|
af473c7519 | ||
|
|
a946c00f8e | ||
|
|
32153763a3 | ||
|
|
a57ce87157 | ||
|
|
81e7a94ca4 | ||
|
|
034b9a3b4d | ||
|
|
363b12ee4c | ||
|
|
90d6ec31b9 | ||
|
|
17f7840a83 | ||
|
|
b2e8636227 | ||
|
|
1ad1608228 | ||
|
|
9f284f0b26 | ||
|
|
df4e144e89 | ||
|
|
96074b24bf | ||
|
|
4d68f8976c | ||
|
|
f2545534af | ||
|
|
69e5cf706b | ||
|
|
75c1dbb052 | ||
|
|
64e2500ca8 | ||
|
|
0cd0f48365 | ||
|
|
c69ee0fe8d | ||
|
|
b9b489aae9 | ||
|
|
21de004779 | ||
|
|
9eec439d35 | ||
|
|
fefee692db | ||
|
|
f18561ee11 | ||
|
|
ace01b2927 | ||
|
|
89f750a6e9 | ||
|
|
d6aebe67cc | ||
|
|
714ea0e779 | ||
|
|
c900889848 | ||
|
|
50c8517603 | ||
|
|
c78d00dca0 | ||
|
|
ddfaf12cd8 | ||
|
|
368c900db8 | ||
|
|
e524fb185d | ||
|
|
cc7bacfb9c | ||
|
|
911e497891 | ||
|
|
3469460635 | ||
|
|
b5adcacec4 | ||
|
|
62d6712f82 | ||
|
|
d34e6197a8 | ||
|
|
f9e867be60 | ||
|
|
96c61a2f55 | ||
|
|
ac94a5406e | ||
|
|
34617e15f0 | ||
|
|
83b3f6ce0a | ||
|
|
f2c8aa0ddf | ||
|
|
63ea13e41e | ||
|
|
b1508bface | ||
|
|
1c6897c8e2 | ||
|
|
170e1dbebd | ||
|
|
25fc5a606a | ||
|
|
8b6c2b28fb | ||
|
|
e59e2ed2a2 | ||
|
|
505c6640c2 | ||
|
|
5b0a063bfe | ||
|
|
32931032d3 | ||
|
|
b7055ef04b | ||
|
|
167025f47a | ||
|
|
b4b50fc547 | ||
|
|
08f7764fe0 | ||
|
|
4e0b8aeaa8 | ||
|
|
d694e9718e | ||
|
|
90218c0d79 | ||
|
|
44c3f9b4e7 | ||
|
|
2526fc8444 | ||
|
|
d7376209ee | ||
|
|
83e2587596 | ||
|
|
6101debe28 | ||
|
|
06eaf41c4f | ||
|
|
9638976991 | ||
|
|
b631ff1add | ||
|
|
fdfaaa8cec | ||
|
|
6a2827f989 | ||
|
|
9d77322d50 | ||
|
|
c1915feb2e | ||
|
|
52f9af3ca0 | ||
|
|
2f90277165 | ||
|
|
356dd56e5f | ||
|
|
e2a2d447ce | ||
|
|
75190a6f98 | ||
|
|
35c5619880 | ||
|
|
b51234cc82 | ||
|
|
cf7b89d3c4 | ||
|
|
ff9f27a778 | ||
|
|
f26f1a17a9 |
23
.github/actions/lfs/action.yml
vendored
23
.github/actions/lfs/action.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Git LFS pull
|
||||
description: Cached Git LFS pull.
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Create LFS file list
|
||||
shell: bash
|
||||
run: git lfs ls-files --long | cut -d ' ' -f1 | sort > .lfs-assets-id
|
||||
|
||||
- name: Restore LFS cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .git/lfs/objects
|
||||
key: lfs-${{ hashFiles('.lfs-assets-id') }}
|
||||
restore-keys: lfs-
|
||||
enableCrossOsArchive: true
|
||||
|
||||
- name: Git LFS pull
|
||||
shell: bash
|
||||
run: |
|
||||
git lfs pull
|
||||
git lfs prune
|
||||
16
.github/actions/vmactions/template.yml
vendored
Normal file
16
.github/actions/vmactions/template.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: VM Actions matrix
|
||||
description: VM Actions matrix template
|
||||
|
||||
inputs:
|
||||
run:
|
||||
description: The CI command to run
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: ${VMACTIONS}
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
run: ${{inputs.run}}
|
||||
10
.github/workflows/build-test.sh
vendored
10
.github/workflows/build-test.sh
vendored
@@ -6,6 +6,10 @@ echo 'set -eu' > test.sh
|
||||
for p in $(go list ./...); do
|
||||
dir=".${p#github.com/ncruces/go-sqlite3}"
|
||||
name="$(basename "$p").test"
|
||||
(cd ${dir}; go test -c)
|
||||
[ -f "${dir}/${name}" ] && echo "(cd ${dir}; ./${name} ${TESTFLAGS})" >> test.sh
|
||||
done
|
||||
(cd ${dir}; go test -c ${BUILDFLAGS:-})
|
||||
[ -f "${dir}/${name}" ] && echo "(cd ${dir}; ./${name} ${TESTFLAGS:-})" >> test.sh
|
||||
done
|
||||
|
||||
if [[ -v VMACTIONS ]]; then
|
||||
envsubst < .github/actions/vmactions/template.yml > .github/actions/vmactions/action.yml
|
||||
fi
|
||||
27
.github/workflows/cross.sh
vendored
27
.github/workflows/cross.sh
vendored
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo android ; GOOS=android GOARCH=amd64 go build .
|
||||
echo darwin ; GOOS=darwin GOARCH=amd64 go build .
|
||||
echo dragonfly ; GOOS=dragonfly GOARCH=amd64 go build .
|
||||
echo freebsd ; GOOS=freebsd GOARCH=amd64 go build .
|
||||
echo illumos ; GOOS=illumos GOARCH=amd64 go build .
|
||||
echo ios ; GOOS=ios GOARCH=amd64 go build .
|
||||
echo linux ; GOOS=linux GOARCH=amd64 go build .
|
||||
echo netbsd ; GOOS=netbsd GOARCH=amd64 go build .
|
||||
echo openbsd ; GOOS=openbsd GOARCH=amd64 go build .
|
||||
echo plan9 ; GOOS=plan9 GOARCH=amd64 go build .
|
||||
echo solaris ; GOOS=solaris GOARCH=amd64 go build .
|
||||
echo windows ; GOOS=windows GOARCH=amd64 go build .
|
||||
echo aix ; GOOS=aix GOARCH=ppc64 go build .
|
||||
echo js ; GOOS=js GOARCH=wasm go build .
|
||||
echo wasip1 ; GOOS=wasip1 GOARCH=wasm go build .
|
||||
echo linux-flock ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_flock .
|
||||
echo linux-noshm ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_noshm .
|
||||
echo linux-nosys ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_nosys .
|
||||
echo darwin-flock ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_flock .
|
||||
echo darwin-noshm ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_noshm .
|
||||
echo darwin-nosys ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_nosys .
|
||||
echo windows-nosys ; GOOS=windows GOARCH=amd64 go build -tags sqlite3_nosys .
|
||||
echo freebsd-nosys ; GOOS=freebsd GOARCH=amd64 go build -tags sqlite3_nosys .
|
||||
echo solaris-flock ; GOOS=solaris GOARCH=amd64 go build -tags sqlite3_flock .
|
||||
16
.github/workflows/cross.yml
vendored
16
.github/workflows/cross.yml
vendored
@@ -1,16 +0,0 @@
|
||||
name: Cross compile
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Build
|
||||
run: .github/workflows/cross.sh
|
||||
16
.github/workflows/repro.sh
vendored
16
.github/workflows/repro.sh
vendored
@@ -2,14 +2,14 @@
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$OSTYPE" == "linux"* ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-linux.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_118/binaryen-version_118-x86_64-linux.tar.gz"
|
||||
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_121/binaryen-version_121-x86_64-linux.tar.gz"
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-macos.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_118/binaryen-version_118-x86_64-macos.tar.gz"
|
||||
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_121/binaryen-version_121-arm64-macos.tar.gz"
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-windows.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_118/binaryen-version_118-x86_64-windows.tar.gz"
|
||||
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_121/binaryen-version_121-x86_64-windows.tar.gz"
|
||||
fi
|
||||
|
||||
# Download tools
|
||||
@@ -27,8 +27,8 @@ embed/build.sh
|
||||
embed/bcw2/build.sh
|
||||
|
||||
# Download and build sqlite-createtable-parser
|
||||
util/vtabutil/parse/download.sh
|
||||
util/vtabutil/parse/build.sh
|
||||
util/sql3util/wasm/download.sh
|
||||
util/sql3util/wasm/build.sh
|
||||
|
||||
# Check diffs
|
||||
git diff --exit-code
|
||||
6
.github/workflows/repro.yml
vendored
6
.github/workflows/repro.yml
vendored
@@ -18,17 +18,15 @@ jobs:
|
||||
steps:
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: .github/workflows/repro.sh
|
||||
|
||||
- uses: actions/attest-build-provenance@v1
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
with:
|
||||
subject-path: |
|
||||
embed/sqlite3.wasm
|
||||
embed/bcw2/bcw2.wasm
|
||||
util/vtabutil/parse/sql3parse_table.wasm
|
||||
util/sql3util/wasm/sql3parse_table.wasm
|
||||
178
.github/workflows/test.yml
vendored
178
.github/workflows/test.yml
vendored
@@ -2,9 +2,17 @@ name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: [ 'main' ]
|
||||
paths:
|
||||
- '**.go'
|
||||
- '**.mod'
|
||||
- '**.wasm'
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: [ 'main' ]
|
||||
paths:
|
||||
- '**.go'
|
||||
- '**.mod'
|
||||
- '**.wasm'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -19,9 +27,6 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Git LFS pull
|
||||
uses: ./.github/actions/lfs
|
||||
|
||||
- name: Format
|
||||
run: gofmt -s -w . && git diff --exit-code
|
||||
if: matrix.os != 'windows-latest'
|
||||
@@ -36,7 +41,7 @@ jobs:
|
||||
run: go mod verify
|
||||
|
||||
- name: Vet
|
||||
run: go vet -tags vet ./...
|
||||
run: go vet ./...
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
@@ -46,24 +51,22 @@ jobs:
|
||||
|
||||
- name: Test BSD locks
|
||||
run: go test -v -tags sqlite3_flock ./...
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os != 'windows-latest'
|
||||
|
||||
- name: Test no shared memory
|
||||
run: go test -v -tags sqlite3_noshm ./...
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
||||
- name: Test no locks
|
||||
run: go test -v -tags sqlite3_nosys ./...
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- name: Test dot locks
|
||||
run: go test -v -tags sqlite3_dotlk ./...
|
||||
if: matrix.os != 'windows-latest'
|
||||
|
||||
- name: Test GORM
|
||||
shell: bash
|
||||
run: gormlite/test.sh
|
||||
|
||||
- name: Test modules
|
||||
shell: bash
|
||||
run: go test -v ./embed/bcw2/...
|
||||
|
||||
- name: Collect coverage
|
||||
run: |
|
||||
go install github.com/dave/courtney@latest
|
||||
courtney
|
||||
run: go run github.com/dave/courtney@latest
|
||||
if: |
|
||||
github.event_name == 'push' &&
|
||||
matrix.os == 'ubuntu-latest'
|
||||
@@ -77,60 +80,106 @@ jobs:
|
||||
github.event_name == 'push' &&
|
||||
matrix.os == 'ubuntu-latest'
|
||||
|
||||
test-intel:
|
||||
runs-on: macos-13
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Git LFS pull
|
||||
uses: ./.github/actions/lfs
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
test-bsd:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- name: freebsd
|
||||
version: '14.1'
|
||||
version: '14.2'
|
||||
flags: '-test.v'
|
||||
- name: openbsd
|
||||
version: '7.5'
|
||||
flags: '-test.v -test.short'
|
||||
- name: netbsd
|
||||
version: '10.0'
|
||||
flags: '-test.v'
|
||||
- name: freebsd
|
||||
arch: arm64
|
||||
version: '14.2'
|
||||
flags: '-test.v -test.short'
|
||||
- name: netbsd
|
||||
arch: arm64
|
||||
version: '10.0'
|
||||
flags: '-test.v -test.short'
|
||||
- name: openbsd
|
||||
version: '7.6'
|
||||
flags: '-test.v -test.short'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Git LFS pull
|
||||
uses: ./.github/actions/lfs
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GOOS: ${{ matrix.os.name }}
|
||||
GOARCH: ${{ matrix.os.arch }}
|
||||
TESTFLAGS: ${{ matrix.os.flags }}
|
||||
run: .github/workflows/build-test.sh
|
||||
|
||||
- name: Test
|
||||
uses: cross-platform-actions/action@v0.25.0
|
||||
uses: cross-platform-actions/action@v0.26.0
|
||||
with:
|
||||
operating_system: ${{ matrix.os.name }}
|
||||
architecture: ${{ matrix.os.arch }}
|
||||
version: ${{ matrix.os.version }}
|
||||
shell: bash
|
||||
run: . ./test.sh
|
||||
sync_files: runner-to-vm
|
||||
|
||||
test-vm:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- name: dragonfly
|
||||
action: 'vmactions/dragonflybsd-vm@v1'
|
||||
tflags: '-test.v'
|
||||
- name: illumos
|
||||
action: 'vmactions/omnios-vm@v1'
|
||||
tflags: '-test.v'
|
||||
- name: solaris
|
||||
action: 'vmactions/solaris-vm@v1'
|
||||
bflags: '-tags sqlite3_dotlk'
|
||||
tflags: '-test.v'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GOOS: ${{ matrix.os.name }}
|
||||
BUILDFLAGS: ${{ matrix.os.bflags }}
|
||||
TESTFLAGS: ${{ matrix.os.tflags }}
|
||||
VMACTIONS: ${{ matrix.os.action }}
|
||||
run: .github/workflows/build-test.sh
|
||||
|
||||
- name: Test
|
||||
uses: ./.github/actions/vmactions
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
run: . ./test.sh
|
||||
|
||||
test-wasip1:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: bytecodealliance/actions/wasmtime/setup@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Set path
|
||||
run: echo "$(go env GOROOT)/misc/wasm" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Test wasmtime
|
||||
env:
|
||||
GOOS: wasip1
|
||||
GOARCH: wasm
|
||||
GOWASIRUNTIME: wasmtime
|
||||
GOWASIRUNTIMEARGS: '--env CI=true'
|
||||
run: go test -v -short -tags sqlite3_dotlk -skip Example ./...
|
||||
|
||||
test-qemu:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
@@ -141,9 +190,6 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Git LFS pull
|
||||
uses: ./.github/actions/lfs
|
||||
|
||||
- name: Test 386 (32-bit)
|
||||
run: GOARCH=386 go test -v -short ./...
|
||||
|
||||
@@ -153,11 +199,14 @@ jobs:
|
||||
- name: Test riscv64 (interpreter)
|
||||
run: GOARCH=riscv64 go test -v -short ./...
|
||||
|
||||
- name: Test s390x (big-endian, z/OS demo)
|
||||
run: GOARCH=s390x go test -v -short -tags sqlite3_flock ./...
|
||||
- name: Test ppc64le (interpreter)
|
||||
run: GOARCH=ppc64le go test -v -short ./...
|
||||
|
||||
test-vm:
|
||||
runs-on: ubuntu-latest
|
||||
- name: Test s390x (big-endian)
|
||||
run: GOARCH=s390x go test -v -short -tags sqlite3_dotlk ./...
|
||||
|
||||
test-macintel:
|
||||
runs-on: macos-13
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
@@ -165,32 +214,5 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Git LFS pull
|
||||
uses: ./.github/actions/lfs
|
||||
|
||||
- name: Build illumos
|
||||
env:
|
||||
GOOS: illumos
|
||||
TESTFLAGS: '-test.v -test.short'
|
||||
run: .github/workflows/build-test.sh
|
||||
|
||||
- name: Test illumos
|
||||
uses: vmactions/omnios-vm@v1
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
run: . ./test.sh
|
||||
|
||||
- name: Build Solaris
|
||||
env:
|
||||
GOOS: solaris
|
||||
TESTFLAGS: '-test.v -test.short'
|
||||
run: .github/workflows/build-test.sh
|
||||
|
||||
- name: Test Solaris
|
||||
uses: vmactions/solaris-vm@v1
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
run: . ./test.sh
|
||||
continue-on-error: true
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
65
README.md
65
README.md
@@ -1,4 +1,4 @@
|
||||
# Go bindings to SQLite using Wazero
|
||||
# Go bindings to SQLite using wazero
|
||||
|
||||
[](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
|
||||
[](https://goreportcard.com/report/github.com/ncruces/go-sqlite3)
|
||||
@@ -10,7 +10,7 @@ as well as direct access to most of the [C SQLite API](https://sqlite.org/cintro
|
||||
|
||||
It wraps a [Wasm](https://webassembly.org/) [build](embed/) of SQLite,
|
||||
and uses [wazero](https://wazero.io/) as the runtime.\
|
||||
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ runtime dependencies [^1].
|
||||
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ direct dependencies.
|
||||
|
||||
### Getting started
|
||||
|
||||
@@ -41,43 +41,6 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
|
||||
- [`github.com/ncruces/go-sqlite3/gormlite`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/gormlite)
|
||||
provides a [GORM](https://gorm.io) driver.
|
||||
|
||||
### Extensions
|
||||
|
||||
- [`github.com/ncruces/go-sqlite3/ext/array`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/array)
|
||||
provides the [`array`](https://sqlite.org/carray.html) table-valued function.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/blobio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blobio)
|
||||
simplifies [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/bloom`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/bloom)
|
||||
provides a [Bloom filter](https://github.com/nalgeon/sqlean/issues/27#issuecomment-1002267134) virtual table.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/csv`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/csv)
|
||||
reads [comma-separated values](https://sqlite.org/csv.html).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/fileio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/fileio)
|
||||
reads, writes and lists files.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/hash`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/hash)
|
||||
provides cryptographic hash functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/lines`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/lines)
|
||||
reads data [line-by-line](https://github.com/asg017/sqlite-lines).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/pivot`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/pivot)
|
||||
creates [pivot tables](https://github.com/jakethaw/pivot_vtab).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/regexp`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/regexp)
|
||||
provides regular expression functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/statement`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/statement)
|
||||
creates [parameterized views](https://github.com/0x09/sqlite-statement-vtab).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/stats`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
|
||||
provides [statistics](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html) functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
|
||||
provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/uuid`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/uuid)
|
||||
generates [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
|
||||
maps multidimensional data to one dimension.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)
|
||||
wraps a VFS to offer encryption at rest.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/memdb`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb)
|
||||
implements an in-memory VFS.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/readervfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs)
|
||||
implements a VFS for immutable databases.
|
||||
|
||||
### Advanced features
|
||||
|
||||
- [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html)
|
||||
@@ -90,7 +53,11 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
|
||||
- [math functions](https://sqlite.org/lang_mathfunc.html)
|
||||
- [full-text search](https://sqlite.org/fts5.html)
|
||||
- [geospatial search](https://sqlite.org/geopoly.html)
|
||||
- [Unicode support](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
|
||||
- [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
|
||||
@@ -107,10 +74,10 @@ This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3
|
||||
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
|
||||
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
|
||||
|
||||
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Test-matrix) on
|
||||
Linux (amd64/arm64/386/riscv64/s390x), macOS (amd64/arm64),
|
||||
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), OpenBSD (amd64), NetBSD (amd64),
|
||||
illumos (amd64), and Solaris (amd64).
|
||||
DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64).
|
||||
|
||||
The Go VFS is tested by running SQLite's
|
||||
[mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c).
|
||||
@@ -123,12 +90,20 @@ Perfomance of the [`database/sql`](https://pkg.go.dev/database/sql) driver is
|
||||
The Wasm and VFS layers are also tested by running SQLite's
|
||||
[speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c).
|
||||
|
||||
### FAQ, issues, new features
|
||||
|
||||
For questions, please see [Discussions](https://github.com/ncruces/go-sqlite3/discussions/categories/q-a).
|
||||
|
||||
Also, post there if you used this driver for something interesting
|
||||
([_"Show and tell"_](https://github.com/ncruces/go-sqlite3/discussions/categories/show-and-tell)),
|
||||
have an [idea](https://github.com/ncruces/go-sqlite3/discussions/categories/ideas)…
|
||||
|
||||
The [Issue](https://github.com/ncruces/go-sqlite3/issues) tracker is for bugs we want fixed,
|
||||
and features we're working on, planning to work on, or asking for help with.
|
||||
|
||||
### Alternatives
|
||||
|
||||
- [`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)
|
||||
|
||||
[^1]: anything else you find in `go.mod` is either a test dependency,
|
||||
or needed by one of the extensions.
|
||||
|
||||
52
blob.go
52
blob.go
@@ -21,6 +21,8 @@ type Blob struct {
|
||||
bytes int64
|
||||
offset int64
|
||||
handle uint32
|
||||
bufptr uint32
|
||||
buflen int64
|
||||
}
|
||||
|
||||
var _ io.ReadWriteSeeker = &Blob{}
|
||||
@@ -29,7 +31,6 @@ 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) {
|
||||
c.checkInterrupt()
|
||||
defer c.arena.mark()()
|
||||
blobPtr := c.arena.new(ptrlen)
|
||||
dbPtr := c.arena.string(db)
|
||||
@@ -41,6 +42,7 @@ func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob,
|
||||
flags = 1
|
||||
}
|
||||
|
||||
c.checkInterrupt(c.handle)
|
||||
r := c.call("sqlite3_blob_open", uint64(c.handle),
|
||||
uint64(dbPtr), uint64(tablePtr), uint64(columnPtr),
|
||||
uint64(row), flags, uint64(blobPtr))
|
||||
@@ -66,7 +68,7 @@ func (b *Blob) Close() error {
|
||||
}
|
||||
|
||||
r := b.c.call("sqlite3_blob_close", uint64(b.handle))
|
||||
|
||||
b.c.free(b.bufptr)
|
||||
b.handle = 0
|
||||
return b.c.error(r)
|
||||
}
|
||||
@@ -86,17 +88,18 @@ func (b *Blob) Read(p []byte) (n int, err error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
avail := b.bytes - b.offset
|
||||
want := int64(len(p))
|
||||
avail := b.bytes - b.offset
|
||||
if want > avail {
|
||||
want = avail
|
||||
}
|
||||
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.new(uint64(want))
|
||||
if want > b.buflen {
|
||||
b.bufptr = b.c.realloc(b.bufptr, uint64(want))
|
||||
b.buflen = want
|
||||
}
|
||||
|
||||
r := b.c.call("sqlite3_blob_read", uint64(b.handle),
|
||||
uint64(ptr), uint64(want), uint64(b.offset))
|
||||
uint64(b.bufptr), uint64(want), uint64(b.offset))
|
||||
err = b.c.error(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -106,7 +109,7 @@ func (b *Blob) Read(p []byte) (n int, err error) {
|
||||
err = io.EOF
|
||||
}
|
||||
|
||||
copy(p, util.View(b.c.mod, ptr, uint64(want)))
|
||||
copy(p, util.View(b.c.mod, b.bufptr, uint64(want)))
|
||||
return int(want), err
|
||||
}
|
||||
|
||||
@@ -123,19 +126,20 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
|
||||
if want > avail {
|
||||
want = avail
|
||||
}
|
||||
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.new(uint64(want))
|
||||
if want > b.buflen {
|
||||
b.bufptr = b.c.realloc(b.bufptr, uint64(want))
|
||||
b.buflen = want
|
||||
}
|
||||
|
||||
for want > 0 {
|
||||
r := b.c.call("sqlite3_blob_read", uint64(b.handle),
|
||||
uint64(ptr), uint64(want), uint64(b.offset))
|
||||
uint64(b.bufptr), uint64(want), uint64(b.offset))
|
||||
err = b.c.error(r)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
mem := util.View(b.c.mod, ptr, uint64(want))
|
||||
mem := util.View(b.c.mod, b.bufptr, uint64(want))
|
||||
m, err := w.Write(mem[:want])
|
||||
b.offset += int64(m)
|
||||
n += int64(m)
|
||||
@@ -159,11 +163,15 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
|
||||
//
|
||||
// https://sqlite.org/c3ref/blob_write.html
|
||||
func (b *Blob) Write(p []byte) (n int, err error) {
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.bytes(p)
|
||||
want := int64(len(p))
|
||||
if want > b.buflen {
|
||||
b.bufptr = b.c.realloc(b.bufptr, uint64(want))
|
||||
b.buflen = want
|
||||
}
|
||||
util.WriteBytes(b.c.mod, b.bufptr, p)
|
||||
|
||||
r := b.c.call("sqlite3_blob_write", uint64(b.handle),
|
||||
uint64(ptr), uint64(len(p)), uint64(b.offset))
|
||||
uint64(b.bufptr), uint64(want), uint64(b.offset))
|
||||
err = b.c.error(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -187,16 +195,17 @@ func (b *Blob) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
if want < 1 {
|
||||
want = 1
|
||||
}
|
||||
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.new(uint64(want))
|
||||
if want > b.buflen {
|
||||
b.bufptr = b.c.realloc(b.bufptr, uint64(want))
|
||||
b.buflen = want
|
||||
}
|
||||
|
||||
for {
|
||||
mem := util.View(b.c.mod, ptr, uint64(want))
|
||||
mem := util.View(b.c.mod, b.bufptr, uint64(want))
|
||||
m, err := r.Read(mem[:want])
|
||||
if m > 0 {
|
||||
r := b.c.call("sqlite3_blob_write", uint64(b.handle),
|
||||
uint64(ptr), uint64(m), uint64(b.offset))
|
||||
uint64(b.bufptr), uint64(m), uint64(b.offset))
|
||||
err := b.c.error(r)
|
||||
if err != nil {
|
||||
return n, err
|
||||
@@ -244,6 +253,7 @@ 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)
|
||||
err := b.c.error(b.c.call("sqlite3_blob_reopen", uint64(b.handle), uint64(row)))
|
||||
b.bytes = int64(b.c.call("sqlite3_blob_bytes", uint64(b.handle)))
|
||||
b.offset = 0
|
||||
|
||||
167
config.go
167
config.go
@@ -2,10 +2,13 @@ package sqlite3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
// Config makes configuration changes to a database connection.
|
||||
@@ -15,8 +18,19 @@ import (
|
||||
//
|
||||
// https://sqlite.org/c3ref/db_config.html
|
||||
func (c *Conn) Config(op DBConfig, arg ...bool) (bool, error) {
|
||||
if op < DBCONFIG_ENABLE_FKEY || op > DBCONFIG_REVERSE_SCANORDER {
|
||||
return false, MISUSE
|
||||
}
|
||||
|
||||
// We need to call sqlite3_db_config, a variadic function.
|
||||
// We only support the `int int*` variants.
|
||||
// The int is a three-valued bool: -1 queries, 0/1 sets false/true.
|
||||
// The int* points to where new state will be written to.
|
||||
// The vararg is a pointer to an array containing these arguments:
|
||||
// an int and an int* pointing to that int.
|
||||
|
||||
defer c.arena.mark()()
|
||||
argsPtr := c.arena.new(2 * ptrlen)
|
||||
argsPtr := c.arena.new(intlen + ptrlen)
|
||||
|
||||
var flag int
|
||||
switch {
|
||||
@@ -57,24 +71,38 @@ func logCallback(ctx context.Context, mod api.Module, _, iCode, zMsg uint32) {
|
||||
}
|
||||
}
|
||||
|
||||
// Log writes a message into the error log established by [Conn.ConfigLog].
|
||||
//
|
||||
// https://sqlite.org/c3ref/log.html
|
||||
func (c *Conn) Log(code ExtendedErrorCode, format string, a ...any) {
|
||||
if c.log != nil {
|
||||
c.log(code, fmt.Sprintf(format, a...))
|
||||
}
|
||||
}
|
||||
|
||||
// FileControl allows low-level control of database files.
|
||||
// Only a subset of opcodes are supported.
|
||||
//
|
||||
// https://sqlite.org/c3ref/file_control.html
|
||||
func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, error) {
|
||||
defer c.arena.mark()()
|
||||
ptr := c.arena.new(max(ptrlen, intlen))
|
||||
|
||||
var schemaPtr uint32
|
||||
if schema != "" {
|
||||
schemaPtr = c.arena.string(schema)
|
||||
}
|
||||
|
||||
var rc uint64
|
||||
var res any
|
||||
switch op {
|
||||
default:
|
||||
return nil, MISUSE
|
||||
|
||||
case FCNTL_RESET_CACHE:
|
||||
r := c.call("sqlite3_file_control",
|
||||
rc = c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), 0)
|
||||
return nil, c.error(r)
|
||||
|
||||
case FCNTL_PERSIST_WAL, FCNTL_POWERSAFE_OVERWRITE:
|
||||
var flag int
|
||||
@@ -84,70 +112,69 @@ func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, erro
|
||||
case arg[0]:
|
||||
flag = 1
|
||||
}
|
||||
ptr := c.arena.new(4)
|
||||
util.WriteUint32(c.mod, ptr, uint32(flag))
|
||||
r := c.call("sqlite3_file_control",
|
||||
rc = c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
return util.ReadUint32(c.mod, ptr) != 0, c.error(r)
|
||||
res = util.ReadUint32(c.mod, ptr) != 0
|
||||
|
||||
case FCNTL_CHUNK_SIZE:
|
||||
ptr := c.arena.new(4)
|
||||
util.WriteUint32(c.mod, ptr, uint32(arg[0].(int)))
|
||||
r := c.call("sqlite3_file_control",
|
||||
rc = c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
return nil, c.error(r)
|
||||
|
||||
case FCNTL_RESERVE_BYTES:
|
||||
bytes := -1
|
||||
if len(arg) > 0 {
|
||||
bytes = arg[0].(int)
|
||||
}
|
||||
ptr := c.arena.new(4)
|
||||
util.WriteUint32(c.mod, ptr, uint32(bytes))
|
||||
r := c.call("sqlite3_file_control",
|
||||
rc = c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
return int(util.ReadUint32(c.mod, ptr)), c.error(r)
|
||||
res = int(util.ReadUint32(c.mod, ptr))
|
||||
|
||||
case FCNTL_DATA_VERSION:
|
||||
ptr := c.arena.new(4)
|
||||
r := c.call("sqlite3_file_control",
|
||||
rc = c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
return util.ReadUint32(c.mod, ptr), c.error(r)
|
||||
res = util.ReadUint32(c.mod, ptr)
|
||||
|
||||
case FCNTL_LOCKSTATE:
|
||||
ptr := c.arena.new(4)
|
||||
r := c.call("sqlite3_file_control",
|
||||
rc = c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
return vfs.LockLevel(util.ReadUint32(c.mod, ptr)), c.error(r)
|
||||
res = vfs.LockLevel(util.ReadUint32(c.mod, ptr))
|
||||
|
||||
case FCNTL_VFS_POINTER:
|
||||
ptr := c.arena.new(4)
|
||||
r := c.call("sqlite3_file_control",
|
||||
rc = c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
const zNameOffset = 16
|
||||
ptr = util.ReadUint32(c.mod, ptr)
|
||||
ptr = util.ReadUint32(c.mod, ptr+zNameOffset)
|
||||
name := util.ReadString(c.mod, ptr, _MAX_NAME)
|
||||
return vfs.Find(name), c.error(r)
|
||||
if rc == _OK {
|
||||
const zNameOffset = 16
|
||||
ptr = util.ReadUint32(c.mod, ptr)
|
||||
ptr = util.ReadUint32(c.mod, ptr+zNameOffset)
|
||||
name := util.ReadString(c.mod, ptr, _MAX_NAME)
|
||||
res = vfs.Find(name)
|
||||
}
|
||||
|
||||
case FCNTL_FILE_POINTER, FCNTL_JOURNAL_POINTER:
|
||||
ptr := c.arena.new(4)
|
||||
r := c.call("sqlite3_file_control",
|
||||
rc = c.call("sqlite3_file_control",
|
||||
uint64(c.handle), uint64(schemaPtr),
|
||||
uint64(op), uint64(ptr))
|
||||
const fileHandleOffset = 4
|
||||
ptr = util.ReadUint32(c.mod, ptr)
|
||||
ptr = util.ReadUint32(c.mod, ptr+fileHandleOffset)
|
||||
return util.GetHandle(c.ctx, ptr), c.error(r)
|
||||
if rc == _OK {
|
||||
const fileHandleOffset = 4
|
||||
ptr = util.ReadUint32(c.mod, ptr)
|
||||
ptr = util.ReadUint32(c.mod, ptr+fileHandleOffset)
|
||||
res = util.GetHandle(c.ctx, ptr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, MISUSE
|
||||
if err := c.error(rc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Limit allows the size of various constructs to be
|
||||
@@ -234,10 +261,10 @@ func traceCallback(ctx context.Context, mod api.Module, evt TraceEvent, pDB, pAr
|
||||
return rc
|
||||
}
|
||||
|
||||
// WalCheckpoint checkpoints a WAL database.
|
||||
// WALCheckpoint checkpoints a WAL database.
|
||||
//
|
||||
// https://sqlite.org/c3ref/wal_checkpoint_v2.html
|
||||
func (c *Conn) WalCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt int, err error) {
|
||||
func (c *Conn) WALCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt int, err error) {
|
||||
defer c.arena.mark()()
|
||||
nLogPtr := c.arena.new(ptrlen)
|
||||
nCkptPtr := c.arena.new(ptrlen)
|
||||
@@ -250,19 +277,19 @@ func (c *Conn) WalCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt in
|
||||
return nLog, nCkpt, c.error(r)
|
||||
}
|
||||
|
||||
// WalAutoCheckpoint configures WAL auto-checkpoints.
|
||||
// WALAutoCheckpoint configures WAL auto-checkpoints.
|
||||
//
|
||||
// https://sqlite.org/c3ref/wal_autocheckpoint.html
|
||||
func (c *Conn) WalAutoCheckpoint(pages int) error {
|
||||
func (c *Conn) WALAutoCheckpoint(pages int) error {
|
||||
r := c.call("sqlite3_wal_autocheckpoint", uint64(c.handle), uint64(pages))
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
// WalHook registers a callback function to be invoked
|
||||
// WALHook registers a callback function to be invoked
|
||||
// each time data is committed to a database in WAL mode.
|
||||
//
|
||||
// https://sqlite.org/c3ref/wal_hook.html
|
||||
func (c *Conn) WalHook(cb func(db *Conn, schema string, pages int) error) {
|
||||
func (c *Conn) WALHook(cb func(db *Conn, schema string, pages int) error) {
|
||||
var enable uint64
|
||||
if cb != nil {
|
||||
enable = 1
|
||||
@@ -284,7 +311,10 @@ func walCallback(ctx context.Context, mod api.Module, _, pDB, zSchema uint32, pa
|
||||
//
|
||||
// https://sqlite.org/c3ref/autovacuum_pages.html
|
||||
func (c *Conn) AutoVacuumPages(cb func(schema string, dbPages, freePages, bytesPerPage uint) uint) error {
|
||||
funcPtr := util.AddHandle(c.ctx, cb)
|
||||
var funcPtr uint32
|
||||
if cb != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, cb)
|
||||
}
|
||||
r := c.call("sqlite3_autovacuum_pages_go", uint64(c.handle), uint64(funcPtr))
|
||||
return c.error(r)
|
||||
}
|
||||
@@ -294,3 +324,60 @@ func autoVacuumCallback(ctx context.Context, mod api.Module, pApp, zSchema, nDbP
|
||||
schema := util.ReadString(mod, zSchema, _MAX_NAME)
|
||||
return uint32(fn(schema, uint(nDbPage), uint(nFreePage), uint(nBytePerPage)))
|
||||
}
|
||||
|
||||
// SoftHeapLimit imposes a soft limit on heap size.
|
||||
//
|
||||
// https://sqlite.org/c3ref/hard_heap_limit64.html
|
||||
func (c *Conn) SoftHeapLimit(n int64) int64 {
|
||||
return int64(c.call("sqlite3_soft_heap_limit64", uint64(n)))
|
||||
}
|
||||
|
||||
// HardHeapLimit imposes a hard limit on heap size.
|
||||
//
|
||||
// https://sqlite.org/c3ref/hard_heap_limit64.html
|
||||
func (c *Conn) HardHeapLimit(n int64) int64 {
|
||||
return int64(c.call("sqlite3_hard_heap_limit64", uint64(n)))
|
||||
}
|
||||
|
||||
// EnableChecksums enables checksums on a database.
|
||||
//
|
||||
// https://sqlite.org/cksumvfs.html
|
||||
func (c *Conn) EnableChecksums(schema string) error {
|
||||
r, err := c.FileControl(schema, FCNTL_RESERVE_BYTES)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r == 8 {
|
||||
// Correct value, enabled.
|
||||
return nil
|
||||
}
|
||||
if r == 0 {
|
||||
// Default value, enable.
|
||||
_, err = c.FileControl(schema, FCNTL_RESERVE_BYTES, 8)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r, err = c.FileControl(schema, FCNTL_RESERVE_BYTES)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if r != 8 {
|
||||
// Invalid value.
|
||||
return util.ErrorString("sqlite3: reserve bytes must be 8, is: " + strconv.Itoa(r.(int)))
|
||||
}
|
||||
|
||||
// VACUUM the database.
|
||||
if schema != "" {
|
||||
err = c.Exec(`VACUUM ` + QuoteIdentifier(schema))
|
||||
} else {
|
||||
err = c.Exec(`VACUUM`)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Checkpoint the WAL.
|
||||
_, _, err = c.WALCheckpoint(schema, CHECKPOINT_RESTART)
|
||||
return err
|
||||
}
|
||||
|
||||
157
conn.go
157
conn.go
@@ -4,13 +4,16 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
// Conn is a database connection handle.
|
||||
@@ -23,7 +26,7 @@ type Conn struct {
|
||||
interrupt context.Context
|
||||
pending *Stmt
|
||||
stmts []*Stmt
|
||||
busy func(int) bool
|
||||
busy func(context.Context, int) bool
|
||||
log func(xErrorCode, string)
|
||||
collation func(*Conn, string)
|
||||
wal func(*Conn, string, int) error
|
||||
@@ -34,17 +37,25 @@ type Conn struct {
|
||||
rollback func()
|
||||
arena arena
|
||||
|
||||
handle uint32
|
||||
busy1st time.Time
|
||||
busylst time.Time
|
||||
handle uint32
|
||||
}
|
||||
|
||||
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE], [OPEN_URI] and [OPEN_NOFOLLOW].
|
||||
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE] and [OPEN_URI].
|
||||
func Open(filename string) (*Conn, error) {
|
||||
return newConn(filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI|OPEN_NOFOLLOW)
|
||||
return newConn(context.Background(), filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
|
||||
}
|
||||
|
||||
// OpenContext is like [Open] but includes a context,
|
||||
// which is used to interrupt the process of opening the connectiton.
|
||||
func OpenContext(ctx context.Context, filename string) (*Conn, error) {
|
||||
return newConn(ctx, filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
|
||||
}
|
||||
|
||||
// OpenFlags opens an SQLite database file as specified by the filename argument.
|
||||
//
|
||||
// If none of the required flags is used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
|
||||
// If none of the required flags are used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
|
||||
// If a URI filename is used, PRAGMA statements to execute can be specified using "_pragma":
|
||||
//
|
||||
// sqlite3.Open("file:demo.db?_pragma=busy_timeout(10000)")
|
||||
@@ -54,25 +65,33 @@ func OpenFlags(filename string, flags OpenFlag) (*Conn, error) {
|
||||
if flags&(OPEN_READONLY|OPEN_READWRITE|OPEN_CREATE) == 0 {
|
||||
flags |= OPEN_READWRITE | OPEN_CREATE
|
||||
}
|
||||
return newConn(filename, flags)
|
||||
return newConn(context.Background(), filename, flags)
|
||||
}
|
||||
|
||||
type connKey struct{}
|
||||
|
||||
func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
|
||||
sqlite, err := instantiateSQLite()
|
||||
func newConn(ctx context.Context, filename string, flags OpenFlag) (res *Conn, _ error) {
|
||||
err := ctx.Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Conn{interrupt: ctx}
|
||||
c.sqlite, err = instantiateSQLite()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if conn == nil {
|
||||
sqlite.close()
|
||||
if res == nil {
|
||||
c.Close()
|
||||
c.sqlite.close()
|
||||
} else {
|
||||
c.interrupt = context.Background()
|
||||
}
|
||||
}()
|
||||
|
||||
c := &Conn{sqlite: sqlite}
|
||||
c.arena = c.newArena(1024)
|
||||
c.ctx = context.WithValue(c.ctx, connKey{}, c)
|
||||
c.arena = c.newArena(1024)
|
||||
c.handle, err = c.openDB(filename, flags)
|
||||
if err == nil {
|
||||
err = initExtensions(c)
|
||||
@@ -97,6 +116,7 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
|
||||
if flags|OPEN_URI != 0 && strings.HasPrefix(filename, "file:") {
|
||||
var pragmas strings.Builder
|
||||
if _, after, ok := strings.Cut(filename, "?"); ok {
|
||||
@@ -108,6 +128,7 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
||||
}
|
||||
}
|
||||
if pragmas.Len() != 0 {
|
||||
c.checkInterrupt(handle)
|
||||
pragmaPtr := c.arena.string(pragmas.String())
|
||||
r := c.call("sqlite3_exec", uint64(handle), uint64(pragmaPtr), 0, 0, 0)
|
||||
if err := c.sqlite.error(r, handle, pragmas.String()); err != nil {
|
||||
@@ -117,7 +138,6 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
@@ -159,10 +179,10 @@ func (c *Conn) Close() error {
|
||||
//
|
||||
// https://sqlite.org/c3ref/exec.html
|
||||
func (c *Conn) Exec(sql string) error {
|
||||
c.checkInterrupt()
|
||||
defer c.arena.mark()()
|
||||
sqlPtr := c.arena.string(sql)
|
||||
|
||||
c.checkInterrupt(c.handle)
|
||||
r := c.call("sqlite3_exec", uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
|
||||
return c.error(r, sql)
|
||||
}
|
||||
@@ -188,6 +208,7 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
|
||||
tailPtr := c.arena.new(ptrlen)
|
||||
sqlPtr := c.arena.string(sql)
|
||||
|
||||
c.checkInterrupt(c.handle)
|
||||
r := c.call("sqlite3_prepare_v3", uint64(c.handle),
|
||||
uint64(sqlPtr), uint64(len(sql)+1), uint64(flags),
|
||||
uint64(stmtPtr), uint64(tailPtr))
|
||||
@@ -300,8 +321,7 @@ func (c *Conn) ReleaseMemory() error {
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
// GetInterrupt gets the context set with [Conn.SetInterrupt],
|
||||
// or nil if none was set.
|
||||
// GetInterrupt gets the context set with [Conn.SetInterrupt].
|
||||
func (c *Conn) GetInterrupt() context.Context {
|
||||
return c.interrupt
|
||||
}
|
||||
@@ -321,9 +341,11 @@ func (c *Conn) GetInterrupt() context.Context {
|
||||
//
|
||||
// https://sqlite.org/c3ref/interrupt.html
|
||||
func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
|
||||
// Is it the same context?
|
||||
if ctx == c.interrupt {
|
||||
return ctx
|
||||
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
|
||||
@@ -332,33 +354,35 @@ func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
|
||||
defer c.arena.mark()()
|
||||
stmtPtr := c.arena.new(ptrlen)
|
||||
loopPtr := c.arena.string(`WITH RECURSIVE c(x) AS (VALUES(0) UNION ALL SELECT x FROM c) SELECT x FROM c`)
|
||||
c.call("sqlite3_prepare_v3", uint64(c.handle), uint64(loopPtr), math.MaxUint64, 0, uint64(stmtPtr), 0)
|
||||
c.call("sqlite3_prepare_v3", uint64(c.handle), uint64(loopPtr), math.MaxUint64,
|
||||
uint64(PREPARE_PERSISTENT), uint64(stmtPtr), 0)
|
||||
c.pending = &Stmt{c: c}
|
||||
c.pending.handle = util.ReadUint32(c.mod, stmtPtr)
|
||||
}
|
||||
|
||||
old = c.interrupt
|
||||
c.interrupt = ctx
|
||||
|
||||
if old != nil && old.Done() != nil && (ctx == nil || ctx.Err() == nil) {
|
||||
if old.Done() != nil && ctx.Err() == nil {
|
||||
c.pending.Reset()
|
||||
}
|
||||
if ctx != nil && ctx.Done() != nil {
|
||||
if ctx.Done() != nil {
|
||||
c.pending.Step()
|
||||
}
|
||||
return old
|
||||
}
|
||||
|
||||
func (c *Conn) checkInterrupt() {
|
||||
if c.interrupt != nil && c.interrupt.Err() != nil {
|
||||
c.call("sqlite3_interrupt", uint64(c.handle))
|
||||
func (c *Conn) checkInterrupt(handle uint32) {
|
||||
if c.interrupt.Err() != nil {
|
||||
c.call("sqlite3_interrupt", uint64(handle))
|
||||
}
|
||||
}
|
||||
|
||||
func progressCallback(ctx context.Context, mod api.Module, pDB uint32) (interrupt uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB &&
|
||||
c.interrupt != nil && c.interrupt.Err() != nil {
|
||||
interrupt = 1
|
||||
func progressCallback(ctx context.Context, mod api.Module, _ uint32) (interrupt uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok {
|
||||
if c.interrupt.Done() != nil {
|
||||
runtime.Gosched()
|
||||
}
|
||||
if c.interrupt.Err() != nil {
|
||||
interrupt = 1
|
||||
}
|
||||
}
|
||||
return interrupt
|
||||
}
|
||||
@@ -372,34 +396,29 @@ func (c *Conn) BusyTimeout(timeout time.Duration) error {
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
func timeoutCallback(ctx context.Context, mod api.Module, pDB uint32, count, tmout int32) (retry uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok &&
|
||||
(c.interrupt == nil || c.interrupt.Err() == nil) {
|
||||
const delays = "\x01\x02\x05\x0a\x0f\x14\x19\x19\x19\x32\x32\x64"
|
||||
const totals = "\x00\x01\x03\x08\x12\x21\x35\x4e\x67\x80\xb2\xe4"
|
||||
const ndelay = int32(len(delays) - 1)
|
||||
|
||||
var delay, prior int32
|
||||
if count <= ndelay {
|
||||
delay = int32(delays[count])
|
||||
prior = int32(totals[count])
|
||||
} else {
|
||||
delay = int32(delays[ndelay])
|
||||
prior = int32(totals[ndelay]) + delay*(count-ndelay)
|
||||
func timeoutCallback(ctx context.Context, mod api.Module, count, tmout int32) (retry uint32) {
|
||||
// https://fractaledmind.github.io/2024/04/15/sqlite-on-rails-the-how-and-why-of-optimal-performance/
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.interrupt.Err() == nil {
|
||||
switch {
|
||||
case count == 0:
|
||||
c.busy1st = time.Now()
|
||||
case time.Since(c.busy1st) >= time.Duration(tmout)*time.Millisecond:
|
||||
return 0
|
||||
}
|
||||
|
||||
if delay = min(delay, tmout-prior); delay > 0 {
|
||||
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||
retry = 1
|
||||
if time.Since(c.busylst) < time.Millisecond {
|
||||
const sleepIncrement = 2*1024*1024 - 1 // power of two, ~2ms
|
||||
time.Sleep(time.Duration(rand.Int63() & sleepIncrement))
|
||||
}
|
||||
c.busylst = time.Now()
|
||||
return 1
|
||||
}
|
||||
return retry
|
||||
return 0
|
||||
}
|
||||
|
||||
// BusyHandler registers a callback to handle [BUSY] errors.
|
||||
//
|
||||
// https://sqlite.org/c3ref/busy_handler.html
|
||||
func (c *Conn) BusyHandler(cb func(count int) (retry bool)) error {
|
||||
func (c *Conn) BusyHandler(cb func(ctx context.Context, count int) (retry bool)) error {
|
||||
var enable uint64
|
||||
if cb != nil {
|
||||
enable = 1
|
||||
@@ -413,9 +432,12 @@ func (c *Conn) BusyHandler(cb func(count int) (retry bool)) error {
|
||||
}
|
||||
|
||||
func busyCallback(ctx context.Context, mod api.Module, pDB uint32, count int32) (retry uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil &&
|
||||
(c.interrupt == nil || c.interrupt.Err() == nil) {
|
||||
if c.busy(int(count)) {
|
||||
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)) {
|
||||
retry = 1
|
||||
}
|
||||
}
|
||||
@@ -427,8 +449,8 @@ func busyCallback(ctx context.Context, mod api.Module, pDB uint32, count int32)
|
||||
// https://sqlite.org/c3ref/db_status.html
|
||||
func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int, err error) {
|
||||
defer c.arena.mark()()
|
||||
hiPtr := c.arena.new(4)
|
||||
curPtr := c.arena.new(4)
|
||||
hiPtr := c.arena.new(intlen)
|
||||
curPtr := c.arena.new(intlen)
|
||||
|
||||
var i uint64
|
||||
if reset {
|
||||
@@ -454,8 +476,8 @@ func (c *Conn) TableColumnMetadata(schema, table, column string) (declType, coll
|
||||
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)
|
||||
primaryKeyPtr := c.arena.new(ptrlen)
|
||||
if schema != "" {
|
||||
schemaPtr = c.arena.string(schema)
|
||||
}
|
||||
@@ -469,8 +491,12 @@ func (c *Conn) TableColumnMetadata(schema, table, column string) (declType, coll
|
||||
uint64(declTypePtr), uint64(collSeqPtr),
|
||||
uint64(notNullPtr), uint64(primaryKeyPtr), uint64(autoIncPtr))
|
||||
if err = c.error(r); err == nil && column != "" {
|
||||
declType = util.ReadString(c.mod, util.ReadUint32(c.mod, declTypePtr), _MAX_NAME)
|
||||
collSeq = util.ReadString(c.mod, util.ReadUint32(c.mod, collSeqPtr), _MAX_NAME)
|
||||
if ptr := util.ReadUint32(c.mod, declTypePtr); ptr != 0 {
|
||||
declType = util.ReadString(c.mod, ptr, _MAX_NAME)
|
||||
}
|
||||
if ptr := util.ReadUint32(c.mod, collSeqPtr); ptr != 0 {
|
||||
collSeq = util.ReadString(c.mod, ptr, _MAX_NAME)
|
||||
}
|
||||
notNull = util.ReadUint32(c.mod, notNullPtr) != 0
|
||||
autoInc = util.ReadUint32(c.mod, autoIncPtr) != 0
|
||||
primaryKey = util.ReadUint32(c.mod, primaryKeyPtr) != 0
|
||||
@@ -489,12 +515,3 @@ func (c *Conn) stmtsIter(yield func(*Stmt) bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DriverConn is implemented by the SQLite [database/sql] driver connection.
|
||||
//
|
||||
// It can be used to access SQLite features like [online backup].
|
||||
//
|
||||
// [online backup]: https://sqlite.org/backup.html
|
||||
type DriverConn interface {
|
||||
Raw() *Conn
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build (go1.23 || goexperiment.rangefunc) && !vet
|
||||
//go:build go1.23
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !(go1.23 || goexperiment.rangefunc) || vet
|
||||
//go:build !go1.23
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
14
const.go
14
const.go
@@ -7,15 +7,13 @@ const (
|
||||
_ROW = 100 /* sqlite3_step() has another row ready */
|
||||
_DONE = 101 /* sqlite3_step() has finished executing */
|
||||
|
||||
_UTF8 = 1
|
||||
|
||||
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
|
||||
_MAX_LENGTH = 1e9
|
||||
_MAX_SQL_LENGTH = 1e9
|
||||
_MAX_ALLOCATION_SIZE = 0x7ffffeff
|
||||
_MAX_FUNCTION_ARG = 100
|
||||
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
|
||||
_MAX_LENGTH = 1e9
|
||||
_MAX_SQL_LENGTH = 1e9
|
||||
_MAX_FUNCTION_ARG = 100
|
||||
|
||||
ptrlen = 4
|
||||
intlen = 4
|
||||
)
|
||||
|
||||
// ErrorCode is a result code that [Error.Code] might return.
|
||||
@@ -180,6 +178,7 @@ const (
|
||||
DETERMINISTIC FunctionFlag = 0x000000800
|
||||
DIRECTONLY FunctionFlag = 0x000080000
|
||||
INNOCUOUS FunctionFlag = 0x000200000
|
||||
SELFORDER1 FunctionFlag = 0x002000000
|
||||
// SUBTYPE FunctionFlag = 0x000100000
|
||||
// RESULT_SUBTYPE FunctionFlag = 0x001000000
|
||||
)
|
||||
@@ -248,6 +247,7 @@ const (
|
||||
DBCONFIG_TRUSTED_SCHEMA DBConfig = 1017
|
||||
DBCONFIG_STMT_SCANSTATUS DBConfig = 1018
|
||||
DBCONFIG_REVERSE_SCANORDER DBConfig = 1019
|
||||
// DBCONFIG_MAX DBConfig = 1019
|
||||
)
|
||||
|
||||
// FcntlOpcode are the available opcodes for [Conn.FileControl].
|
||||
|
||||
21
context.go
21
context.go
@@ -84,19 +84,18 @@ func (ctx Context) ResultFloat(value float64) {
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultText(value string) {
|
||||
ptr := ctx.c.newString(value)
|
||||
ctx.c.call("sqlite3_result_text64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
|
||||
uint64(ctx.c.freer), _UTF8)
|
||||
ctx.c.call("sqlite3_result_text_go",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)))
|
||||
}
|
||||
|
||||
// ResultRawText sets the text result of the function to a []byte.
|
||||
// Returning a nil slice is the same as calling [Context.ResultNull].
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultRawText(value []byte) {
|
||||
ptr := ctx.c.newBytes(value)
|
||||
ctx.c.call("sqlite3_result_text64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
|
||||
uint64(ctx.c.freer), _UTF8)
|
||||
ctx.c.call("sqlite3_result_text_go",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)))
|
||||
}
|
||||
|
||||
// ResultBlob sets the result of the function to a []byte.
|
||||
@@ -105,9 +104,8 @@ func (ctx Context) ResultRawText(value []byte) {
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultBlob(value []byte) {
|
||||
ptr := ctx.c.newBytes(value)
|
||||
ctx.c.call("sqlite3_result_blob64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
|
||||
uint64(ctx.c.freer))
|
||||
ctx.c.call("sqlite3_result_blob_go",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)))
|
||||
}
|
||||
|
||||
// ResultZeroBlob sets the result of the function to a zero-filled, length n BLOB.
|
||||
@@ -154,9 +152,8 @@ func (ctx Context) resultRFC3339Nano(value time.Time) {
|
||||
buf := util.View(ctx.c.mod, ptr, maxlen)
|
||||
buf = value.AppendFormat(buf[:0], time.RFC3339Nano)
|
||||
|
||||
ctx.c.call("sqlite3_result_text64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(buf)),
|
||||
uint64(ctx.c.freer), _UTF8)
|
||||
ctx.c.call("sqlite3_result_text_go",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(buf)))
|
||||
}
|
||||
|
||||
// ResultPointer sets the result of the function to NULL, just like [Context.ResultNull],
|
||||
|
||||
257
driver/driver.go
257
driver/driver.go
@@ -40,14 +40,14 @@
|
||||
// When using a custom time struct, you'll have to implement
|
||||
// [database/sql/driver.Valuer] and [database/sql.Scanner].
|
||||
//
|
||||
// The Value method should ideally serialise to a time [format] supported by SQLite.
|
||||
// The Value method should ideally encode to a time [format] supported by SQLite.
|
||||
// This ensures SQL date and time functions work as they should,
|
||||
// and that your schema works with other SQLite tools.
|
||||
// [sqlite3.TimeFormat.Encode] may help.
|
||||
//
|
||||
// The Scan method needs to take into account that the value it receives can be of differing types.
|
||||
// It can already be a [time.Time], if the driver decoded the value according to "_timefmt" rules.
|
||||
// Or it can be a: string, int64, float64, []byte, nil,
|
||||
// Or it can be a: string, int64, float64, []byte, or nil,
|
||||
// depending on the column type and what whoever wrote the value.
|
||||
// [sqlite3.TimeFormat.Decode] may help.
|
||||
//
|
||||
@@ -81,6 +81,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
@@ -107,17 +108,17 @@ func init() {
|
||||
// The second callback is called before the driver closes a connection.
|
||||
// The [sqlite3.Conn] can be used to execute queries, register functions, etc.
|
||||
func Open(dataSourceName string, fn ...func(*sqlite3.Conn) error) (*sql.DB, error) {
|
||||
var drv SQLite
|
||||
if len(fn) > 2 {
|
||||
return nil, sqlite3.MISUSE
|
||||
}
|
||||
var init, term func(*sqlite3.Conn) error
|
||||
if len(fn) > 1 {
|
||||
drv.term = fn[1]
|
||||
term = fn[1]
|
||||
}
|
||||
if len(fn) > 0 {
|
||||
drv.init = fn[0]
|
||||
init = fn[0]
|
||||
}
|
||||
c, err := drv.OpenConnector(dataSourceName)
|
||||
c, err := newConnector(dataSourceName, init, term)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -125,14 +126,16 @@ func Open(dataSourceName string, fn ...func(*sqlite3.Conn) error) (*sql.DB, erro
|
||||
}
|
||||
|
||||
// SQLite implements [database/sql/driver.Driver].
|
||||
type SQLite struct {
|
||||
init func(*sqlite3.Conn) error
|
||||
term func(*sqlite3.Conn) error
|
||||
}
|
||||
type SQLite struct{}
|
||||
|
||||
var (
|
||||
// Ensure these interfaces are implemented:
|
||||
_ driver.DriverContext = &SQLite{}
|
||||
)
|
||||
|
||||
// Open implements [database/sql/driver.Driver].
|
||||
func (d *SQLite) Open(name string) (driver.Conn, error) {
|
||||
c, err := d.newConnector(name)
|
||||
c, err := newConnector(name, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -141,11 +144,11 @@ func (d *SQLite) Open(name string) (driver.Conn, error) {
|
||||
|
||||
// OpenConnector implements [database/sql/driver.DriverContext].
|
||||
func (d *SQLite) OpenConnector(name string) (driver.Connector, error) {
|
||||
return d.newConnector(name)
|
||||
return newConnector(name, nil, nil)
|
||||
}
|
||||
|
||||
func (d *SQLite) newConnector(name string) (*connector, error) {
|
||||
c := connector{driver: d, name: name}
|
||||
func newConnector(name string, init, term func(*sqlite3.Conn) error) (*connector, error) {
|
||||
c := connector{name: name, init: init, term: term}
|
||||
|
||||
var txlock, timefmt string
|
||||
if strings.HasPrefix(name, "file:") {
|
||||
@@ -185,7 +188,8 @@ func (d *SQLite) newConnector(name string) (*connector, error) {
|
||||
}
|
||||
|
||||
type connector struct {
|
||||
driver *SQLite
|
||||
init func(*sqlite3.Conn) error
|
||||
term func(*sqlite3.Conn) error
|
||||
name string
|
||||
txLock string
|
||||
tmRead sqlite3.TimeFormat
|
||||
@@ -194,22 +198,22 @@ type connector struct {
|
||||
}
|
||||
|
||||
func (n *connector) Driver() driver.Driver {
|
||||
return n.driver
|
||||
return &SQLite{}
|
||||
}
|
||||
|
||||
func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) {
|
||||
c := &conn{
|
||||
txLock: n.txLock,
|
||||
tmRead: n.tmRead,
|
||||
tmWrite: n.tmWrite,
|
||||
}
|
||||
|
||||
c.Conn, err = sqlite3.Open(n.name)
|
||||
c.Conn, err = sqlite3.OpenContext(ctx, n.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if res == nil {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
@@ -223,17 +227,18 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if n.driver.init != nil {
|
||||
err = n.driver.init(c.Conn)
|
||||
if n.init != nil {
|
||||
err = n.init(c.Conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if n.pragmas || n.driver.init != nil {
|
||||
if n.pragmas || n.init != nil {
|
||||
s, _, err := c.Conn.Prepare(`PRAGMA query_only`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.Close()
|
||||
if s.Step() && s.ColumnBool(0) {
|
||||
c.readOnly = '1'
|
||||
} else {
|
||||
@@ -244,9 +249,9 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if n.driver.term != nil {
|
||||
if n.term != nil {
|
||||
err = c.Conn.Trace(sqlite3.TRACE_CLOSE, func(sqlite3.TraceEvent, any, any) error {
|
||||
return n.driver.term(c.Conn)
|
||||
return n.term(c.Conn)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -255,6 +260,38 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Conn is implemented by the SQLite [database/sql] driver connections.
|
||||
//
|
||||
// It can be used to access SQLite features like [online backup]:
|
||||
//
|
||||
// db, err := driver.Open("temp.db")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer db.Close()
|
||||
//
|
||||
// conn, err := db.Conn(context.TODO())
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer conn.Close()
|
||||
//
|
||||
// err = conn.Raw(func(driverConn any) error {
|
||||
// conn := driverConn.(driver.Conn)
|
||||
// return conn.Raw().Backup("main", "backup.db")
|
||||
// })
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// [online backup]: https://sqlite.org/backup.html
|
||||
type Conn interface {
|
||||
Raw() *sqlite3.Conn
|
||||
driver.Conn
|
||||
driver.ConnBeginTx
|
||||
driver.ConnPrepareContext
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
*sqlite3.Conn
|
||||
txLock string
|
||||
@@ -266,10 +303,8 @@ type conn struct {
|
||||
|
||||
var (
|
||||
// Ensure these interfaces are implemented:
|
||||
_ driver.ConnPrepareContext = &conn{}
|
||||
_ driver.ExecerContext = &conn{}
|
||||
_ driver.ConnBeginTx = &conn{}
|
||||
_ sqlite3.DriverConn = &conn{}
|
||||
_ Conn = &conn{}
|
||||
_ driver.ExecerContext = &conn{}
|
||||
)
|
||||
|
||||
func (c *conn) Raw() *sqlite3.Conn {
|
||||
@@ -345,7 +380,7 @@ func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, e
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tail != "" {
|
||||
if notWhitespace(tail) {
|
||||
s.Close()
|
||||
return nil, util.TailErr
|
||||
}
|
||||
@@ -432,6 +467,7 @@ func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (drive
|
||||
defer s.Stmt.Conn().SetInterrupt(old)
|
||||
|
||||
err = s.Stmt.Exec()
|
||||
s.Stmt.ClearBindings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -454,7 +490,7 @@ func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
|
||||
if arg.Name == "" {
|
||||
ids = append(ids, arg.Ordinal)
|
||||
} else {
|
||||
for _, prefix := range []string{":", "@", "$"} {
|
||||
for _, prefix := range [...]string{":", "@", "$"} {
|
||||
if id := s.Stmt.BindIndex(prefix + arg.Name); id != 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
@@ -488,9 +524,9 @@ func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
|
||||
default:
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -544,8 +580,29 @@ type rows struct {
|
||||
*stmt
|
||||
names []string
|
||||
types []string
|
||||
nulls []bool
|
||||
scans []scantype
|
||||
}
|
||||
|
||||
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{}
|
||||
)
|
||||
|
||||
func (r *rows) Close() error {
|
||||
r.Stmt.ClearBindings()
|
||||
return r.Stmt.Reset()
|
||||
@@ -554,27 +611,69 @@ func (r *rows) Close() error {
|
||||
func (r *rows) Columns() []string {
|
||||
if r.names == nil {
|
||||
count := r.Stmt.ColumnCount()
|
||||
r.names = make([]string, count)
|
||||
for i := range r.names {
|
||||
r.names[i] = r.Stmt.ColumnName(i)
|
||||
names := make([]string, count)
|
||||
for i := range names {
|
||||
names[i] = r.Stmt.ColumnName(i)
|
||||
}
|
||||
r.names = names
|
||||
}
|
||||
return r.names
|
||||
}
|
||||
|
||||
func (r *rows) loadColumnMetadata() {
|
||||
if r.nulls == nil {
|
||||
count := r.Stmt.ColumnCount()
|
||||
nulls := make([]bool, count)
|
||||
types := make([]string, count)
|
||||
scans := make([]scantype, count)
|
||||
for i := range nulls {
|
||||
if col := r.Stmt.ColumnOriginName(i); col != "" {
|
||||
types[i], _, nulls[i], _, _, _ = r.Stmt.Conn().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
|
||||
}
|
||||
}
|
||||
}
|
||||
r.nulls = nulls
|
||||
r.types = types
|
||||
r.scans = scans
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rows) declType(index int) string {
|
||||
if r.types == nil {
|
||||
count := r.Stmt.ColumnCount()
|
||||
r.types = make([]string, count)
|
||||
for i := range r.types {
|
||||
r.types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
|
||||
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 {
|
||||
decltype := r.declType(index)
|
||||
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]
|
||||
@@ -583,6 +682,58 @@ func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
|
||||
return strings.TrimSpace(decltype)
|
||||
}
|
||||
|
||||
func (r *rows) ColumnTypeNullable(index int) (nullable, ok bool) {
|
||||
r.loadColumnMetadata()
|
||||
if r.nulls[index] {
|
||||
return false, true
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
|
||||
r.loadColumnMetadata()
|
||||
scan := r.scans[index]
|
||||
|
||||
if r.Stmt.Busy() {
|
||||
// SQLite is dynamically typed and we now have a row.
|
||||
// Always use the type of the value itself,
|
||||
// unless the scan type is more specific
|
||||
// and can scan the actual value.
|
||||
val := scantype(r.Stmt.ColumnType(index))
|
||||
useValType := true
|
||||
switch {
|
||||
case scan == _TIME && val != _BLOB && val != _NULL:
|
||||
t := r.Stmt.ColumnTime(index, r.tmRead)
|
||||
useValType = t == time.Time{}
|
||||
case scan == _BOOL && val == _INT:
|
||||
i := r.Stmt.ColumnInt64(index)
|
||||
useValType = i != 0 && i != 1
|
||||
case scan == _BLOB && val == _NULL:
|
||||
useValType = false
|
||||
}
|
||||
if useValType {
|
||||
scan = val
|
||||
}
|
||||
}
|
||||
|
||||
switch scan {
|
||||
case _INT:
|
||||
return reflect.TypeOf(int64(0))
|
||||
case _REAL:
|
||||
return reflect.TypeOf(float64(0))
|
||||
case _TEXT:
|
||||
return reflect.TypeOf("")
|
||||
case _BLOB:
|
||||
return reflect.TypeOf([]byte{})
|
||||
case _BOOL:
|
||||
return reflect.TypeOf(false)
|
||||
case _TIME:
|
||||
return reflect.TypeOf(time.Time{})
|
||||
default:
|
||||
return reflect.TypeOf((*any)(nil)).Elem()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rows) Next(dest []driver.Value) error {
|
||||
old := r.Stmt.Conn().SetInterrupt(r.ctx)
|
||||
defer r.Stmt.Conn().SetInterrupt(old)
|
||||
@@ -595,31 +746,27 @@ func (r *rows) Next(dest []driver.Value) error {
|
||||
}
|
||||
|
||||
data := unsafe.Slice((*any)(unsafe.SliceData(dest)), len(dest))
|
||||
err := r.Stmt.Columns(data)
|
||||
err := r.Stmt.Columns(data...)
|
||||
for i := range dest {
|
||||
if t, ok := r.decodeTime(i, dest[i]); ok {
|
||||
dest[i] = t
|
||||
continue
|
||||
}
|
||||
if s, ok := dest[i].(string); ok {
|
||||
t, ok := maybeTime(s)
|
||||
if ok {
|
||||
dest[i] = t
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rows) decodeTime(i int, v any) (_ time.Time, ok bool) {
|
||||
switch r.tmRead {
|
||||
case sqlite3.TimeFormatDefault, time.RFC3339Nano:
|
||||
// handled by maybeTime
|
||||
return
|
||||
}
|
||||
switch v.(type) {
|
||||
case int64, float64, string:
|
||||
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
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"math"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -224,8 +225,8 @@ func Test_Prepare(t *testing.T) {
|
||||
}
|
||||
|
||||
_, err = db.Prepare(`SELECT 1; `)
|
||||
if err.Error() != string(util.TailErr) {
|
||||
t.Error("want tailErr")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = db.Prepare(`SELECT 1; SELECT`)
|
||||
@@ -365,3 +366,104 @@ func Test_time(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ColumnType_ScanType(t *testing.T) {
|
||||
var (
|
||||
INT = reflect.TypeOf(int64(0))
|
||||
REAL = reflect.TypeOf(float64(0))
|
||||
TEXT = reflect.TypeOf("")
|
||||
BLOB = reflect.TypeOf([]byte{})
|
||||
BOOL = reflect.TypeOf(false)
|
||||
TIME = reflect.TypeOf(time.Time{})
|
||||
ANY = reflect.TypeOf((*any)(nil)).Elem()
|
||||
)
|
||||
|
||||
t.Parallel()
|
||||
tmp := memdb.TestDB(t)
|
||||
|
||||
db, err := sql.Open("sqlite3", tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE test (
|
||||
col_int INTEGER,
|
||||
col_real REAL,
|
||||
col_text TEXT,
|
||||
col_blob BLOB,
|
||||
col_bool BOOLEAN,
|
||||
col_time DATETIME,
|
||||
col_decimal DECIMAL
|
||||
);
|
||||
INSERT INTO test VALUES
|
||||
(1, 1, 1, 1, 1, 1, 1),
|
||||
(2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0),
|
||||
('1', '1', '1', '1', '1', '1', '1'),
|
||||
('x', 'x', 'x', 'x', 'x', 'x', 'x'),
|
||||
(x'', x'', x'', x'', x'', x'', x''),
|
||||
('2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z',
|
||||
'2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z'),
|
||||
(TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
|
||||
(NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rows, err := db.Query(`SELECT * FROM test`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
cols, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := [][]reflect.Type{
|
||||
{INT, REAL, TEXT, BLOB, BOOL, TIME, ANY},
|
||||
{INT, REAL, TEXT, INT, BOOL, TIME, INT},
|
||||
{INT, REAL, TEXT, REAL, INT, TIME, INT},
|
||||
{INT, REAL, TEXT, TEXT, BOOL, TIME, INT},
|
||||
{TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT},
|
||||
{BLOB, BLOB, BLOB, BLOB, BLOB, BLOB, BLOB},
|
||||
{TEXT, TEXT, TEXT, TEXT, TEXT, TIME, TEXT},
|
||||
{INT, REAL, TEXT, INT, BOOL, TIME, INT},
|
||||
{ANY, ANY, ANY, BLOB, ANY, ANY, ANY},
|
||||
}
|
||||
for j, c := range cols {
|
||||
got := c.ScanType()
|
||||
if got != want[0][j] {
|
||||
t.Errorf("want %v, got %v, at column %d", want[0][j], got, j)
|
||||
}
|
||||
}
|
||||
|
||||
dest := make([]any, len(cols))
|
||||
for i := 1; rows.Next(); i++ {
|
||||
cols, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for j, c := range cols {
|
||||
got := c.ScanType()
|
||||
if got != want[i][j] {
|
||||
t.Errorf("want %v, got %v, at row %d column %d", want[i][j], got, i, j)
|
||||
}
|
||||
dest[j] = reflect.New(got).Interface()
|
||||
}
|
||||
|
||||
err = rows.Scan(dest...)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
144
driver/example2_test.go
Normal file
144
driver/example2_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
//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"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func Example_customTime() {
|
||||
db, err := sql.Open("sqlite3", "file:/time.db?vfs=memdb")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE data (
|
||||
id INTEGER PRIMARY KEY,
|
||||
date_time TEXT
|
||||
) STRICT;
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// This one will be returned as string to [sql.Scanner] because it doesn't
|
||||
// pass the driver's round-trip test when it tries to figure out if it's
|
||||
// a time. 2009-11-17T20:34:58.650Z goes in, but parsing and formatting
|
||||
// it with [time.RFC3338Nano] results in 2009-11-17T20:34:58.65Z. Though
|
||||
// the times are identical, the trailing zero is lost in the string
|
||||
// representation so the driver considers the conversion unsuccessful.
|
||||
c1 := CustomTime{time.Date(
|
||||
2009, 11, 17, 20, 34, 58, 650000000, time.UTC)}
|
||||
|
||||
// Store our custom time in the database.
|
||||
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var strc1 string
|
||||
// Retrieve it as a string, the result of Value().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&strc1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("in db:", strc1)
|
||||
|
||||
var resc1 CustomTime
|
||||
// Retrieve it as our custom time type, going through Scan().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&resc1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("custom time:", resc1)
|
||||
|
||||
// This one will be returned as [time.Time] to [sql.Scanner] because it does
|
||||
// pass the driver's round-trip test when it tries to figure out if it's
|
||||
// a time. 2009-11-17T20:34:58.651Z goes in, and parsing and formatting
|
||||
// it with [time.RFC3339Nano] results in 2009-11-17T20:34:58.651Z.
|
||||
c2 := CustomTime{time.Date(
|
||||
2009, 11, 17, 20, 34, 58, 651000000, time.UTC)}
|
||||
// Store our custom time in the database.
|
||||
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var strc2 string
|
||||
// Retrieve it as a string, the result of Value().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&strc2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("in db:", strc2)
|
||||
|
||||
var resc2 CustomTime
|
||||
// Retrieve it as our custom time type, going through Scan().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&resc2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("custom time:", resc2)
|
||||
// Output:
|
||||
// in db: 2009-11-17T20:34:58.650Z
|
||||
// scan type string: 2009-11-17T20:34:58.650Z
|
||||
// custom time: 2009-11-17 20:34:58.65 +0000 UTC
|
||||
// in db: 2009-11-17T20:34:58.651Z
|
||||
// scan type time: 2009-11-17 20:34:58.651 +0000 UTC
|
||||
// custom time: 2009-11-17 20:34:58.651 +0000 UTC
|
||||
}
|
||||
|
||||
type CustomTime struct{ time.Time }
|
||||
|
||||
func (c CustomTime) Value() (driver.Value, error) {
|
||||
return sqlite3.TimeFormat7TZ.Encode(c.UTC()), nil
|
||||
}
|
||||
|
||||
func (c *CustomTime) Scan(value any) error {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
*c = CustomTime{time.Time{}}
|
||||
case time.Time:
|
||||
fmt.Println("scan type time:", v)
|
||||
*c = CustomTime{v}
|
||||
case string:
|
||||
fmt.Println("scan type string:", v)
|
||||
t, err := sqlite3.TimeFormat7TZ.Decode(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*c = CustomTime{t}
|
||||
default:
|
||||
panic("unsupported value type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build (linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock) && !sqlite3_nosys
|
||||
//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk
|
||||
|
||||
package driver_test
|
||||
|
||||
@@ -6,13 +6,10 @@ package driver_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
@@ -153,129 +150,3 @@ func addAlbum(alb Album) (int64, error) {
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func Example_customTime() {
|
||||
db, err := sql.Open("sqlite3", "file:/time.db?vfs=memdb")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE data (
|
||||
id INTEGER PRIMARY KEY,
|
||||
date_time TEXT
|
||||
) STRICT;
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// This one will be returned as string to [sql.Scanner] because it doesn't
|
||||
// pass the driver's round-trip test when it tries to figure out if it's
|
||||
// a time. 2009-11-17T20:34:58.650Z goes in, but parsing and formatting
|
||||
// it with [time.RFC3338Nano] results in 2009-11-17T20:34:58.65Z. Though
|
||||
// the times are identical, the trailing zero is lost in the string
|
||||
// representation so the driver considers the conversion unsuccessful.
|
||||
c1 := CustomTime{time.Date(
|
||||
2009, 11, 17, 20, 34, 58, 650000000, time.UTC)}
|
||||
|
||||
// Store our custom time in the database.
|
||||
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var strc1 string
|
||||
// Retrieve it as a string, the result of Value().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&strc1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("in db:", strc1)
|
||||
|
||||
var resc1 CustomTime
|
||||
// Retrieve it as our custom time type, going through Scan().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&resc1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("custom time:", resc1)
|
||||
|
||||
// This one will be returned as [time.Time] to [sql.Scanner] because it does
|
||||
// pass the driver's round-trip test when it tries to figure out if it's
|
||||
// a time. 2009-11-17T20:34:58.651Z goes in, and parsing and formatting
|
||||
// it with [time.RFC3339Nano] results in 2009-11-17T20:34:58.651Z.
|
||||
c2 := CustomTime{time.Date(
|
||||
2009, 11, 17, 20, 34, 58, 651000000, time.UTC)}
|
||||
// Store our custom time in the database.
|
||||
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var strc2 string
|
||||
// Retrieve it as a string, the result of Value().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&strc2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("in db:", strc2)
|
||||
|
||||
var resc2 CustomTime
|
||||
// Retrieve it as our custom time type, going through Scan().
|
||||
err = db.QueryRow(`
|
||||
SELECT date_time
|
||||
FROM data
|
||||
WHERE id = last_insert_rowid()
|
||||
`).Scan(&resc2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("custom time:", resc2)
|
||||
// Output:
|
||||
// in db: 2009-11-17T20:34:58.650Z
|
||||
// scan type string: 2009-11-17T20:34:58.650Z
|
||||
// custom time: 2009-11-17 20:34:58.65 +0000 UTC
|
||||
// in db: 2009-11-17T20:34:58.651Z
|
||||
// scan type time: 2009-11-17 20:34:58.651 +0000 UTC
|
||||
// custom time: 2009-11-17 20:34:58.651 +0000 UTC
|
||||
}
|
||||
|
||||
type CustomTime struct{ time.Time }
|
||||
|
||||
func (c CustomTime) Value() (driver.Value, error) {
|
||||
return sqlite3.TimeFormat7TZ.Encode(c.UTC()), nil
|
||||
}
|
||||
|
||||
func (c *CustomTime) Scan(value any) error {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
*c = CustomTime{time.Time{}}
|
||||
case time.Time:
|
||||
fmt.Println("scan type time:", v)
|
||||
*c = CustomTime{v}
|
||||
case string:
|
||||
fmt.Println("scan type string:", v)
|
||||
t, err := sqlite3.TimeFormat7TZ.Decode(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*c = CustomTime{t}
|
||||
default:
|
||||
panic("unsupported value type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
import "time"
|
||||
|
||||
// Convert a string in [time.RFC3339Nano] format into a [time.Time]
|
||||
// if it roundtrips back to the same string.
|
||||
|
||||
@@ -27,12 +27,12 @@ func Fuzz_stringOrTime_1(f *testing.F) {
|
||||
// Make sure times round-trip to the same string:
|
||||
// https://pkg.go.dev/database/sql#Rows.Scan
|
||||
if v.Format(time.RFC3339Nano) != str {
|
||||
t.Fatalf("did not round-trip: %q", str)
|
||||
t.Errorf("did not round-trip: %q", str)
|
||||
}
|
||||
} else {
|
||||
date, err := time.Parse(time.RFC3339Nano, str)
|
||||
if err == nil && date.Format(time.RFC3339Nano) == str {
|
||||
t.Fatalf("would round-trip: %q", str)
|
||||
t.Errorf("would round-trip: %q", str)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,3 +12,63 @@ func namedValues(args []driver.Value) []driver.NamedValue {
|
||||
}
|
||||
return named
|
||||
}
|
||||
|
||||
func notWhitespace(sql string) bool {
|
||||
const (
|
||||
code = iota
|
||||
slash
|
||||
minus
|
||||
ccomment
|
||||
sqlcomment
|
||||
endcomment
|
||||
)
|
||||
|
||||
state := code
|
||||
for _, b := range ([]byte)(sql) {
|
||||
if b == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
switch state {
|
||||
case code:
|
||||
switch b {
|
||||
case '/':
|
||||
state = slash
|
||||
case '-':
|
||||
state = minus
|
||||
case ' ', ';', '\t', '\n', '\v', '\f', '\r':
|
||||
continue
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case slash:
|
||||
if b != '*' {
|
||||
return true
|
||||
}
|
||||
state = ccomment
|
||||
case minus:
|
||||
if b != '-' {
|
||||
return true
|
||||
}
|
||||
state = sqlcomment
|
||||
case ccomment:
|
||||
if b == '*' {
|
||||
state = endcomment
|
||||
}
|
||||
case sqlcomment:
|
||||
if b == '\n' {
|
||||
state = code
|
||||
}
|
||||
case endcomment:
|
||||
switch b {
|
||||
case '/':
|
||||
state = code
|
||||
case '*':
|
||||
state = endcomment
|
||||
default:
|
||||
state = ccomment
|
||||
}
|
||||
}
|
||||
}
|
||||
return state == slash || state == minus
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func Test_namedValues(t *testing.T) {
|
||||
@@ -16,3 +20,67 @@ func Test_namedValues(t *testing.T) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Fuzz_notWhitespace(f *testing.F) {
|
||||
f.Add("")
|
||||
f.Add(" ")
|
||||
f.Add(";")
|
||||
f.Add("0")
|
||||
f.Add("-")
|
||||
f.Add("-0")
|
||||
f.Add("--")
|
||||
f.Add("--0")
|
||||
f.Add("--\n")
|
||||
f.Add("--0\n")
|
||||
f.Add("/0")
|
||||
f.Add("/*")
|
||||
f.Add("/*/")
|
||||
f.Add("/**")
|
||||
f.Add("/*0")
|
||||
f.Add("/**/")
|
||||
f.Add("/***/")
|
||||
f.Add("/**0/")
|
||||
f.Add("\v")
|
||||
f.Add(" \v")
|
||||
f.Add("\xf0")
|
||||
f.Add("\000")
|
||||
|
||||
db, err := Open(":memory:")
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
f.Fuzz(func(t *testing.T, str string) {
|
||||
if len(str) > 128 {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
c, err := db.Conn(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
c.Raw(func(driverConn any) error {
|
||||
conn := driverConn.(*conn).Conn
|
||||
stmt, tail, err := conn.Prepare(str)
|
||||
stmt.Close()
|
||||
|
||||
// It's hard to be bug for bug compatible with SQLite.
|
||||
// We settle for somewhat less:
|
||||
// - if SQLite reports whitespace, we must too
|
||||
// - if we report whitespace, SQLite must not parse a statement
|
||||
if notWhitespace(str) {
|
||||
if stmt == nil && tail == "" && err == nil {
|
||||
t.Errorf("was whitespace: %q", str)
|
||||
}
|
||||
} else {
|
||||
if stmt != nil {
|
||||
t.Errorf("was not whitespace: %q (%v)", str, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Embeddable Wasm build of SQLite
|
||||
|
||||
This folder includes an embeddable Wasm build of SQLite 3.46.1 for use with
|
||||
This folder includes an embeddable Wasm build of SQLite 3.47.2 for use with
|
||||
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
|
||||
|
||||
The following optional features are compiled in:
|
||||
@@ -9,6 +9,7 @@ The following optional features are compiled in:
|
||||
- [JSON](https://sqlite.org/json1.html)
|
||||
- [R*Tree](https://sqlite.org/rtree.html)
|
||||
- [GeoPoly](https://sqlite.org/geopoly.html)
|
||||
- [Spellfix1](https://sqlite.org/spellfix1.html)
|
||||
- [soundex](https://sqlite.org/lang_corefunc.html#soundex)
|
||||
- [stat4](https://sqlite.org/compile.html#enable_stat4)
|
||||
- [base64](https://github.com/sqlite/sqlite/blob/master/ext/misc/base64.c)
|
||||
@@ -35,6 +36,6 @@ You can use your own custom build of SQLite.
|
||||
Examples of custom builds of SQLite are:
|
||||
- [`github.com/ncruces/go-sqlite3/embed/bcw2`](https://github.com/ncruces/go-sqlite3/tree/main/embed/bcw2)
|
||||
built from a branch supporting [`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md)
|
||||
and [Wal2](https://www.sqlite.org/cgi/src/doc/wal2/doc/wal2.md).
|
||||
and [Wal2](https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md).
|
||||
- [`github.com/asg017/sqlite-vec-go-bindings/ncruces`](https://github.com/asg017/sqlite-vec-go-bindings)
|
||||
which includes the [`sqlite-vec`](https://github.com/asg017/sqlite-vec) vector search extension.
|
||||
2
embed/bcw2/.gitignore
vendored
Normal file
2
embed/bcw2/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
build/
|
||||
sqlite/
|
||||
@@ -1,8 +1,8 @@
|
||||
# Embeddable Wasm build of SQLite
|
||||
|
||||
This folder includes an embeddable Wasm build of SQLite 3.46.1, including the experimental
|
||||
This folder includes an embeddable Wasm build of SQLite, including the experimental
|
||||
[`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md) and
|
||||
[Wal2](https://www.sqlite.org/cgi/src/doc/wal2/doc/wal2.md) patches.
|
||||
[Wal2](https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md) patches.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This package is experimental.
|
||||
|
||||
Binary file not shown.
@@ -32,6 +32,11 @@ func Test_bcw2(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`DELETE FROM test LIMIT 1`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -42,7 +47,7 @@ func Test_bcw2(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != "3.46.1" {
|
||||
if version != "3.48.0" {
|
||||
t.Error(version)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@ mkdir -p build/ext/
|
||||
cp "$ROOT"/sqlite3/*.[ch] build/
|
||||
cp "$ROOT"/sqlite3/*.patch build/
|
||||
|
||||
curl -# https://www.sqlite.org/src/tarball/sqlite.tar.gz?r=bedrock-3.46 | tar xz
|
||||
# https://sqlite.org/src/info/ec5d7025cba9f4ac
|
||||
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=ec5d7025 | tar xz
|
||||
|
||||
cd sqlite
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c
|
||||
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c OPTS=-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT
|
||||
else
|
||||
sh configure
|
||||
make sqlite3.c
|
||||
sh configure --enable-update-limit && make sqlite3.c
|
||||
fi
|
||||
cd ~-
|
||||
|
||||
@@ -33,6 +33,7 @@ mv sqlite/ext/misc/decimal.c build/ext/
|
||||
mv sqlite/ext/misc/ieee754.c build/ext/
|
||||
mv sqlite/ext/misc/regexp.c build/ext/
|
||||
mv sqlite/ext/misc/series.c build/ext/
|
||||
mv sqlite/ext/misc/spellfix.c build/ext/
|
||||
mv sqlite/ext/misc/uint.c build/ext/
|
||||
|
||||
cd build
|
||||
@@ -44,7 +45,7 @@ cd ~-
|
||||
-o bcw2.wasm "build/main.c" \
|
||||
-I"build" \
|
||||
-mexec-model=reactor \
|
||||
-matomics -msimd128 -mmutable-globals \
|
||||
-msimd128 -mmutable-globals -mmultivalue \
|
||||
-mbulk-memory -mreference-types \
|
||||
-mnontrapping-fptoint -msign-ext \
|
||||
-fno-stack-protector -fno-stack-clash-protection \
|
||||
@@ -52,6 +53,7 @@ cd ~-
|
||||
-Wl,--import-undefined \
|
||||
-Wl,--initial-memory=327680 \
|
||||
-D_HAVE_SQLITE_CONFIG_H \
|
||||
-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \
|
||||
-DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \
|
||||
$(awk '{print "-Wl,--export="$0}' ../exports.txt)
|
||||
|
||||
|
||||
13
embed/bcw2/go.mod
Normal file
13
embed/bcw2/go.mod
Normal file
@@ -0,0 +1,13 @@
|
||||
module github.com/ncruces/go-sqlite3/embed/bcw2
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require github.com/ncruces/go-sqlite3 v0.21.3
|
||||
|
||||
require (
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.2 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
)
|
||||
10
embed/bcw2/go.sum
Normal file
10
embed/bcw2/go.sum
Normal file
@@ -0,0 +1,10 @@
|
||||
github.com/ncruces/go-sqlite3 v0.21.3 h1:hHkfNQLcbnxPJZhC/RGw9SwP3bfkv/Y0xUHWsr1CdMQ=
|
||||
github.com/ncruces/go-sqlite3 v0.21.3/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
|
||||
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.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
@@ -6,7 +6,7 @@
|
||||
// import _ "github.com/ncruces/go-sqlite3/embed/bcw2"
|
||||
//
|
||||
// [BEGIN CONCURRENT]: https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md
|
||||
// [Wal2]: https://www.sqlite.org/cgi/src/doc/wal2/doc/wal2.md
|
||||
// [Wal2]: https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md
|
||||
package bcw2
|
||||
|
||||
import (
|
||||
|
||||
@@ -14,7 +14,7 @@ trap 'rm -f sqlite3.tmp' EXIT
|
||||
-o sqlite3.wasm "$ROOT/sqlite3/main.c" \
|
||||
-I"$ROOT/sqlite3" \
|
||||
-mexec-model=reactor \
|
||||
-matomics -msimd128 -mmutable-globals \
|
||||
-msimd128 -mmutable-globals -mmultivalue \
|
||||
-mbulk-memory -mreference-types \
|
||||
-mnontrapping-fptoint -msign-ext \
|
||||
-fno-stack-protector -fno-stack-clash-protection \
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
aligned_alloc
|
||||
free
|
||||
malloc
|
||||
malloc_destructor
|
||||
sqlite3_anycollseq_init
|
||||
sqlite3_autovacuum_pages_go
|
||||
sqlite3_backup_finish
|
||||
@@ -9,7 +6,7 @@ sqlite3_backup_init
|
||||
sqlite3_backup_pagecount
|
||||
sqlite3_backup_remaining
|
||||
sqlite3_backup_step
|
||||
sqlite3_bind_blob64
|
||||
sqlite3_bind_blob_go
|
||||
sqlite3_bind_double
|
||||
sqlite3_bind_int64
|
||||
sqlite3_bind_null
|
||||
@@ -17,7 +14,7 @@ sqlite3_bind_parameter_count
|
||||
sqlite3_bind_parameter_index
|
||||
sqlite3_bind_parameter_name
|
||||
sqlite3_bind_pointer_go
|
||||
sqlite3_bind_text64
|
||||
sqlite3_bind_text_go
|
||||
sqlite3_bind_value
|
||||
sqlite3_bind_zeroblob64
|
||||
sqlite3_blob_bytes
|
||||
@@ -54,6 +51,7 @@ sqlite3_create_collation_go
|
||||
sqlite3_create_function_go
|
||||
sqlite3_create_module_go
|
||||
sqlite3_create_window_function_go
|
||||
sqlite3_data_count
|
||||
sqlite3_database_file_object
|
||||
sqlite3_db_cacheflush
|
||||
sqlite3_db_config
|
||||
@@ -74,17 +72,21 @@ sqlite3_filename_database
|
||||
sqlite3_filename_journal
|
||||
sqlite3_filename_wal
|
||||
sqlite3_finalize
|
||||
sqlite3_free
|
||||
sqlite3_get_autocommit
|
||||
sqlite3_get_auxdata
|
||||
sqlite3_hard_heap_limit64
|
||||
sqlite3_interrupt
|
||||
sqlite3_last_insert_rowid
|
||||
sqlite3_limit
|
||||
sqlite3_malloc64
|
||||
sqlite3_open_v2
|
||||
sqlite3_overload_function
|
||||
sqlite3_prepare_v3
|
||||
sqlite3_progress_handler_go
|
||||
sqlite3_realloc64
|
||||
sqlite3_reset
|
||||
sqlite3_result_blob64
|
||||
sqlite3_result_blob_go
|
||||
sqlite3_result_double
|
||||
sqlite3_result_error
|
||||
sqlite3_result_error_code
|
||||
@@ -93,13 +95,14 @@ sqlite3_result_error_toobig
|
||||
sqlite3_result_int64
|
||||
sqlite3_result_null
|
||||
sqlite3_result_pointer_go
|
||||
sqlite3_result_text64
|
||||
sqlite3_result_text_go
|
||||
sqlite3_result_value
|
||||
sqlite3_result_zeroblob64
|
||||
sqlite3_rollback_hook_go
|
||||
sqlite3_set_authorizer_go
|
||||
sqlite3_set_auxdata_go
|
||||
sqlite3_set_last_insert_rowid
|
||||
sqlite3_soft_heap_limit64
|
||||
sqlite3_step
|
||||
sqlite3_stmt_busy
|
||||
sqlite3_stmt_readonly
|
||||
|
||||
@@ -19,7 +19,7 @@ func Test_init(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != "3.46.1" {
|
||||
if version != "3.47.2" {
|
||||
t.Error(version)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
10
error.go
10
error.go
@@ -106,6 +106,11 @@ func (e ErrorCode) Temporary() bool {
|
||||
return e == BUSY
|
||||
}
|
||||
|
||||
// ExtendedCode returns the extended error code for this error.
|
||||
func (e ErrorCode) ExtendedCode() ExtendedErrorCode {
|
||||
return ExtendedErrorCode(e)
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e ExtendedErrorCode) Error() string {
|
||||
return util.ErrorCodeString(uint32(e))
|
||||
@@ -136,6 +141,11 @@ func (e ExtendedErrorCode) Timeout() bool {
|
||||
return e == BUSY_TIMEOUT
|
||||
}
|
||||
|
||||
// Code returns the primary error code for this error.
|
||||
func (e ExtendedErrorCode) Code() ErrorCode {
|
||||
return ErrorCode(e)
|
||||
}
|
||||
|
||||
func errorCode(err error, def ErrorCode) (msg string, code uint32) {
|
||||
switch code := err.(type) {
|
||||
case nil:
|
||||
|
||||
37
ext/README.md
Normal file
37
ext/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Go SQLite Extensions
|
||||
|
||||
This folder collects optional SQLite extensions
|
||||
you can load into your database connections.
|
||||
|
||||
### Extensions
|
||||
|
||||
- [`github.com/ncruces/go-sqlite3/ext/array`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/array)
|
||||
provides the [`array`](https://sqlite.org/carray.html) table-valued function.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/blobio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blobio)
|
||||
simplifies [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/bloom`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/bloom)
|
||||
provides a [Bloom filter](https://github.com/nalgeon/sqlean/issues/27#issuecomment-1002267134) virtual table.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/closure`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/closure)
|
||||
provides a transitive closure virtual table.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/csv`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/csv)
|
||||
reads [comma-separated values](https://sqlite.org/csv.html).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/fileio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/fileio)
|
||||
reads, writes and lists files.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/hash`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/hash)
|
||||
provides cryptographic hash functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/lines`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/lines)
|
||||
reads data [line-by-line](https://github.com/asg017/sqlite-lines).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/pivot`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/pivot)
|
||||
creates [pivot tables](https://github.com/jakethaw/pivot_vtab).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/regexp`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/regexp)
|
||||
provides regular expression functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/statement`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/statement)
|
||||
creates [parameterized views](https://github.com/0x09/sqlite-statement-vtab).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/stats`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
|
||||
provides [statistics](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html) functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
|
||||
provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/uuid`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/uuid)
|
||||
generates [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
|
||||
maps multidimensional data to one dimension.
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
|
||||
// Register registers the SQL functions:
|
||||
//
|
||||
// readblob(schema, table, column, rowid, offset, n)
|
||||
// readblob(schema, table, column, rowid, offset, n/writer)
|
||||
//
|
||||
// Reads n bytes of a blob, starting at offset.
|
||||
//
|
||||
// writeblob(schema, table, column, rowid, offset, data)
|
||||
// writeblob(schema, table, column, rowid, offset, data/reader)
|
||||
//
|
||||
// Writes data into a blob, at the given offset.
|
||||
//
|
||||
@@ -27,6 +27,10 @@ import (
|
||||
// using [sqlite3.BindPointer] or [sqlite3.Pointer].
|
||||
// The optional args will be passed to the callback,
|
||||
// along with the [sqlite3.Blob] handle.
|
||||
// The [sqlite3.Blob] handle is only valid during
|
||||
// the execution of the callback. Callers cannot
|
||||
// read or write to the handle after the callback
|
||||
// exits.
|
||||
//
|
||||
// https://sqlite.org/c3ref/blob.html
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
@@ -40,6 +44,8 @@ func Register(db *sqlite3.Conn) error {
|
||||
type OpenCallback func(*sqlite3.Blob, ...sqlite3.Value) error
|
||||
|
||||
func readblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
_ = arg[5] // bounds check
|
||||
|
||||
blob, err := getAuxBlob(ctx, arg, false)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
@@ -52,23 +58,30 @@ func readblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
return // notest
|
||||
}
|
||||
|
||||
n := arg[5].Int64()
|
||||
if n <= 0 {
|
||||
return
|
||||
if p, ok := arg[5].Pointer().(io.Writer); ok {
|
||||
var n int64
|
||||
n, err = blob.WriteTo(p)
|
||||
ctx.ResultInt64(n)
|
||||
} else {
|
||||
n := arg[5].Int64()
|
||||
if n <= 0 {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
_, err = io.ReadFull(blob, buf)
|
||||
ctx.ResultBlob(buf)
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
|
||||
_, err = io.ReadFull(blob, buf)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
|
||||
ctx.ResultBlob(buf)
|
||||
setAuxBlob(ctx, blob, false)
|
||||
}
|
||||
|
||||
func writeblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
_ = arg[5] // bounds check
|
||||
|
||||
blob, err := getAuxBlob(ctx, arg, true)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
@@ -82,7 +95,9 @@ func writeblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
if p, ok := arg[5].Pointer().(io.Reader); ok {
|
||||
_, err = blob.ReadFrom(p)
|
||||
var n int64
|
||||
n, err = blob.ReadFrom(p)
|
||||
ctx.ResultInt64(n)
|
||||
} else {
|
||||
_, err = blob.Write(arg[5].RawBlob())
|
||||
}
|
||||
|
||||
@@ -34,23 +34,26 @@ func Example() {
|
||||
const message = "Hello BLOB!"
|
||||
|
||||
// Create the BLOB.
|
||||
_, err = db.Exec(`INSERT INTO test VALUES (?)`, sqlite3.ZeroBlob(len(message)))
|
||||
r, err := db.Exec(`INSERT INTO test VALUES (?)`, sqlite3.ZeroBlob(len(message)))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := r.LastInsertId()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Write the BLOB.
|
||||
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', last_insert_rowid(), 0, ?)`, message)
|
||||
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', ?, 0, ?)`,
|
||||
id, message)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Read the BLOB.
|
||||
_, err = db.Exec(`SELECT openblob('main', 'test', 'col', rowid, false, ?) FROM test`,
|
||||
sqlite3.Pointer[blobio.OpenCallback](func(blob *sqlite3.Blob, _ ...sqlite3.Value) error {
|
||||
_, err = blob.WriteTo(os.Stdout)
|
||||
return err
|
||||
}))
|
||||
_, err = db.Exec(`SELECT readblob('main', 'test', 'col', ?, 0, ?)`,
|
||||
id, sqlite3.Pointer(os.Stdout))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/dchest/siphash"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
@@ -34,7 +35,7 @@ type bloom struct {
|
||||
}
|
||||
|
||||
func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom, err error) {
|
||||
t := bloom{
|
||||
b := bloom{
|
||||
db: db,
|
||||
schema: schema,
|
||||
storage: table + "_storage",
|
||||
@@ -54,34 +55,40 @@ func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom,
|
||||
}
|
||||
|
||||
if len(arg) > 1 {
|
||||
t.prob, err = strconv.ParseFloat(arg[1], 64)
|
||||
b.prob, err = strconv.ParseFloat(arg[1], 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.prob <= 0 || t.prob >= 1 {
|
||||
if b.prob <= 0 || b.prob >= 1 {
|
||||
return nil, util.ErrorString("bloom: probability must be in the range (0,1)")
|
||||
}
|
||||
} else {
|
||||
t.prob = 0.01
|
||||
b.prob = 0.01
|
||||
}
|
||||
|
||||
if len(arg) > 2 {
|
||||
t.hashes, err = strconv.Atoi(arg[2])
|
||||
b.hashes, err = strconv.Atoi(arg[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.hashes <= 0 {
|
||||
if b.hashes <= 0 {
|
||||
return nil, util.ErrorString("bloom: number of hash functions must be positive")
|
||||
}
|
||||
} else {
|
||||
t.hashes = max(1, numHashes(t.prob))
|
||||
b.hashes = max(1, numHashes(b.prob))
|
||||
}
|
||||
|
||||
t.bytes = numBytes(nelem, t.prob)
|
||||
b.bytes = numBytes(nelem, b.prob)
|
||||
|
||||
err = db.DeclareVTab(
|
||||
`CREATE TABLE x(present, word HIDDEN NOT NULL PRIMARY KEY) WITHOUT ROWID`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.Exec(fmt.Sprintf(
|
||||
`CREATE TABLE %s.%s (data BLOB, p REAL, n INTEGER, m INTEGER, k INTEGER)`,
|
||||
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage)))
|
||||
sqlite3.QuoteIdentifier(b.schema), sqlite3.QuoteIdentifier(b.storage)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -92,23 +99,17 @@ func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom,
|
||||
err = db.Exec(fmt.Sprintf(
|
||||
`INSERT INTO %s.%s (rowid, data, p, n, m, k)
|
||||
VALUES (1, zeroblob(%d), %f, %d, %d, %d)`,
|
||||
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage),
|
||||
t.bytes, t.prob, nelem, 8*t.bytes, t.hashes))
|
||||
sqlite3.QuoteIdentifier(b.schema), sqlite3.QuoteIdentifier(b.storage),
|
||||
b.bytes, b.prob, nelem, 8*b.bytes, b.hashes))
|
||||
if err != nil {
|
||||
b.Destroy()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.DeclareVTab(
|
||||
`CREATE TABLE x(present, word HIDDEN NOT NULL PRIMARY KEY) WITHOUT ROWID`)
|
||||
if err != nil {
|
||||
t.Destroy()
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func connect(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom, err error) {
|
||||
t := bloom{
|
||||
b := bloom{
|
||||
db: db,
|
||||
schema: schema,
|
||||
storage: table + "_storage",
|
||||
@@ -122,7 +123,7 @@ func connect(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom
|
||||
|
||||
load, _, err := db.Prepare(fmt.Sprintf(
|
||||
`SELECT m/8, p, k FROM %s.%s WHERE rowid = 1`,
|
||||
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage)))
|
||||
sqlite3.QuoteIdentifier(b.schema), sqlite3.QuoteIdentifier(b.storage)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -135,10 +136,10 @@ func connect(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom
|
||||
return nil, sqlite3.CORRUPT_VTAB
|
||||
}
|
||||
|
||||
t.bytes = load.ColumnInt64(0)
|
||||
t.prob = load.ColumnFloat(1)
|
||||
t.hashes = load.ColumnInt(2)
|
||||
return &t, nil
|
||||
b.bytes = load.ColumnInt64(0)
|
||||
b.prob = load.ColumnFloat(1)
|
||||
b.hashes = load.ColumnInt(2)
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func (b *bloom) Destroy() error {
|
||||
@@ -198,10 +199,10 @@ func (t *bloom) Integrity(schema, table string, flags int) error {
|
||||
}
|
||||
|
||||
func (b *bloom) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
for n, cst := range idx.Constraint {
|
||||
for i, cst := range idx.Constraint {
|
||||
if cst.Usable && cst.Column == 1 &&
|
||||
cst.Op == sqlite3.INDEX_CONSTRAINT_EQ {
|
||||
idx.ConstraintUsage[n].ArgvIndex = 1
|
||||
idx.ConstraintUsage[i].ArgvIndex = 1
|
||||
idx.OrderByConsumed = true
|
||||
idx.EstimatedRows = 1
|
||||
idx.EstimatedCost = float64(b.hashes)
|
||||
@@ -272,10 +273,6 @@ type cursor struct {
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
if len(arg) != 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.eof = false
|
||||
c.arg = &arg[0]
|
||||
blob := arg[0].RawBlob()
|
||||
|
||||
BIN
ext/bloom/testdata/bloom.db
vendored
BIN
ext/bloom/testdata/bloom.db
vendored
Binary file not shown.
260
ext/closure/closure.go
Normal file
260
ext/closure/closure.go
Normal file
@@ -0,0 +1,260 @@
|
||||
// Package closure provides a transitive closure virtual table.
|
||||
//
|
||||
// The transitive_closure virtual table finds the transitive closure of
|
||||
// a parent/child relationship in a real table.
|
||||
//
|
||||
// https://sqlite.org/src/doc/tip/ext/misc/closure.c
|
||||
package closure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/util/sql3util"
|
||||
)
|
||||
|
||||
const (
|
||||
_COL_ID = 0
|
||||
_COL_DEPTH = 1
|
||||
_COL_ROOT = 2
|
||||
_COL_TABLENAME = 3
|
||||
_COL_IDCOLUMN = 4
|
||||
_COL_PARENTCOLUMN = 5
|
||||
)
|
||||
|
||||
// Register registers the transitive_closure virtual table:
|
||||
//
|
||||
// CREATE VIRTUAL TABLE temp.closure USING transitive_closure;
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
return sqlite3.CreateModule(db, "transitive_closure", nil,
|
||||
func(db *sqlite3.Conn, _, _, _ string, arg ...string) (*closure, error) {
|
||||
var (
|
||||
table string
|
||||
column string
|
||||
parent string
|
||||
|
||||
done = util.Set[string]{}
|
||||
)
|
||||
|
||||
for _, arg := range arg {
|
||||
key, val := sql3util.NamedArg(arg)
|
||||
if done.Contains(key) {
|
||||
return nil, fmt.Errorf("transitive_closure: more than one %q parameter", key)
|
||||
}
|
||||
switch key {
|
||||
case "tablename":
|
||||
table = sql3util.Unquote(val)
|
||||
case "idcolumn":
|
||||
column = sql3util.Unquote(val)
|
||||
case "parentcolumn":
|
||||
parent = sql3util.Unquote(val)
|
||||
default:
|
||||
return nil, fmt.Errorf("transitive_closure: unknown %q parameter", key)
|
||||
}
|
||||
done.Add(key)
|
||||
}
|
||||
|
||||
err := db.DeclareVTab(`CREATE TABLE x(id,depth,root HIDDEN,tablename HIDDEN,idcolumn HIDDEN,parentcolumn HIDDEN)`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &closure{
|
||||
db: db,
|
||||
table: table,
|
||||
column: column,
|
||||
parent: parent,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type closure struct {
|
||||
db *sqlite3.Conn
|
||||
table string
|
||||
column string
|
||||
parent string
|
||||
}
|
||||
|
||||
func (c *closure) Destroy() error { return nil }
|
||||
|
||||
func (c *closure) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
plan := 0
|
||||
posi := 1
|
||||
cost := 1e7
|
||||
|
||||
for i, cst := range idx.Constraint {
|
||||
switch {
|
||||
case !cst.Usable:
|
||||
continue
|
||||
|
||||
case plan&1 == 0 && cst.Column == _COL_ROOT:
|
||||
switch cst.Op {
|
||||
case sqlite3.INDEX_CONSTRAINT_EQ:
|
||||
plan |= 1
|
||||
cost /= 100
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
ArgvIndex: 1,
|
||||
Omit: true,
|
||||
}
|
||||
}
|
||||
|
||||
case plan&0xf0 == 0 && cst.Column == _COL_DEPTH:
|
||||
switch cst.Op {
|
||||
case sqlite3.INDEX_CONSTRAINT_LT, sqlite3.INDEX_CONSTRAINT_LE, sqlite3.INDEX_CONSTRAINT_EQ:
|
||||
plan |= posi << 4
|
||||
cost /= 5
|
||||
posi += 1
|
||||
idx.ConstraintUsage[i].ArgvIndex = posi
|
||||
if cst.Op == sqlite3.INDEX_CONSTRAINT_LT {
|
||||
plan |= 2
|
||||
}
|
||||
}
|
||||
|
||||
case plan&0xf00 == 0 && cst.Column == _COL_TABLENAME:
|
||||
switch cst.Op {
|
||||
case sqlite3.INDEX_CONSTRAINT_EQ:
|
||||
plan |= posi << 8
|
||||
cost /= 5
|
||||
posi += 1
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
ArgvIndex: posi,
|
||||
Omit: true,
|
||||
}
|
||||
}
|
||||
|
||||
case plan&0xf000 == 0 && cst.Column == _COL_IDCOLUMN:
|
||||
switch cst.Op {
|
||||
case sqlite3.INDEX_CONSTRAINT_EQ:
|
||||
plan |= posi << 12
|
||||
posi += 1
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
ArgvIndex: posi,
|
||||
Omit: true,
|
||||
}
|
||||
}
|
||||
|
||||
case plan&0xf0000 == 0 && cst.Column == _COL_PARENTCOLUMN:
|
||||
switch cst.Op {
|
||||
case sqlite3.INDEX_CONSTRAINT_EQ:
|
||||
plan |= posi << 16
|
||||
posi += 1
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
ArgvIndex: posi,
|
||||
Omit: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if plan&1 == 0 ||
|
||||
c.table == "" && plan&0xf00 == 0 ||
|
||||
c.column == "" && plan&0xf000 == 0 ||
|
||||
c.parent == "" && plan&0xf0000 == 0 {
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
|
||||
idx.EstimatedCost = cost
|
||||
idx.IdxNum = plan
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *closure) Open() (sqlite3.VTabCursor, error) {
|
||||
return &cursor{closure: c}, nil
|
||||
}
|
||||
|
||||
type cursor struct {
|
||||
*closure
|
||||
nodes []node
|
||||
}
|
||||
|
||||
type node struct {
|
||||
id int64
|
||||
depth int
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
root := arg[0].Int64()
|
||||
maxDepth := math.MaxInt
|
||||
if idxNum&0xf0 != 0 {
|
||||
maxDepth = arg[(idxNum>>4)&0xf].Int()
|
||||
if idxNum&2 != 0 {
|
||||
maxDepth -= 1
|
||||
}
|
||||
}
|
||||
table := c.table
|
||||
if idxNum&0xf00 != 0 {
|
||||
table = arg[(idxNum>>8)&0xf].Text()
|
||||
}
|
||||
column := c.column
|
||||
if idxNum&0xf000 != 0 {
|
||||
column = arg[(idxNum>>12)&0xf].Text()
|
||||
}
|
||||
parent := c.parent
|
||||
if idxNum&0xf0000 != 0 {
|
||||
parent = arg[(idxNum>>16)&0xf].Text()
|
||||
}
|
||||
|
||||
sql := fmt.Sprintf(
|
||||
`SELECT %[1]s.%[2]s FROM %[1]s WHERE %[1]s.%[3]s=?`,
|
||||
sqlite3.QuoteIdentifier(table),
|
||||
sqlite3.QuoteIdentifier(column),
|
||||
sqlite3.QuoteIdentifier(parent),
|
||||
)
|
||||
stmt, _, err := c.db.Prepare(sql)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
c.nodes = []node{{root, 0}}
|
||||
set := util.Set[int64]{}
|
||||
set.Add(root)
|
||||
for i := 0; i < len(c.nodes); i++ {
|
||||
curr := c.nodes[i]
|
||||
if curr.depth >= maxDepth {
|
||||
continue
|
||||
}
|
||||
stmt.BindInt64(1, curr.id)
|
||||
for stmt.Step() {
|
||||
if stmt.ColumnType(0) == sqlite3.INTEGER {
|
||||
next := stmt.ColumnInt64(0)
|
||||
if !set.Contains(next) {
|
||||
set.Add(next)
|
||||
c.nodes = append(c.nodes, node{next, curr.depth + 1})
|
||||
}
|
||||
}
|
||||
}
|
||||
stmt.Reset()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
|
||||
switch n {
|
||||
case _COL_ID:
|
||||
ctx.ResultInt64(c.nodes[0].id)
|
||||
case _COL_DEPTH:
|
||||
ctx.ResultInt(c.nodes[0].depth)
|
||||
case _COL_TABLENAME:
|
||||
ctx.ResultText(c.table)
|
||||
case _COL_IDCOLUMN:
|
||||
ctx.ResultText(c.column)
|
||||
case _COL_PARENTCOLUMN:
|
||||
ctx.ResultText(c.parent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) Next() error {
|
||||
c.nodes = c.nodes[1:]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) EOF() bool {
|
||||
return len(c.nodes) == 0
|
||||
}
|
||||
|
||||
func (c *cursor) RowID() (int64, error) {
|
||||
return c.nodes[0].id, nil
|
||||
}
|
||||
184
ext/closure/closure_test.go
Normal file
184
ext/closure/closure_test.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package closure_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/closure"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
sqlite3.AutoExtension(closure.Register)
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func Example() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
closure.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE employees (
|
||||
id INTEGER PRIMARY KEY,
|
||||
parent_id INTEGER,
|
||||
name TEXT
|
||||
);
|
||||
CREATE INDEX employees_parent_idx ON employees(parent_id);
|
||||
INSERT INTO employees (id, parent_id, name) VALUES
|
||||
(11, NULL, 'Diane'),
|
||||
(12, 11, 'Bob'),
|
||||
(21, 11, 'Emma'),
|
||||
(22, 21, 'Grace'),
|
||||
(23, 21, 'Henry'),
|
||||
(24, 21, 'Irene'),
|
||||
(25, 21, 'Frank'),
|
||||
(31, 11, 'Cindy'),
|
||||
(32, 31, 'Dave'),
|
||||
(33, 31, 'Alice');
|
||||
CREATE VIRTUAL TABLE hierarchy USING transitive_closure(
|
||||
tablename = "employees",
|
||||
idcolumn = "id",
|
||||
parentcolumn = "parent_id"
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`
|
||||
SELECT employees.id, name FROM employees, hierarchy
|
||||
WHERE employees.id = hierarchy.id AND hierarchy.root = 31
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for stmt.Step() {
|
||||
fmt.Println(stmt.ColumnInt(0), stmt.ColumnText(1))
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// 31 Cindy
|
||||
// 32 Dave
|
||||
// 33 Alice
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE employees (
|
||||
id INTEGER PRIMARY KEY,
|
||||
parent_id INTEGER,
|
||||
name TEXT
|
||||
);
|
||||
CREATE INDEX employees_parent_idx ON employees(parent_id);
|
||||
INSERT INTO employees (id, parent_id, name) VALUES
|
||||
(11, NULL, 'Diane'),
|
||||
(12, 11, 'Bob'),
|
||||
(21, 11, 'Emma'),
|
||||
(22, 21, 'Grace'),
|
||||
(23, 21, 'Henry'),
|
||||
(24, 21, 'Irene'),
|
||||
(25, 21, 'Frank'),
|
||||
(31, 11, 'Cindy'),
|
||||
(32, 31, 'Dave'),
|
||||
(33, 31, 'Alice');
|
||||
CREATE VIRTUAL TABLE temp.closure USING transitive_closure;
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`
|
||||
SELECT employees.id, name FROM employees, closure
|
||||
WHERE employees.id = closure.id
|
||||
AND closure.root = 31
|
||||
AND closure.depth < 1
|
||||
AND closure.tablename='employees'
|
||||
AND closure.idcolumn='id'
|
||||
AND closure.parentcolumn='parent_id'
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if !stmt.Step() {
|
||||
t.Error("want row")
|
||||
}
|
||||
if stmt.Step() {
|
||||
t.Error("don't want row")
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE hierarchy USING transitive_closure(table='employees')`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE hierarchy USING transitive_closure(tablename='employees', tablename="employees")`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
err = db.Exec("CREATE VIRTUAL TABLE hierarchy USING transitive_closure(tablename=`employees`)")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT * FROM hierarchy`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/util/vtabutil"
|
||||
"github.com/ncruces/go-sqlite3/util/sql3util"
|
||||
)
|
||||
|
||||
func uintArg(key, val string) (int, error) {
|
||||
@@ -20,7 +19,7 @@ func boolArg(key, val string) (bool, error) {
|
||||
if val == "" {
|
||||
return true, nil
|
||||
}
|
||||
b, ok := util.ParseBool(val)
|
||||
b, ok := sql3util.ParseBool(val)
|
||||
if ok {
|
||||
return b, nil
|
||||
}
|
||||
@@ -28,7 +27,7 @@ func boolArg(key, val string) (bool, error) {
|
||||
}
|
||||
|
||||
func runeArg(key, val string) (rune, error) {
|
||||
r, _, tail, err := strconv.UnquoteChar(vtabutil.Unquote(val), 0)
|
||||
r, _, tail, err := strconv.UnquoteChar(sql3util.Unquote(val), 0)
|
||||
if tail != "" || err != nil {
|
||||
return 0, fmt.Errorf("csv: invalid %q parameter: %s", key, val)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package csv
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/util/vtabutil"
|
||||
"github.com/ncruces/go-sqlite3/util/sql3util"
|
||||
)
|
||||
|
||||
func Test_uintArg(t *testing.T) {
|
||||
@@ -24,7 +24,7 @@ func Test_uintArg(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.arg, func(t *testing.T) {
|
||||
key, val := vtabutil.NamedArg(tt.arg)
|
||||
key, val := sql3util.NamedArg(tt.arg)
|
||||
if key != tt.key {
|
||||
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func Test_boolArg(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.arg, func(t *testing.T) {
|
||||
key, val := vtabutil.NamedArg(tt.arg)
|
||||
key, val := sql3util.NamedArg(tt.arg)
|
||||
if key != tt.key {
|
||||
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func Test_runeArg(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.arg, func(t *testing.T) {
|
||||
key, val := vtabutil.NamedArg(tt.arg)
|
||||
key, val := sql3util.NamedArg(tt.arg)
|
||||
if key != tt.key {
|
||||
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/util/osutil"
|
||||
"github.com/ncruces/go-sqlite3/util/vtabutil"
|
||||
"github.com/ncruces/go-sqlite3/util/sql3util"
|
||||
)
|
||||
|
||||
// Register registers the CSV virtual table.
|
||||
@@ -30,7 +30,7 @@ func Register(db *sqlite3.Conn) error {
|
||||
// RegisterFS registers the CSV virtual table.
|
||||
// If a filename is specified, fsys is used to open the file.
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
|
||||
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
|
||||
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (res *table, err error) {
|
||||
var (
|
||||
filename string
|
||||
data string
|
||||
@@ -40,21 +40,21 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
|
||||
comma rune = ','
|
||||
comment rune
|
||||
|
||||
done = map[string]struct{}{}
|
||||
done = util.Set[string]{}
|
||||
)
|
||||
|
||||
for _, arg := range arg {
|
||||
key, val := vtabutil.NamedArg(arg)
|
||||
if _, ok := done[key]; ok {
|
||||
key, val := sql3util.NamedArg(arg)
|
||||
if done.Contains(key) {
|
||||
return nil, fmt.Errorf("csv: more than one %q parameter", key)
|
||||
}
|
||||
switch key {
|
||||
case "filename":
|
||||
filename = vtabutil.Unquote(val)
|
||||
filename = sql3util.Unquote(val)
|
||||
case "data":
|
||||
data = vtabutil.Unquote(val)
|
||||
data = sql3util.Unquote(val)
|
||||
case "schema":
|
||||
schema = vtabutil.Unquote(val)
|
||||
schema = sql3util.Unquote(val)
|
||||
case "header":
|
||||
header, err = boolArg(key, val)
|
||||
case "columns":
|
||||
@@ -69,14 +69,14 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
done[key] = struct{}{}
|
||||
done.Add(key)
|
||||
}
|
||||
|
||||
if (filename == "") == (data == "") {
|
||||
return nil, util.ErrorString(`csv: must specify either "filename" or "data" but not both`)
|
||||
}
|
||||
|
||||
table := &table{
|
||||
t := &table{
|
||||
fsys: fsys,
|
||||
name: filename,
|
||||
data: data,
|
||||
@@ -88,7 +88,7 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
|
||||
if schema == "" {
|
||||
var row []string
|
||||
if header || columns < 0 {
|
||||
csv, c, err := table.newReader()
|
||||
csv, c, err := t.newReader()
|
||||
defer c.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -100,22 +100,20 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
|
||||
}
|
||||
schema = getSchema(header, columns, row)
|
||||
} else {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
table.typs, err = getColumnAffinities(schema)
|
||||
}
|
||||
}()
|
||||
t.typs, err = getColumnAffinities(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = db.DeclareVTab(schema)
|
||||
if err == nil {
|
||||
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return table, nil
|
||||
return t, nil
|
||||
}
|
||||
|
||||
return sqlite3.CreateModule(db, "csv", declare, declare)
|
||||
|
||||
@@ -3,7 +3,7 @@ package csv
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/util/vtabutil"
|
||||
"github.com/ncruces/go-sqlite3/util/sql3util"
|
||||
)
|
||||
|
||||
type affinity byte
|
||||
@@ -17,13 +17,14 @@ const (
|
||||
)
|
||||
|
||||
func getColumnAffinities(schema string) ([]affinity, error) {
|
||||
tab, err := vtabutil.Parse(schema)
|
||||
tab, err := sql3util.ParseTable(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
types := make([]affinity, len(tab.Columns))
|
||||
for i, col := range tab.Columns {
|
||||
columns := tab.Columns
|
||||
types := make([]affinity, len(columns))
|
||||
for i, col := range columns {
|
||||
types[i] = getAffinity(col.Type)
|
||||
}
|
||||
return types, nil
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !(go1.23 || goexperiment.rangefunc) || vet
|
||||
//go:build !go1.23
|
||||
|
||||
package fileio
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
|
||||
db.CreateFunction("lsmode", 1, sqlite3.DETERMINISTIC, lsmode),
|
||||
sqlite3.CreateModule(db, "fsdir", nil, func(db *sqlite3.Conn, _, _, _ string, _ ...string) (fsdir, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(name,mode,mtime TIMESTAMP,data,path HIDDEN,dir HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
if err == nil {
|
||||
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
}
|
||||
return fsdir{fsys}, err
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -10,13 +10,22 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
const (
|
||||
_COL_NAME = 0
|
||||
_COL_MODE = 1
|
||||
_COL_TIME = 2
|
||||
_COL_DATA = 3
|
||||
_COL_ROOT = 4
|
||||
_COL_BASE = 5
|
||||
)
|
||||
|
||||
type fsdir struct{ fsys fs.FS }
|
||||
|
||||
func (d fsdir) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
var root, base bool
|
||||
for i, cst := range idx.Constraint {
|
||||
switch cst.Column {
|
||||
case 4: // root
|
||||
case _COL_ROOT:
|
||||
if !cst.Usable || cst.Op != sqlite3.INDEX_CONSTRAINT_EQ {
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
@@ -25,7 +34,7 @@ func (d fsdir) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
ArgvIndex: 1,
|
||||
}
|
||||
root = true
|
||||
case 5: // base
|
||||
case _COL_BASE:
|
||||
if !cst.Usable || cst.Op != sqlite3.INDEX_CONSTRAINT_EQ {
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
@@ -116,25 +125,25 @@ func (c *cursor) RowID() (int64, error) {
|
||||
|
||||
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
|
||||
switch n {
|
||||
case 0: // name
|
||||
case _COL_NAME:
|
||||
name := strings.TrimPrefix(c.curr.path, c.base)
|
||||
ctx.ResultText(name)
|
||||
|
||||
case 1: // mode
|
||||
case _COL_MODE:
|
||||
i, err := c.curr.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.ResultInt64(int64(i.Mode()))
|
||||
|
||||
case 2: // mtime
|
||||
case _COL_TIME:
|
||||
i, err := c.curr.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.ResultTime(i.ModTime(), sqlite3.TimeFormatUnixFrac)
|
||||
|
||||
case 3: // data
|
||||
case _COL_DATA:
|
||||
switch typ := c.curr.Type(); {
|
||||
case typ.IsRegular():
|
||||
var data []byte
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !(go1.23 || goexperiment.rangefunc) || vet
|
||||
//go:build !go1.23
|
||||
|
||||
package fileio
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build (go1.23 || goexperiment.rangefunc) && !vet
|
||||
//go:build go1.23
|
||||
|
||||
package fileio
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
// Register registers cryptographic hash functions for a database connection.
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
|
||||
var errs util.ErrorJoiner
|
||||
if crypto.MD4.Available() {
|
||||
|
||||
@@ -7,15 +7,16 @@ import (
|
||||
_ "crypto/sha512"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
_ "golang.org/x/crypto/blake2b"
|
||||
_ "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"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
|
||||
@@ -38,14 +38,18 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
|
||||
return errors.Join(
|
||||
sqlite3.CreateModule(db, "lines", nil,
|
||||
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_INNOCUOUS)
|
||||
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN, delim HIDDEN)`)
|
||||
if err == nil {
|
||||
err = db.VTabConfig(sqlite3.VTAB_INNOCUOUS)
|
||||
}
|
||||
return lines{}, err
|
||||
}),
|
||||
sqlite3.CreateModule(db, "lines_read", nil,
|
||||
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN, delim HIDDEN)`)
|
||||
if err == nil {
|
||||
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
}
|
||||
return lines{fsys}, err
|
||||
}))
|
||||
}
|
||||
@@ -54,19 +58,29 @@ type lines struct {
|
||||
fsys fs.FS
|
||||
}
|
||||
|
||||
func (l lines) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
func (l lines) BestIndex(idx *sqlite3.IndexInfo) (err error) {
|
||||
err = sqlite3.CONSTRAINT
|
||||
for i, cst := range idx.Constraint {
|
||||
if cst.Column == 1 && cst.Op == sqlite3.INDEX_CONSTRAINT_EQ && cst.Usable {
|
||||
if !cst.Usable || cst.Op != sqlite3.INDEX_CONSTRAINT_EQ {
|
||||
continue
|
||||
}
|
||||
switch cst.Column {
|
||||
case 1:
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
Omit: true,
|
||||
ArgvIndex: 1,
|
||||
}
|
||||
idx.EstimatedCost = 1e6
|
||||
idx.EstimatedRows = 100
|
||||
return nil
|
||||
err = nil
|
||||
case 2:
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
Omit: true,
|
||||
ArgvIndex: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
return sqlite3.CONSTRAINT
|
||||
return err
|
||||
}
|
||||
|
||||
func (l lines) Open() (sqlite3.VTabCursor, error) {
|
||||
@@ -81,6 +95,7 @@ type cursor struct {
|
||||
line []byte
|
||||
rowID int64
|
||||
eof bool
|
||||
delim byte
|
||||
}
|
||||
|
||||
func (c *cursor) EOF() bool {
|
||||
@@ -136,6 +151,15 @@ func (c *reader) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
return fmt.Errorf("lines: unsupported argument:%.0w %v", sqlite3.MISMATCH, typ)
|
||||
}
|
||||
|
||||
c.delim = '\n'
|
||||
if len(arg) > 1 {
|
||||
b := arg[1].RawText()
|
||||
if len(b) != 1 {
|
||||
return fmt.Errorf("lines: delimiter must be a single byte%.0w", sqlite3.MISMATCH)
|
||||
}
|
||||
c.delim = b[0]
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(r)
|
||||
c.closer, _ = r.(io.Closer)
|
||||
c.rowID = 0
|
||||
@@ -146,7 +170,12 @@ func (c *reader) Next() (err error) {
|
||||
c.line = c.line[:0]
|
||||
for more := true; more; {
|
||||
var line []byte
|
||||
line, more, err = c.reader.ReadLine()
|
||||
if c.delim == '\n' {
|
||||
line, more, err = c.reader.ReadLine()
|
||||
} else {
|
||||
line, err = c.reader.ReadSlice(c.delim)
|
||||
more = err == bufio.ErrBufferFull
|
||||
}
|
||||
c.line = append(c.line, line...)
|
||||
}
|
||||
if err == io.EOF {
|
||||
@@ -173,18 +202,27 @@ func (c *buffer) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
return fmt.Errorf("lines: unsupported argument:%.0w %v", sqlite3.MISMATCH, typ)
|
||||
}
|
||||
|
||||
c.delim = '\n'
|
||||
if len(arg) > 1 {
|
||||
b := arg[1].RawText()
|
||||
if len(b) != 1 {
|
||||
return fmt.Errorf("lines: delimiter must be a single byte%.0w", sqlite3.MISMATCH)
|
||||
}
|
||||
c.delim = b[0]
|
||||
}
|
||||
|
||||
c.rowID = 0
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (c *buffer) Next() error {
|
||||
i := bytes.IndexByte(c.data, '\n')
|
||||
i := bytes.IndexByte(c.data, c.delim)
|
||||
j := i + 1
|
||||
switch {
|
||||
case i < 0:
|
||||
i = len(c.data)
|
||||
j = i
|
||||
case i > 0 && c.data[i-1] == '\r':
|
||||
case i > 0 && c.delim == '\n' && c.data[i-1] == '\r':
|
||||
i--
|
||||
}
|
||||
c.eof = len(c.data) == 0
|
||||
|
||||
@@ -163,7 +163,7 @@ func Test_lines_test(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`SELECT rowid, line FROM lines_read(?)`, "lines_test.go")
|
||||
rows, err := db.Query(`SELECT rowid, line FROM lines_read(?, '}')`, "lines_test.go")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
t.Skip(err)
|
||||
}
|
||||
|
||||
23
ext/parquet/go.mod
Normal file
23
ext/parquet/go.mod
Normal file
@@ -0,0 +1,23 @@
|
||||
module github.com/ncruces/go-sqlite3/ext/parquet
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/ncruces/go-sqlite3 v0.21.0
|
||||
github.com/parquet-go/parquet-go v0.24.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.2 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
)
|
||||
32
ext/parquet/go.sum
Normal file
32
ext/parquet/go.sum
Normal file
@@ -0,0 +1,32 @@
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/ncruces/go-sqlite3 v0.21.0 h1:EwKFoy1hHEopN4sFZarmi+McXdbCcbTuLixhEayXVbQ=
|
||||
github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/parquet-go/parquet-go v0.24.0 h1:VrsifmLPDnas8zpoHmYiWDZ1YHzLmc7NmNwPGkI2JM4=
|
||||
github.com/parquet-go/parquet-go v0.24.0/go.mod h1:OqBBRGBl7+llplCvDMql8dEKaDqjaFA/VAPw+OJiNiw=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
62
ext/parquet/parquet.go
Normal file
62
ext/parquet/parquet.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package parquet
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/util/osutil"
|
||||
"github.com/ncruces/go-sqlite3/util/sql3util"
|
||||
"github.com/parquet-go/parquet-go"
|
||||
)
|
||||
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
|
||||
if len(arg) == 0 {
|
||||
return nil, util.ErrorString(`parquet: must specify a filename`)
|
||||
}
|
||||
|
||||
file, err := osutil.OpenFile(sql3util.Unquote(arg[0]), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := parquet.NewReader(file)
|
||||
|
||||
column := make(map[int]string)
|
||||
|
||||
var schema strings.Builder
|
||||
schema.WriteString("CREATE TABLE x(")
|
||||
for i, field := range reader.Schema().Fields() {
|
||||
if i > 0 {
|
||||
schema.WriteByte(',')
|
||||
}
|
||||
schema.WriteString(sqlite3.QuoteIdentifier(field.Name()))
|
||||
schema.WriteByte(' ')
|
||||
switch field.Type().Kind() {
|
||||
case parquet.Boolean:
|
||||
schema.WriteString("BOOLEAN")
|
||||
case parquet.Int32, parquet.Int64, parquet.Int96:
|
||||
schema.WriteString("INTEGER")
|
||||
case parquet.Float, parquet.Double:
|
||||
schema.WriteString("REAL")
|
||||
case parquet.ByteArray, parquet.FixedLenByteArray:
|
||||
schema.WriteString("TEXT")
|
||||
}
|
||||
// Save the column name
|
||||
column[i] = field.Name()
|
||||
}
|
||||
schema.WriteString(");")
|
||||
err = db.DeclareVTab(schema.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &table{}, nil
|
||||
}
|
||||
|
||||
return sqlite3.CreateModule(db, "parquet", declare, declare)
|
||||
}
|
||||
|
||||
type table struct {
|
||||
}
|
||||
@@ -25,15 +25,15 @@ type table struct {
|
||||
cols []*sqlite3.Value
|
||||
}
|
||||
|
||||
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
|
||||
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (res *table, err error) {
|
||||
if len(arg) != 3 {
|
||||
return nil, fmt.Errorf("pivot: wrong number of arguments")
|
||||
}
|
||||
|
||||
table := &table{db: db}
|
||||
t := &table{db: db}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
table.Close()
|
||||
if res == nil {
|
||||
t.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -42,17 +42,17 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
|
||||
create.WriteString("CREATE TABLE x(")
|
||||
|
||||
// Row key query.
|
||||
table.scan = "SELECT * FROM\n" + arg[0]
|
||||
stmt, _, err := db.Prepare(table.scan)
|
||||
t.scan = "SELECT * FROM\n" + arg[0]
|
||||
stmt, _, err := db.Prepare(t.scan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
table.keys = make([]string, stmt.ColumnCount())
|
||||
for i := range table.keys {
|
||||
t.keys = make([]string, stmt.ColumnCount())
|
||||
for i := range t.keys {
|
||||
name := sqlite3.QuoteIdentifier(stmt.ColumnName(i))
|
||||
table.keys[i] = name
|
||||
t.keys[i] = name
|
||||
create.WriteString(sep)
|
||||
create.WriteString(name)
|
||||
sep = ","
|
||||
@@ -70,15 +70,15 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
|
||||
}
|
||||
for stmt.Step() {
|
||||
name := sqlite3.QuoteIdentifier(stmt.ColumnText(1))
|
||||
table.cols = append(table.cols, stmt.ColumnValue(0).Dup())
|
||||
t.cols = append(t.cols, stmt.ColumnValue(0).Dup())
|
||||
create.WriteString(",")
|
||||
create.WriteString(name)
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
// Pivot cell query.
|
||||
table.cell = "SELECT * FROM\n" + arg[2]
|
||||
stmt, _, err = db.Prepare(table.cell)
|
||||
t.cell = "SELECT * FROM\n" + arg[2]
|
||||
stmt, _, err = db.Prepare(t.cell)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -86,8 +86,8 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
|
||||
if stmt.ColumnCount() != 1 {
|
||||
return nil, util.ErrorString("pivot: cell query expects 1 result columns")
|
||||
}
|
||||
if stmt.BindCount() != len(table.keys)+1 {
|
||||
return nil, fmt.Errorf("pivot: cell query expects %d bound parameters", len(table.keys)+1)
|
||||
if stmt.BindCount() != len(t.keys)+1 {
|
||||
return nil, fmt.Errorf("pivot: cell query expects %d bound parameters", len(t.keys)+1)
|
||||
}
|
||||
|
||||
create.WriteByte(')')
|
||||
@@ -95,12 +95,12 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return table, nil
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *table) Close() error {
|
||||
for i := range t.cols {
|
||||
t.cols[i].Close()
|
||||
for _, c := range t.cols {
|
||||
c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Package regexp provides additional regular expression functions.
|
||||
//
|
||||
// It provides the following Unicode aware functions:
|
||||
// - regexp_like(),
|
||||
// - regexp_substr(),
|
||||
// - regexp_replace(),
|
||||
// - regexp_like(text, pattern),
|
||||
// - regexp_count(text, pattern [, start]),
|
||||
// - regexp_instr(text, pattern [, start [, N [, endoption [, subexpr ]]]]),
|
||||
// - regexp_substr(text, pattern [, start [, N [, subexpr ]]]),
|
||||
// - regexp_replace(text, pattern, replacement [, start [, N ]]),
|
||||
// - and a REGEXP operator.
|
||||
//
|
||||
// The implementation uses Go [regexp/syntax] for regular expressions.
|
||||
@@ -14,18 +16,50 @@ package regexp
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Register registers Unicode aware functions for a database connection.
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
return errors.Join(
|
||||
db.CreateFunction("regexp", 2, flags, regex),
|
||||
db.CreateFunction("regexp_like", 2, flags, regexLike),
|
||||
db.CreateFunction("regexp_count", 2, flags, regexCount),
|
||||
db.CreateFunction("regexp_count", 3, flags, regexCount),
|
||||
db.CreateFunction("regexp_instr", 2, flags, regexInstr),
|
||||
db.CreateFunction("regexp_instr", 3, flags, regexInstr),
|
||||
db.CreateFunction("regexp_instr", 4, flags, regexInstr),
|
||||
db.CreateFunction("regexp_instr", 5, flags, regexInstr),
|
||||
db.CreateFunction("regexp_instr", 6, flags, regexInstr),
|
||||
db.CreateFunction("regexp_substr", 2, flags, regexSubstr),
|
||||
db.CreateFunction("regexp_replace", 3, flags, regexReplace))
|
||||
db.CreateFunction("regexp_substr", 3, flags, regexSubstr),
|
||||
db.CreateFunction("regexp_substr", 4, flags, regexSubstr),
|
||||
db.CreateFunction("regexp_substr", 5, flags, regexSubstr),
|
||||
db.CreateFunction("regexp_replace", 3, flags, regexReplace),
|
||||
db.CreateFunction("regexp_replace", 4, flags, regexReplace),
|
||||
db.CreateFunction("regexp_replace", 5, flags, regexReplace))
|
||||
}
|
||||
|
||||
// GlobPrefix returns a GLOB for a regular expression
|
||||
// appropriate to take advantage of the [LIKE optimization]
|
||||
// in a query such as:
|
||||
//
|
||||
// SELECT column WHERE column GLOB :glob_prefix AND column REGEXP :regexp
|
||||
//
|
||||
// [LIKE optimization]: https://sqlite.org/optoverview.html#the_like_optimization
|
||||
func GlobPrefix(re *regexp.Regexp) string {
|
||||
prefix, complete := re.LiteralPrefix()
|
||||
i := strings.IndexAny(prefix, "*?[")
|
||||
if i < 0 {
|
||||
if complete {
|
||||
return prefix
|
||||
}
|
||||
i = len(prefix)
|
||||
}
|
||||
return prefix[:i] + "*"
|
||||
}
|
||||
|
||||
func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
|
||||
@@ -42,37 +76,168 @@ func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
|
||||
}
|
||||
|
||||
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
_ = arg[1] // bounds check
|
||||
re, err := load(ctx, 0, arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultBool(re.Match(arg[1].RawText()))
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
text := arg[1].RawText()
|
||||
ctx.ResultBool(re.Match(text))
|
||||
}
|
||||
|
||||
func regexLike(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultBool(re.Match(arg[0].RawText()))
|
||||
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())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
|
||||
text := arg[0].RawText()
|
||||
if len(arg) > 2 {
|
||||
pos := arg[2].Int()
|
||||
text = text[skip(text, pos):]
|
||||
}
|
||||
ctx.ResultInt(len(re.FindAll(text, -1)))
|
||||
}
|
||||
|
||||
func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultRawText(re.Find(arg[0].RawText()))
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
|
||||
text := arg[0].RawText()
|
||||
var pos, n, subexpr int
|
||||
if len(arg) > 2 {
|
||||
pos = arg[2].Int()
|
||||
}
|
||||
if len(arg) > 3 {
|
||||
n = arg[3].Int()
|
||||
}
|
||||
if len(arg) > 4 {
|
||||
subexpr = arg[4].Int()
|
||||
}
|
||||
|
||||
loc := regexFind(re, text, pos, n, subexpr)
|
||||
if loc != nil {
|
||||
ctx.ResultRawText(text[loc[0]:loc[1]])
|
||||
}
|
||||
}
|
||||
|
||||
func regexInstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
|
||||
text := arg[0].RawText()
|
||||
var pos, n, end, subexpr int
|
||||
if len(arg) > 2 {
|
||||
pos = arg[2].Int()
|
||||
}
|
||||
if len(arg) > 3 {
|
||||
n = arg[3].Int()
|
||||
}
|
||||
if len(arg) > 4 && arg[4].Bool() {
|
||||
end = 1
|
||||
}
|
||||
if len(arg) > 5 {
|
||||
subexpr = arg[5].Int()
|
||||
}
|
||||
|
||||
loc := regexFind(re, text, pos, n, subexpr)
|
||||
if loc != nil {
|
||||
ctx.ResultInt(loc[end] + 1)
|
||||
}
|
||||
}
|
||||
|
||||
func regexReplace(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
_ = arg[2] // bounds check
|
||||
|
||||
re, err := load(ctx, 1, arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultRawText(re.ReplaceAll(arg[0].RawText(), arg[2].RawText()))
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
|
||||
text := arg[0].RawText()
|
||||
repl := arg[2].RawText()
|
||||
var pos, n int
|
||||
if len(arg) > 3 {
|
||||
pos = arg[3].Int()
|
||||
}
|
||||
if len(arg) > 4 {
|
||||
n = arg[4].Int()
|
||||
}
|
||||
|
||||
res := text
|
||||
pos = skip(text, pos)
|
||||
if n > 0 {
|
||||
all := re.FindAllSubmatchIndex(text[pos:], n)
|
||||
if n <= len(all) {
|
||||
loc := all[n-1]
|
||||
res = text[:pos+loc[0]]
|
||||
res = re.Expand(res, repl, text[pos:], loc)
|
||||
res = append(res, text[pos+loc[1]:]...)
|
||||
}
|
||||
} else {
|
||||
res = append(text[:pos], re.ReplaceAll(text[pos:], repl)...)
|
||||
}
|
||||
ctx.ResultRawText(res)
|
||||
}
|
||||
|
||||
func regexFind(re *regexp.Regexp, text []byte, pos, n, subexpr int) (loc []int) {
|
||||
pos = skip(text, pos)
|
||||
text = text[pos:]
|
||||
|
||||
if n <= 1 {
|
||||
if subexpr == 0 {
|
||||
loc = re.FindIndex(text)
|
||||
} else {
|
||||
loc = re.FindSubmatchIndex(text)
|
||||
}
|
||||
} else {
|
||||
if subexpr == 0 {
|
||||
all := re.FindAllIndex(text, n)
|
||||
if n <= len(all) {
|
||||
loc = all[n-1]
|
||||
}
|
||||
} else {
|
||||
all := re.FindAllSubmatchIndex(text, n)
|
||||
if n <= len(all) {
|
||||
loc = all[n-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if 2+2*subexpr <= len(loc) {
|
||||
loc = loc[2*subexpr : 2+2*subexpr]
|
||||
loc[0] += pos
|
||||
loc[1] += pos
|
||||
return loc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func skip(text []byte, start int) int {
|
||||
for pos := range string(text) {
|
||||
if start--; start <= 0 {
|
||||
return pos
|
||||
}
|
||||
}
|
||||
return len(text)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package regexp
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
@@ -29,18 +31,47 @@ func TestRegister(t *testing.T) {
|
||||
{`regexp_like('Hello', 'elo')`, "0"},
|
||||
{`regexp_like('Hello', 'ell')`, "1"},
|
||||
{`regexp_like('Hello', 'el.')`, "1"},
|
||||
{`regexp_count('Hello', 'l')`, "2"},
|
||||
{`regexp_instr('Hello', 'el.')`, "2"},
|
||||
{`regexp_instr('Hello', '.', 6)`, ""},
|
||||
{`regexp_substr('Hello', 'el.')`, "ell"},
|
||||
{`regexp_replace('Hello', 'llo', 'll')`, "Hell"},
|
||||
// https://www.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"},
|
||||
{`regexp_instr('ABCDEFGHI', '(?i)(c..)(...)', 1, 1, 0, 2)`, "6"},
|
||||
{`regexp_substr('number of your street, town zip, FR', '[^,]+', 1, 2)`, " town zip"},
|
||||
{`regexp_substr('ABCDEFGHI', '(?i)(c..)(...)', 1, 1, 2)`, "FGH"},
|
||||
{`regexp_replace('foobarbaz', 'b..', 'X', 1, 1)`, "fooXbaz"},
|
||||
{`regexp_replace('foobarbaz', 'b..', 'X')`, "fooXX"},
|
||||
{`regexp_replace('foobarbaz', 'b(..)', 'X${1}Y')`, "fooXarYXazY"},
|
||||
{`regexp_replace('A PostgreSQL function', '(?i)a|e|i|o|u', 'X', 1, 0)`, "X PXstgrXSQL fXnctXXn"},
|
||||
{`regexp_replace('A PostgreSQL function', '(?i)a|e|i|o|u', 'X', 1, 3)`, "A PostgrXSQL function"},
|
||||
// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/REGEXP_COUNT.html
|
||||
{`regexp_count('123123123123123', '(12)3', 1)`, "5"},
|
||||
{`regexp_count('123123123123', '123', 3)`, "3"},
|
||||
{`regexp_instr('500 Oracle Parkway, Redwood Shores, CA', '[^ ]+', 1, 6)`, "37"},
|
||||
{`regexp_instr('500 Oracle Parkway, Redwood Shores, CA', '(?i)[s|r|p][[:alpha:]]{6}', 3, 2, 1)`, "28"},
|
||||
{`regexp_instr('1234567890', '(123)(4(56)(78))', 1, 1, 0, 1)`, "1"},
|
||||
{`regexp_instr('1234567890', '(123)(4(56)(78))', 1, 1, 0, 2)`, "4"},
|
||||
{`regexp_instr('1234567890', '(123)(4(56)(78))', 1, 1, 0, 4)`, "7"},
|
||||
{`regexp_substr('500 Oracle Parkway, Redwood Shores, CA', ',[^,]+,')`, ", Redwood Shores,"},
|
||||
{`regexp_substr('http://www.example.com/products', 'http://([[:alnum:]]+\.?){3,4}/?')`, "http://www.example.com/"},
|
||||
{`regexp_substr('1234567890', '(123)(4(56)(78))', 1, 1, 1)`, "123"},
|
||||
{`regexp_substr('1234567890', '(123)(4(56)(78))', 1, 1, 4)`, "78"},
|
||||
{`regexp_substr('123123123123', '1(.)3', 3, 2, 1)`, "2"},
|
||||
{`regexp_replace('500 Oracle Parkway, Redwood Shores, CA', '( ){2,}', ' ')`, "500 Oracle Parkway, Redwood Shores, CA"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
var got string
|
||||
var got sql.NullString
|
||||
err := db.QueryRow(`SELECT ` + tt.test).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q, want %q", got, tt.want)
|
||||
if got.String != tt.want {
|
||||
t.Errorf("got %q, want %q", got.String, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +89,8 @@ func TestRegister_errors(t *testing.T) {
|
||||
tests := []string{
|
||||
`'' REGEXP ?`,
|
||||
`regexp_like('', ?)`,
|
||||
`regexp_count('', ?)`,
|
||||
`regexp_instr('', ?)`,
|
||||
`regexp_substr('', ?)`,
|
||||
`regexp_replace('', ?, '')`,
|
||||
}
|
||||
@@ -69,3 +102,25 @@ func TestRegister_errors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
re string
|
||||
want string
|
||||
}{
|
||||
{``, ""},
|
||||
{`a`, "a"},
|
||||
{`a*`, "*"},
|
||||
{`a+`, "a*"},
|
||||
{`ab*`, "a*"},
|
||||
{`ab+`, "ab*"},
|
||||
{`a\?b`, "a*"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.re, func(t *testing.T) {
|
||||
if got := GlobPrefix(regexp.MustCompile(tt.re)); got != tt.want {
|
||||
t.Errorf("GlobPrefix() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (*table, error) {
|
||||
|
||||
sql := "SELECT * FROM\n" + arg[0]
|
||||
|
||||
stmt, _, err := db.Prepare(sql)
|
||||
stmt, _, err := db.PrepareFlags(sql, sqlite3.PREPARE_PERSISTENT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -26,21 +26,21 @@ func (b *boolean) Value(ctx sqlite3.Context) {
|
||||
}
|
||||
|
||||
func (b *boolean) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if arg[0].Type() == sqlite3.NULL {
|
||||
return
|
||||
}
|
||||
if arg[0].Bool() {
|
||||
a := arg[0]
|
||||
if a.Bool() {
|
||||
b.count++
|
||||
}
|
||||
b.total++
|
||||
if a.Type() != sqlite3.NULL {
|
||||
b.total++
|
||||
}
|
||||
}
|
||||
|
||||
func (b *boolean) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if arg[0].Type() == sqlite3.NULL {
|
||||
return
|
||||
}
|
||||
if arg[0].Bool() {
|
||||
a := arg[0]
|
||||
if a.Bool() {
|
||||
b.count--
|
||||
}
|
||||
b.total--
|
||||
if a.Type() != sqlite3.NULL {
|
||||
b.total--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
const (
|
||||
median = iota
|
||||
percentile_100
|
||||
percentile_cont
|
||||
percentile_disc
|
||||
)
|
||||
@@ -28,17 +29,25 @@ type percentile struct {
|
||||
}
|
||||
|
||||
func (q *percentile) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if a := arg[0]; a.NumericType() != sqlite3.NULL {
|
||||
q.nums = append(q.nums, a.Float())
|
||||
a := arg[0]
|
||||
f := a.Float()
|
||||
if f != 0.0 || a.NumericType() != sqlite3.NULL {
|
||||
q.nums = append(q.nums, f)
|
||||
}
|
||||
if q.kind != median {
|
||||
q.arg1 = append(q.arg1[:0], arg[1].RawText()...)
|
||||
if q.kind != median && q.arg1 == nil {
|
||||
q.arg1 = append(q.arg1, arg[1].RawText()...)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *percentile) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
// Implementing inverse allows certain queries that don't really need it to succeed.
|
||||
ctx.ResultError(util.ErrorString("percentile: may not be used as a window function"))
|
||||
a := arg[0]
|
||||
f := a.Float()
|
||||
if f != 0.0 || a.NumericType() != sqlite3.NULL {
|
||||
i := slices.Index(q.nums, f)
|
||||
l := len(q.nums) - 1
|
||||
q.nums[i] = q.nums[l]
|
||||
q.nums = q.nums[:l]
|
||||
}
|
||||
}
|
||||
|
||||
func (q *percentile) Value(ctx sqlite3.Context) {
|
||||
@@ -52,13 +61,13 @@ func (q *percentile) Value(ctx sqlite3.Context) {
|
||||
floats []float64
|
||||
)
|
||||
if q.kind == median {
|
||||
float, err = getPercentile(q.nums, 0.5, false)
|
||||
float, err = q.at(0.5)
|
||||
ctx.ResultFloat(float)
|
||||
} else if err = json.Unmarshal(q.arg1, &float); err == nil {
|
||||
float, err = getPercentile(q.nums, float, q.kind == percentile_disc)
|
||||
float, err = q.at(float)
|
||||
ctx.ResultFloat(float)
|
||||
} else if err = json.Unmarshal(q.arg1, &floats); err == nil {
|
||||
err = getPercentiles(q.nums, floats, q.kind == percentile_disc)
|
||||
err = q.atMore(floats)
|
||||
ctx.ResultJSON(floats)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -66,25 +75,28 @@ func (q *percentile) Value(ctx sqlite3.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func getPercentile(nums []float64, pos float64, disc bool) (float64, error) {
|
||||
func (q *percentile) at(pos float64) (float64, error) {
|
||||
if q.kind == percentile_100 {
|
||||
pos = pos / 100
|
||||
}
|
||||
if pos < 0 || pos > 1 {
|
||||
return 0, util.ErrorString("invalid pos")
|
||||
}
|
||||
|
||||
i, f := math.Modf(pos * float64(len(nums)-1))
|
||||
m0 := quick.Select(nums, int(i))
|
||||
i, f := math.Modf(pos * float64(len(q.nums)-1))
|
||||
m0 := quick.Select(q.nums, int(i))
|
||||
|
||||
if f == 0 || disc {
|
||||
if f == 0 || q.kind == percentile_disc {
|
||||
return m0, nil
|
||||
}
|
||||
|
||||
m1 := slices.Min(nums[int(i)+1:])
|
||||
return math.FMA(f, m1, math.FMA(-f, m0, m0)), nil
|
||||
m1 := slices.Min(q.nums[int(i)+1:])
|
||||
return util.Lerp(m0, m1, f), nil
|
||||
}
|
||||
|
||||
func getPercentiles(nums []float64, pos []float64, disc bool) error {
|
||||
func (q *percentile) atMore(pos []float64) error {
|
||||
for i := range pos {
|
||||
v, err := getPercentile(nums, pos[i], disc)
|
||||
v, err := q.at(pos[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ func TestRegister_percentile(t *testing.T) {
|
||||
stmt, _, err := db.Prepare(`
|
||||
SELECT
|
||||
median(x),
|
||||
percentile(x, 50),
|
||||
percentile_disc(x, 0.5),
|
||||
percentile_cont(x, '[0.25, 0.5, 0.75]')
|
||||
FROM data`)
|
||||
@@ -41,11 +42,14 @@ func TestRegister_percentile(t *testing.T) {
|
||||
if got := stmt.ColumnFloat(0); got != 10 {
|
||||
t.Errorf("got %v, want 10", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(1); got != 7 {
|
||||
if got := stmt.ColumnFloat(1); got != 10 {
|
||||
t.Errorf("got %v, want 10", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(2); got != 7 {
|
||||
t.Errorf("got %v, want 7", got)
|
||||
}
|
||||
var got []float64
|
||||
if err := stmt.ColumnJSON(2, &got); err != nil {
|
||||
if err := stmt.ColumnJSON(3, &got); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !slices.Equal(got, []float64{6.25, 10, 13.75}) {
|
||||
@@ -54,9 +58,44 @@ func TestRegister_percentile(t *testing.T) {
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
stmt, _, err = db.Prepare(`
|
||||
SELECT
|
||||
median(x) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)
|
||||
FROM data`)
|
||||
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() {
|
||||
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() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
stmt, _, err = db.Prepare(`
|
||||
SELECT
|
||||
median(x),
|
||||
percentile(x, 50),
|
||||
percentile_disc(x, 0.5),
|
||||
percentile_cont(x, '[0.25, 0.5, 0.75]')
|
||||
FROM data
|
||||
@@ -71,8 +110,11 @@ func TestRegister_percentile(t *testing.T) {
|
||||
if got := stmt.ColumnFloat(1); got != 4 {
|
||||
t.Errorf("got %v, want 4", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(2); got != 4 {
|
||||
t.Errorf("got %v, want 4", got)
|
||||
}
|
||||
var got []float64
|
||||
if err := stmt.ColumnJSON(2, &got); err != nil {
|
||||
if err := stmt.ColumnJSON(3, &got); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !slices.Equal(got, []float64{4, 4, 4}) {
|
||||
@@ -84,6 +126,7 @@ func TestRegister_percentile(t *testing.T) {
|
||||
stmt, _, err = db.Prepare(`
|
||||
SELECT
|
||||
median(x),
|
||||
percentile(x, 50),
|
||||
percentile_disc(x, 0.5),
|
||||
percentile_cont(x, '[0.25, 0.5, 0.75]')
|
||||
FROM data
|
||||
@@ -101,6 +144,9 @@ func TestRegister_percentile(t *testing.T) {
|
||||
if got := stmt.ColumnType(2); got != sqlite3.NULL {
|
||||
t.Error("want NULL")
|
||||
}
|
||||
if got := stmt.ColumnType(3); got != sqlite3.NULL {
|
||||
t.Error("want NULL")
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ import (
|
||||
|
||||
// Register registers statistics functions.
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
const 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)),
|
||||
@@ -71,9 +72,10 @@ func Register(db *sqlite3.Conn) error {
|
||||
db.CreateWindowFunction("regr_intercept", 2, flags, newCovariance(regr_intercept)),
|
||||
db.CreateWindowFunction("regr_count", 2, flags, newCovariance(regr_count)),
|
||||
db.CreateWindowFunction("regr_json", 2, flags, newCovariance(regr_json)),
|
||||
db.CreateWindowFunction("median", 1, flags, newPercentile(median)),
|
||||
db.CreateWindowFunction("percentile_cont", 2, flags, newPercentile(percentile_cont)),
|
||||
db.CreateWindowFunction("percentile_disc", 2, flags, newPercentile(percentile_disc)),
|
||||
db.CreateWindowFunction("median", 1, order, newPercentile(median)),
|
||||
db.CreateWindowFunction("percentile", 2, order, newPercentile(percentile_100)),
|
||||
db.CreateWindowFunction("percentile_cont", 2, order, newPercentile(percentile_cont)),
|
||||
db.CreateWindowFunction("percentile_disc", 2, order, newPercentile(percentile_disc)),
|
||||
db.CreateWindowFunction("every", 1, flags, newBoolean(every)),
|
||||
db.CreateWindowFunction("some", 1, flags, newBoolean(some)))
|
||||
}
|
||||
@@ -121,14 +123,18 @@ func (fn *variance) Value(ctx sqlite3.Context) {
|
||||
}
|
||||
|
||||
func (fn *variance) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if a := arg[0]; a.NumericType() != sqlite3.NULL {
|
||||
fn.enqueue(a.Float())
|
||||
a := arg[0]
|
||||
f := a.Float()
|
||||
if f != 0.0 || a.NumericType() != sqlite3.NULL {
|
||||
fn.enqueue(f)
|
||||
}
|
||||
}
|
||||
|
||||
func (fn *variance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if a := arg[0]; a.NumericType() != sqlite3.NULL {
|
||||
fn.dequeue(a.Float())
|
||||
a := arg[0]
|
||||
f := a.Float()
|
||||
if f != 0.0 || a.NumericType() != sqlite3.NULL {
|
||||
fn.dequeue(f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,15 +183,23 @@ func (fn *covariance) Value(ctx sqlite3.Context) {
|
||||
}
|
||||
|
||||
func (fn *covariance) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
a, b := arg[0], arg[1]
|
||||
if a.NumericType() != sqlite3.NULL && b.NumericType() != sqlite3.NULL {
|
||||
fn.enqueue(a.Float(), b.Float())
|
||||
b, a := arg[1], arg[0] // avoid a bounds check
|
||||
fa := a.Float()
|
||||
fb := b.Float()
|
||||
if true &&
|
||||
(fa != 0.0 || a.NumericType() != sqlite3.NULL) &&
|
||||
(fb != 0.0 || b.NumericType() != sqlite3.NULL) {
|
||||
fn.enqueue(fa, fb)
|
||||
}
|
||||
}
|
||||
|
||||
func (fn *covariance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
a, b := arg[0], arg[1]
|
||||
if a.NumericType() != sqlite3.NULL && b.NumericType() != sqlite3.NULL {
|
||||
fn.dequeue(a.Float(), b.Float())
|
||||
b, a := arg[1], arg[0] // avoid a bounds check
|
||||
fa := a.Float()
|
||||
fb := b.Float()
|
||||
if true &&
|
||||
(fa != 0.0 || a.NumericType() != sqlite3.NULL) &&
|
||||
(fb != 0.0 || b.NumericType() != sqlite3.NULL) {
|
||||
fn.dequeue(fa, fb)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
// - LIKE and REGEXP operators,
|
||||
// - collation sequences.
|
||||
//
|
||||
// It also provides, from PostgreSQL:
|
||||
// - unaccent(),
|
||||
// - initcap().
|
||||
//
|
||||
// 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;
|
||||
@@ -21,26 +25,44 @@ import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Set RegisterLike 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
|
||||
var RegisterLike = true
|
||||
|
||||
// Register registers Unicode aware functions for a database connection.
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
return errors.Join(
|
||||
db.CreateFunction("like", 2, flags, like),
|
||||
db.CreateFunction("like", 3, flags, like),
|
||||
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
var errs util.ErrorJoiner
|
||||
if RegisterLike {
|
||||
errs.Join(
|
||||
db.CreateFunction("like", 2, flags, like),
|
||||
db.CreateFunction("like", 3, flags, like))
|
||||
}
|
||||
errs.Join(
|
||||
db.CreateFunction("upper", 1, flags, upper),
|
||||
db.CreateFunction("upper", 2, flags, upper),
|
||||
db.CreateFunction("lower", 1, flags, lower),
|
||||
db.CreateFunction("lower", 2, flags, lower),
|
||||
db.CreateFunction("regexp", 2, flags, regex),
|
||||
db.CreateFunction("initcap", 1, flags, initcap),
|
||||
db.CreateFunction("initcap", 2, flags, initcap),
|
||||
db.CreateFunction("unaccent", 1, flags, unaccent),
|
||||
db.CreateFunction("icu_load_collation", 2, sqlite3.DIRECTONLY,
|
||||
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
name := arg[1].Text()
|
||||
@@ -48,12 +70,13 @@ func Register(db *sqlite3.Conn) error {
|
||||
return
|
||||
}
|
||||
|
||||
err := RegisterCollation(db, arg[0].Text(), name)
|
||||
err := RegisterCollation(ctx.Conn(), arg[0].Text(), name)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
}))
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// RegisterCollation registers a Unicode collation sequence for a database connection.
|
||||
@@ -65,6 +88,15 @@ func RegisterCollation(db *sqlite3.Conn, locale, name string) error {
|
||||
return db.CreateCollation(name, collate.New(tag).Compare)
|
||||
}
|
||||
|
||||
// RegisterCollationsNeeded registers Unicode collation sequences on demand for a database connection.
|
||||
func RegisterCollationsNeeded(db *sqlite3.Conn) error {
|
||||
return db.CollationNeeded(func(db *sqlite3.Conn, name string) {
|
||||
if tag, err := language.Parse(name); err == nil {
|
||||
db.CreateCollation(name, collate.New(tag).Compare)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func upper(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if len(arg) == 1 {
|
||||
ctx.ResultRawText(bytes.ToUpper(arg[0].RawText()))
|
||||
@@ -103,6 +135,35 @@ func lower(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
|
||||
}
|
||||
|
||||
func initcap(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if len(arg) == 1 {
|
||||
ctx.ResultRawText(bytes.Title(arg[0].RawText()))
|
||||
return
|
||||
}
|
||||
cs, ok := ctx.GetAuxData(1).(cases.Caser)
|
||||
if !ok {
|
||||
t, err := language.Parse(arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return // notest
|
||||
}
|
||||
c := cases.Title(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
cs = c
|
||||
}
|
||||
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
|
||||
}
|
||||
|
||||
func unaccent(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
unaccent := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||
res, _, err := transform.Bytes(unaccent, arg[0].RawText())
|
||||
if err != nil {
|
||||
ctx.ResultError(err) // notest
|
||||
} else {
|
||||
ctx.ResultRawText(res)
|
||||
}
|
||||
}
|
||||
|
||||
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, ok := ctx.GetAuxData(0).(*regexp.Regexp)
|
||||
if !ok {
|
||||
@@ -128,6 +189,7 @@ func like(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = arg[1] // bounds check
|
||||
|
||||
type likeData struct {
|
||||
*regexp.Regexp
|
||||
|
||||
@@ -47,6 +47,9 @@ func TestRegister(t *testing.T) {
|
||||
{`upper('istanbul', 'tr-TR')`, "İSTANBUL"},
|
||||
{`lower('Dünyanın İlk Borsası', 'tr-TR')`, "dünyanın ilk borsası"},
|
||||
{`upper('Dünyanın İlk Borsası', 'tr-TR')`, "DÜNYANIN İLK BORSASI"},
|
||||
{`initcap('Kad je hladno Marko nosi džemper')`, "Kad Je Hladno Marko Nosi Džemper"},
|
||||
{`initcap('Kad je hladno Marko nosi džemper', 'hr-HR')`, "Kad Je Hladno Marko Nosi Džemper"},
|
||||
{`unaccent('Hôtel')`, "Hotel"},
|
||||
{`'Hello' REGEXP 'ell'`, "1"},
|
||||
{`'Hello' REGEXP 'el.'`, "1"},
|
||||
{`'Hello' LIKE 'hel_'`, "0"},
|
||||
@@ -92,7 +95,7 @@ func TestRegister_collation(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT icu_load_collation('fr_FR', 'french')`)
|
||||
err = db.Exec(`SELECT icu_load_collation('fr-FR', 'french')`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -127,6 +130,57 @@ func TestRegister_collation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterCollationsNeeded(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
RegisterCollationsNeeded(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO words (word) VALUES ('côte'), ('cote'), ('coter'), ('coté'), ('cotée'), ('côté')`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT word FROM words ORDER BY word COLLATE fr_FR`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
got, want := []string{}, []string{"cote", "coté", "côte", "côté", "cotée", "coter"}
|
||||
|
||||
for stmt.Step() {
|
||||
got = append(got, stmt.ColumnText(0))
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Error("not equal")
|
||||
}
|
||||
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
//
|
||||
// Converts a UUID into a 16-byte blob.
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
return errors.Join(
|
||||
db.CreateFunction("uuid", 0, sqlite3.INNOCUOUS, generate),
|
||||
db.CreateFunction("uuid", 1, sqlite3.INNOCUOUS, generate),
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// Register registers the zorder and unzorder SQL functions.
|
||||
func Register(db *sqlite3.Conn) error {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
return errors.Join(
|
||||
db.CreateFunction("zorder", -1, flags, zorder),
|
||||
db.CreateFunction("unzorder", 3, flags, unzorder))
|
||||
@@ -47,9 +47,9 @@ func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
}
|
||||
|
||||
func unzorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
z := arg[0].Int64()
|
||||
n := arg[1].Int64()
|
||||
i := arg[2].Int64()
|
||||
n := arg[1].Int64()
|
||||
z := arg[0].Int64()
|
||||
|
||||
var k int
|
||||
var x int64
|
||||
|
||||
31
func.go
31
func.go
@@ -4,8 +4,9 @@ import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// CollationNeeded registers a callback to be invoked
|
||||
@@ -33,16 +34,23 @@ func (c *Conn) CollationNeeded(cb func(db *Conn, name string)) error {
|
||||
// one or more unknown collating sequences.
|
||||
func (c Conn) AnyCollationNeeded() error {
|
||||
r := c.call("sqlite3_anycollseq_init", uint64(c.handle), 0, 0)
|
||||
return c.error(r)
|
||||
if err := c.error(r); err != nil {
|
||||
return err
|
||||
}
|
||||
c.collation = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var funcPtr uint32
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
funcPtr := util.AddHandle(c.ctx, fn)
|
||||
if fn != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, fn)
|
||||
}
|
||||
r := c.call("sqlite3_create_collation_go",
|
||||
uint64(c.handle), uint64(namePtr), uint64(funcPtr))
|
||||
return c.error(r)
|
||||
@@ -52,9 +60,12 @@ func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
func (c *Conn) CreateFunction(name string, nArg int, flag FunctionFlag, fn ScalarFunction) error {
|
||||
var funcPtr uint32
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
funcPtr := util.AddHandle(c.ctx, fn)
|
||||
if fn != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, fn)
|
||||
}
|
||||
r := c.call("sqlite3_create_function_go",
|
||||
uint64(c.handle), uint64(namePtr), uint64(nArg),
|
||||
uint64(flag), uint64(funcPtr))
|
||||
@@ -71,10 +82,13 @@ type ScalarFunction func(ctx Context, arg ...Value)
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn func() AggregateFunction) error {
|
||||
var funcPtr uint32
|
||||
defer c.arena.mark()()
|
||||
call := "sqlite3_create_aggregate_function_go"
|
||||
namePtr := c.arena.string(name)
|
||||
funcPtr := util.AddHandle(c.ctx, fn)
|
||||
if fn != nil {
|
||||
funcPtr = util.AddHandle(c.ctx, fn)
|
||||
}
|
||||
call := "sqlite3_create_aggregate_function_go"
|
||||
if _, ok := fn().(WindowFunction); ok {
|
||||
call = "sqlite3_create_window_function_go"
|
||||
}
|
||||
@@ -184,11 +198,12 @@ func callbackAggregate(db *Conn, pAgg, pApp uint32) (AggregateFunction, uint32)
|
||||
|
||||
// We need to create the aggregate.
|
||||
fn := util.GetHandle(db.ctx, pApp).(func() AggregateFunction)()
|
||||
handle := util.AddHandle(db.ctx, fn)
|
||||
if pAgg != 0 {
|
||||
handle := util.AddHandle(db.ctx, fn)
|
||||
util.WriteUint32(db.mod, pAgg, handle)
|
||||
return fn, handle
|
||||
}
|
||||
return fn, handle
|
||||
return fn, 0
|
||||
}
|
||||
|
||||
func callbackArgs(db *Conn, arg []Value, pArg uint32) {
|
||||
|
||||
20
go.mod
20
go.mod
@@ -5,18 +5,20 @@ go 1.21
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/dchest/siphash v1.2.3
|
||||
github.com/ncruces/julianday v1.0.0
|
||||
github.com/ncruces/sort v0.1.2
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/tetratelabs/wazero v1.8.0
|
||||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/sys v0.24.0
|
||||
golang.org/x/text v0.17.0
|
||||
lukechampine.com/adiantum v1.1.1
|
||||
github.com/tetratelabs/wazero v1.8.2
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/sys v0.29.0
|
||||
)
|
||||
|
||||
require github.com/google/uuid v1.6.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.10.0 // test
|
||||
golang.org/x/text v0.21.0 // ext/unicode
|
||||
lukechampine.com/adiantum v1.1.1 // vfs/adiantum
|
||||
)
|
||||
|
||||
retract v0.4.0 // tagged from the wrong branch
|
||||
|
||||
20
go.sum
20
go.sum
@@ -8,15 +8,15 @@ github.com/ncruces/sort v0.1.2 h1:zKQ9CA4fpHPF6xsUhRTfi5EEryspuBpe/QA4VWQOV1U=
|
||||
github.com/ncruces/sort v0.1.2/go.mod h1:vEJUTBJtebIuCMmXD18GKo5GJGhsay+xZFOoBEIXFmE=
|
||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
|
||||
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
|
||||
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
|
||||
@@ -17,23 +17,11 @@ var (
|
||||
indexRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)CREATE(?: UNIQUE)? INDEX [%v]?[\w\d-]+[%v]?(?s:.*?)ON (.*)$`, sqliteSeparator, sqliteSeparator))
|
||||
tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?`, sqliteSeparator, sqliteSeparator))
|
||||
separatorRegexp = regexp.MustCompile(fmt.Sprintf("[%v]", sqliteSeparator))
|
||||
columnsRegexp = regexp.MustCompile(fmt.Sprintf(`[(,][%v]?(\w+)[%v]?`, sqliteSeparator, sqliteSeparator))
|
||||
columnRegexp = regexp.MustCompile(fmt.Sprintf(`^[%v]?([\w\d]+)[%v]?\s+([\w\(\)\d]+)(.*)$`, sqliteSeparator, sqliteSeparator))
|
||||
defaultValueRegexp = regexp.MustCompile(`(?i) DEFAULT \(?(.+)?\)?( |COLLATE|GENERATED|$)`)
|
||||
regRealDataType = regexp.MustCompile(`[^\d](\d+)[^\d]?`)
|
||||
)
|
||||
|
||||
func getAllColumns(s string) []string {
|
||||
allMatches := columnsRegexp.FindAllStringSubmatch(s, -1)
|
||||
columns := make([]string, 0, len(allMatches))
|
||||
for _, matches := range allMatches {
|
||||
if len(matches) > 1 {
|
||||
columns = append(columns, matches[1])
|
||||
}
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
type ddl struct {
|
||||
head string
|
||||
fields []string
|
||||
@@ -110,9 +98,10 @@ func parseDDL(strs ...string) (*ddl, error) {
|
||||
if strings.HasPrefix(fUpper, "CONSTRAINT") {
|
||||
matches := uniqueRegexp.FindStringSubmatch(f)
|
||||
if len(matches) > 0 {
|
||||
if columns := getAllColumns(matches[1]); len(columns) == 1 {
|
||||
cols, err := parseAllColumns(matches[1])
|
||||
if err == nil && len(cols) == 1 {
|
||||
for idx, column := range result.columns {
|
||||
if column.NameValue.String == columns[0] {
|
||||
if column.NameValue.String == cols[0] {
|
||||
column.UniqueValue = sql.NullBool{Bool: true, Valid: true}
|
||||
result.columns[idx] = column
|
||||
break
|
||||
@@ -123,12 +112,15 @@ func parseDDL(strs ...string) (*ddl, error) {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(fUpper, "PRIMARY KEY") {
|
||||
for _, name := range getAllColumns(f) {
|
||||
for idx, column := range result.columns {
|
||||
if column.NameValue.String == name {
|
||||
column.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true}
|
||||
result.columns[idx] = column
|
||||
break
|
||||
cols, err := parseAllColumns(f)
|
||||
if err == nil {
|
||||
for _, name := range cols {
|
||||
for idx, column := range result.columns {
|
||||
if column.NameValue.String == name {
|
||||
column.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true}
|
||||
result.columns[idx] = column
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
117
gormlite/ddlmod_parse_all_columns.go
Normal file
117
gormlite/ddlmod_parse_all_columns.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type parseAllColumnsState int
|
||||
|
||||
const (
|
||||
parseAllColumnsState_NONE parseAllColumnsState = iota
|
||||
parseAllColumnsState_Beginning
|
||||
parseAllColumnsState_ReadingRawName
|
||||
parseAllColumnsState_ReadingQuotedName
|
||||
parseAllColumnsState_EndOfName
|
||||
parseAllColumnsState_State_End
|
||||
)
|
||||
|
||||
func parseAllColumns(in string) ([]string, error) {
|
||||
s := []rune(in)
|
||||
columns := make([]string, 0)
|
||||
state := parseAllColumnsState_NONE
|
||||
quote := rune(0)
|
||||
name := make([]rune, 0)
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch state {
|
||||
case parseAllColumnsState_NONE:
|
||||
if s[i] == '(' {
|
||||
state = parseAllColumnsState_Beginning
|
||||
}
|
||||
case parseAllColumnsState_Beginning:
|
||||
if isSpace(s[i]) {
|
||||
continue
|
||||
}
|
||||
if isQuote(s[i]) {
|
||||
state = parseAllColumnsState_ReadingQuotedName
|
||||
quote = s[i]
|
||||
continue
|
||||
}
|
||||
if s[i] == '[' {
|
||||
state = parseAllColumnsState_ReadingQuotedName
|
||||
quote = ']'
|
||||
continue
|
||||
} else if s[i] == ')' {
|
||||
return columns, fmt.Errorf("unexpected token: %s", string(s[i]))
|
||||
}
|
||||
state = parseAllColumnsState_ReadingRawName
|
||||
name = append(name, s[i])
|
||||
case parseAllColumnsState_ReadingRawName:
|
||||
if isSeparator(s[i]) {
|
||||
state = parseAllColumnsState_Beginning
|
||||
columns = append(columns, string(name))
|
||||
name = make([]rune, 0)
|
||||
continue
|
||||
}
|
||||
if s[i] == ')' {
|
||||
state = parseAllColumnsState_State_End
|
||||
columns = append(columns, string(name))
|
||||
}
|
||||
if isQuote(s[i]) {
|
||||
return nil, fmt.Errorf("unexpected token: %s", string(s[i]))
|
||||
}
|
||||
if isSpace(s[i]) {
|
||||
state = parseAllColumnsState_EndOfName
|
||||
columns = append(columns, string(name))
|
||||
name = make([]rune, 0)
|
||||
continue
|
||||
}
|
||||
name = append(name, s[i])
|
||||
case parseAllColumnsState_ReadingQuotedName:
|
||||
if s[i] == quote {
|
||||
// check if quote character is escaped
|
||||
if i+1 < len(s) && s[i+1] == quote {
|
||||
name = append(name, quote)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
state = parseAllColumnsState_EndOfName
|
||||
columns = append(columns, string(name))
|
||||
name = make([]rune, 0)
|
||||
continue
|
||||
}
|
||||
name = append(name, s[i])
|
||||
case parseAllColumnsState_EndOfName:
|
||||
if isSpace(s[i]) {
|
||||
continue
|
||||
}
|
||||
if isSeparator(s[i]) {
|
||||
state = parseAllColumnsState_Beginning
|
||||
continue
|
||||
}
|
||||
if s[i] == ')' {
|
||||
state = parseAllColumnsState_State_End
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected token: %s", string(s[i]))
|
||||
case parseAllColumnsState_State_End:
|
||||
break
|
||||
}
|
||||
}
|
||||
if state != parseAllColumnsState_State_End {
|
||||
return nil, errors.New("unexpected end")
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
func isQuote(r rune) bool {
|
||||
return r == '`' || r == '"' || r == '\''
|
||||
}
|
||||
|
||||
func isSeparator(r rune) bool {
|
||||
return r == ','
|
||||
}
|
||||
48
gormlite/ddlmod_parse_all_columns_test.go
Normal file
48
gormlite/ddlmod_parse_all_columns_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package gormlite
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseAllColumns(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "Simple case",
|
||||
input: "PRIMARY KEY (column1, column2)",
|
||||
expected: []string{"column1", "column2"},
|
||||
},
|
||||
{
|
||||
name: "Quoted column name",
|
||||
input: "PRIMARY KEY (`column,xxx`, \"column 2\", \"column)3\", 'column''4', \"column\"\"5\")",
|
||||
expected: []string{"column,xxx", "column 2", "column)3", "column'4", "column\"5"},
|
||||
},
|
||||
{
|
||||
name: "Japanese column name",
|
||||
input: "PRIMARY KEY (カラム1, `カラム2`)",
|
||||
expected: []string{"カラム1", "カラム2"},
|
||||
},
|
||||
{
|
||||
name: "Column name quoted with []",
|
||||
input: "PRIMARY KEY ([column1], [column2])",
|
||||
expected: []string{"column1", "column2"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cols, err := parseAllColumns(tt.input)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to parse columns: %s", err)
|
||||
}
|
||||
if len(cols) != len(tt.expected) {
|
||||
t.Errorf("Expected %d columns, got %d", len(tt.expected), len(cols))
|
||||
}
|
||||
for i, col := range cols {
|
||||
if col != tt.expected[i] {
|
||||
t.Errorf("Expected %s, got %s", tt.expected[i], col)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@ set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/ddlmod.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/ddlmod_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/error_translator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/migrator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/sqlite.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/sqlite_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.6/sqlite_test.go"
|
||||
curl -#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 -#L "https://github.com/glebarez/sqlite/raw/v1.11.0/sqlite_error_translator_test.go" > error_translator_test.go
|
||||
@@ -3,10 +3,12 @@ package gormlite
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Translate it will translate the error to native gorm errors.
|
||||
func (_Dialector) Translate(err error) error {
|
||||
switch {
|
||||
case
|
||||
|
||||
@@ -5,18 +5,17 @@ import (
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func TestErrorTranslator(t *testing.T) {
|
||||
// This is the DSN of the in-memory SQLite database for these tests.
|
||||
const InMemoryDSN = "file:testdatabase?mode=memory&cache=shared"
|
||||
|
||||
// This is the example object for testing the unique constraint error
|
||||
type Article struct {
|
||||
ArticleNumber string `gorm:"unique"`
|
||||
}
|
||||
|
||||
db, err := gorm.Open(Open(InMemoryDSN), &gorm.Config{
|
||||
db, err := gorm.Open(Open(memdb.TestDB(t)), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
TranslateError: true})
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@ go 1.21
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/ncruces/go-sqlite3 v0.18.0
|
||||
gorm.io/gorm v1.25.11
|
||||
github.com/ncruces/go-sqlite3 v0.21.3
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
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.8.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.2 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.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.18.0 h1:aH7WGzOC0CYpUPG1LdFg7JApybiuXgYUE2itzLBwhPM=
|
||||
github.com/ncruces/go-sqlite3 v0.18.0/go.mod h1:eEOyZnW1dGTJ+zDpMuzfYamEUBtdFz5zeYhqLBtHxvM=
|
||||
github.com/ncruces/go-sqlite3 v0.21.3 h1:hHkfNQLcbnxPJZhC/RGw9SwP3bfkv/Y0xUHWsr1CdMQ=
|
||||
github.com/ncruces/go-sqlite3 v0.21.3/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
|
||||
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.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
|
||||
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
|
||||
@@ -14,6 +14,11 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
)
|
||||
|
||||
type _Dialector struct {
|
||||
DSN string
|
||||
Conn gorm.ConnPool
|
||||
}
|
||||
|
||||
// Open opens a GORM dialector from a data source name.
|
||||
func Open(dsn string) gorm.Dialector {
|
||||
return &_Dialector{DSN: dsn}
|
||||
@@ -24,11 +29,6 @@ func OpenDB(db gorm.ConnPool) gorm.Dialector {
|
||||
return &_Dialector{Conn: db}
|
||||
}
|
||||
|
||||
type _Dialector struct {
|
||||
DSN string
|
||||
Conn gorm.ConnPool
|
||||
}
|
||||
|
||||
func (dialector _Dialector) Name() string {
|
||||
return "sqlite"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ rm -rf gorm/ tests/
|
||||
go work use -r .
|
||||
go test
|
||||
|
||||
git clone --branch v1.25.11 --filter=blob:none https://github.com/go-gorm/gorm.git
|
||||
git clone --branch v1.25.12 --filter=blob:none https://github.com/go-gorm/gorm.git
|
||||
mv gorm/tests tests
|
||||
rm -rf gorm/
|
||||
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
//go:build !(unix || windows) || sqlite3_nosys
|
||||
//go:build !unix && !windows
|
||||
|
||||
package alloc
|
||||
|
||||
import "github.com/tetratelabs/wazero/experimental"
|
||||
|
||||
func Virtual(cap, max uint64) experimental.LinearMemory {
|
||||
return Slice(cap, max)
|
||||
func NewMemory(cap, max uint64) experimental.LinearMemory {
|
||||
return &sliceMemory{make([]byte, 0, cap)}
|
||||
}
|
||||
|
||||
type sliceMemory struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (b *sliceMemory) Free() {}
|
||||
|
||||
func (b *sliceMemory) Reallocate(size uint64) []byte {
|
||||
if cap := uint64(cap(b.buf)); size > cap {
|
||||
b.buf = append(b.buf[:cap], make([]byte, size-cap)...)
|
||||
} else {
|
||||
b.buf = b.buf[:size]
|
||||
}
|
||||
return b.buf
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//go:build !(darwin || linux) || !(amd64 || arm64 || riscv64) || sqlite3_noshm || sqlite3_nosys
|
||||
|
||||
package alloc
|
||||
|
||||
import "github.com/tetratelabs/wazero/experimental"
|
||||
|
||||
func Slice(cap, _ uint64) experimental.LinearMemory {
|
||||
return &sliceMemory{make([]byte, 0, cap)}
|
||||
}
|
||||
|
||||
type sliceMemory struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (b *sliceMemory) Free() {}
|
||||
|
||||
func (b *sliceMemory) Reallocate(size uint64) []byte {
|
||||
if cap := uint64(cap(b.buf)); size > cap {
|
||||
b.buf = append(b.buf[:cap], make([]byte, size-cap)...)
|
||||
} else {
|
||||
b.buf = b.buf[:size]
|
||||
}
|
||||
return b.buf
|
||||
}
|
||||
@@ -9,6 +9,6 @@ import (
|
||||
|
||||
func TestVirtual(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
alloc.Virtual(math.MaxInt+2, math.MaxInt+2)
|
||||
alloc.NewMemory(math.MaxInt+2, math.MaxInt+2)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build unix && !sqlite3_nosys
|
||||
//go:build unix
|
||||
|
||||
package alloc
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func Virtual(_, max uint64) experimental.LinearMemory {
|
||||
func NewMemory(_, max uint64) experimental.LinearMemory {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(unix.Getpagesize() - 1)
|
||||
max = (max + rnd) &^ rnd
|
||||
@@ -39,7 +39,7 @@ type mmappedMemory struct {
|
||||
func (m *mmappedMemory) Reallocate(size uint64) []byte {
|
||||
com := uint64(len(m.buf))
|
||||
res := uint64(cap(m.buf))
|
||||
if com < size && size < res {
|
||||
if com < size && size <= res {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(unix.Getpagesize() - 1)
|
||||
new := (size + rnd) &^ rnd
|
||||
@@ -47,7 +47,7 @@ func (m *mmappedMemory) Reallocate(size uint64) []byte {
|
||||
// Commit additional memory up to new bytes.
|
||||
err := unix.Mprotect(m.buf[com:new], unix.PROT_READ|unix.PROT_WRITE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update committed memory.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build !sqlite3_nosys
|
||||
|
||||
package alloc
|
||||
|
||||
import (
|
||||
@@ -11,7 +9,7 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func Virtual(_, max uint64) experimental.LinearMemory {
|
||||
func NewMemory(_, max uint64) experimental.LinearMemory {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(windows.Getpagesize() - 1)
|
||||
max = (max + rnd) &^ rnd
|
||||
@@ -48,7 +46,7 @@ type virtualMemory struct {
|
||||
func (m *virtualMemory) Reallocate(size uint64) []byte {
|
||||
com := uint64(len(m.buf))
|
||||
res := uint64(cap(m.buf))
|
||||
if com < size && size < res {
|
||||
if com < size && size <= res {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(windows.Getpagesize() - 1)
|
||||
new := (size + rnd) &^ rnd
|
||||
@@ -56,7 +54,7 @@ func (m *virtualMemory) Reallocate(size uint64) []byte {
|
||||
// Commit additional memory up to new bytes.
|
||||
_, err := windows.VirtualAlloc(m.addr, uintptr(new), windows.MEM_COMMIT, windows.PAGE_READWRITE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update committed memory.
|
||||
|
||||
29
internal/dotlk/dotlk.go
Normal file
29
internal/dotlk/dotlk.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package dotlk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
// LockShm creates a directory on disk to prevent SQLite
|
||||
// from using this path for a shared memory file.
|
||||
func LockShm(name string) error {
|
||||
err := os.Mkdir(name, 0777)
|
||||
if errors.Is(err, fs.ErrExist) {
|
||||
s, err := os.Lstat(name)
|
||||
if err == nil && s.IsDir() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Unlock removes the lock or shared memory file.
|
||||
func Unlock(name string) error {
|
||||
err := os.Remove(name)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
13
internal/dotlk/dotlk_other.go
Normal file
13
internal/dotlk/dotlk_other.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !unix
|
||||
|
||||
package dotlk
|
||||
|
||||
import "os"
|
||||
|
||||
// TryLock returns nil if it acquired the lock,
|
||||
// fs.ErrExist if another process has the lock.
|
||||
func TryLock(name string) error {
|
||||
f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
50
internal/dotlk/dotlk_unix.go
Normal file
50
internal/dotlk/dotlk_unix.go
Normal file
@@ -0,0 +1,50 @@
|
||||
//go:build unix
|
||||
|
||||
package dotlk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// TryLock returns nil if it acquired the lock,
|
||||
// fs.ErrExist if another process has the lock.
|
||||
func TryLock(name string) error {
|
||||
for retry := true; retry; retry = false {
|
||||
f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||
if err == nil {
|
||||
f.WriteString(strconv.Itoa(os.Getpid()))
|
||||
f.Close()
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, fs.ErrExist) {
|
||||
return err
|
||||
}
|
||||
if !removeStale(name) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return fs.ErrExist
|
||||
}
|
||||
|
||||
func removeStale(name string) bool {
|
||||
buf, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
return errors.Is(err, fs.ErrNotExist)
|
||||
}
|
||||
|
||||
pid, err := strconv.Atoi(string(buf))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if unix.Kill(pid, 0) == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
err = os.Remove(name)
|
||||
return err == nil || errors.Is(err, fs.ErrNotExist)
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
package testcfg
|
||||
|
||||
import (
|
||||
"math/bits"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/tetratelabs/wazero"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// notest
|
||||
|
||||
func init() {
|
||||
if bits.UintSize < 64 {
|
||||
return
|
||||
if util.CompilerSupported() {
|
||||
sqlite3.RuntimeConfig = wazero.NewRuntimeConfigCompiler()
|
||||
} else {
|
||||
sqlite3.RuntimeConfig = wazero.NewRuntimeConfigInterpreter()
|
||||
}
|
||||
|
||||
sqlite3.RuntimeConfig = wazero.NewRuntimeConfig().
|
||||
WithMemoryCapacityFromMax(true).
|
||||
WithMemoryLimitPages(1024)
|
||||
|
||||
sqlite3.RuntimeConfig = sqlite3.RuntimeConfig.WithMemoryLimitPages(512)
|
||||
if os.Getenv("CI") != "" {
|
||||
path := filepath.Join(os.TempDir(), "wazero")
|
||||
if err := os.MkdirAll(path, 0777); err == nil {
|
||||
if cache, err := wazero.NewCompilationCacheWithDir(path); err == nil {
|
||||
sqlite3.RuntimeConfig.WithCompilationCache(cache)
|
||||
sqlite3.RuntimeConfig = sqlite3.RuntimeConfig.
|
||||
WithCompilationCache(cache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package util
|
||||
|
||||
import "strings"
|
||||
|
||||
func ParseBool(s string) (b, ok bool) {
|
||||
if len(s) == 0 {
|
||||
return false, false
|
||||
}
|
||||
if s[0] == '0' {
|
||||
return false, true
|
||||
}
|
||||
if '1' <= s[0] && s[0] <= '9' {
|
||||
return true, true
|
||||
}
|
||||
switch strings.ToLower(s) {
|
||||
case "true", "yes", "on":
|
||||
return true, true
|
||||
case "false", "no", "off":
|
||||
return false, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user