mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-12 22:19:14 +00:00
Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34d0289534 | ||
|
|
dbf764aaf4 | ||
|
|
8fd878afd6 | ||
|
|
9b769d94d0 | ||
|
|
79c83cdce5 | ||
|
|
e9ed4c103d | ||
|
|
d78a53a789 | ||
|
|
19bc6e3fac | ||
|
|
3955c226cb | ||
|
|
8a3d454935 | ||
|
|
fa7516ce30 | ||
|
|
dbf93b2171 | ||
|
|
f29a999ea7 | ||
|
|
00d52a873f | ||
|
|
94fb25e84c | ||
|
|
b1f2ff55a0 | ||
|
|
53eef1510f | ||
|
|
d23bdcd225 | ||
|
|
321d359663 | ||
|
|
8f88b687d4 | ||
|
|
d1075f7dad | ||
|
|
ed932ee93b | ||
|
|
3d30a561f0 | ||
|
|
bdaf77a657 | ||
|
|
323bd6e47e | ||
|
|
5f1c372a65 | ||
|
|
3950be71c1 | ||
|
|
f3dc9bdafc | ||
|
|
e0720fdb92 | ||
|
|
5fdcdff7e0 | ||
|
|
4d23fc3cee | ||
|
|
34882e7c8d | ||
|
|
57686a2cf3 | ||
|
|
190ca0f0cc | ||
|
|
1a223fa69f | ||
|
|
12111a619a | ||
|
|
1c58744f87 | ||
|
|
f0ce3e58eb | ||
|
|
5d5c302ff4 | ||
|
|
10c494031c | ||
|
|
d84152dd8d | ||
|
|
19209b372c | ||
|
|
1e03c6c1fb | ||
|
|
bb279cb426 | ||
|
|
7b646100cb | ||
|
|
e0a209908b | ||
|
|
67d859a5b4 | ||
|
|
57daee7f59 | ||
|
|
f976ab0dee | ||
|
|
beba988824 | ||
|
|
992676d7ec | ||
|
|
82d8a2d796 | ||
|
|
811e6e63be | ||
|
|
3c21784aee | ||
|
|
019246d1be | ||
|
|
fa259bdc94 | ||
|
|
8e327a9783 | ||
|
|
09a0ce04ce | ||
|
|
fdb2ed0376 | ||
|
|
3fb0eeec51 | ||
|
|
7f6446ad31 | ||
|
|
77cdf1841f | ||
|
|
189fbc98ac | ||
|
|
d4027b0133 | ||
|
|
62b79d2ac3 | ||
|
|
07241d064a | ||
|
|
2c30bc996a | ||
|
|
9d2194b4ea | ||
|
|
b3a1cb3dd6 | ||
|
|
ec1ed22149 | ||
|
|
e86789b285 | ||
|
|
a1fcafa780 | ||
|
|
d3f5745790 | ||
|
|
ec609ea131 | ||
|
|
7bab8bb949 | ||
|
|
ce97e820d5 | ||
|
|
7d8249efa5 | ||
|
|
d2362b0311 | ||
|
|
17f1681477 | ||
|
|
cc0b011e8d | ||
|
|
46086916d4 | ||
|
|
4322c71a09 | ||
|
|
da9077cbea | ||
|
|
1c3ad12434 | ||
|
|
7260962aba | ||
|
|
e503be641a | ||
|
|
11c03a16f9 | ||
|
|
f1c376cb49 | ||
|
|
91fd1457aa | ||
|
|
63938d5705 | ||
|
|
10daa594f5 | ||
|
|
2c2b6835b4 | ||
|
|
af7fc3dcb7 | ||
|
|
0f9ce387b9 | ||
|
|
b7d22e8fbf | ||
|
|
617982f947 | ||
|
|
36583542e1 | ||
|
|
fd3a3a3499 | ||
|
|
ec96c77715 | ||
|
|
c61f7b90f6 | ||
|
|
7bd31c3443 | ||
|
|
f2f698b78a | ||
|
|
846b95d2d4 | ||
|
|
b9453aefb6 | ||
|
|
28b6fedef0 | ||
|
|
fed9ce6e1c | ||
|
|
0ec08c2e74 | ||
|
|
4439cd302c | ||
|
|
705eab456a | ||
|
|
d1d5e355c4 | ||
|
|
d3da8cc4f3 | ||
|
|
6def6f735c | ||
|
|
e02c5b5db0 | ||
|
|
52d42e4b21 | ||
|
|
396e6537b4 | ||
|
|
78cb9abefd | ||
|
|
b76cb33e62 | ||
|
|
c7eea620a3 | ||
|
|
76e2733fef | ||
|
|
58df08329d | ||
|
|
cdab468a92 | ||
|
|
7438fdb664 | ||
|
|
da0e98f17e | ||
|
|
bb0c77c6fa | ||
|
|
ea8894162b | ||
|
|
9898fbfffa |
23
.github/actions/lfs/action.yml
vendored
Normal file
23
.github/actions/lfs/action.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
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
|
||||
24
.github/workflows/bsd.yml
vendored
24
.github/workflows/bsd.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: BSD
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: 'true'
|
||||
|
||||
- name: Test
|
||||
uses: cross-platform-actions/action@v0.22.0
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '13.2'
|
||||
memory: 8G
|
||||
sync_files: runner-to-vm
|
||||
run: |
|
||||
sudo pkg install -y go121
|
||||
go121 test -v ./...
|
||||
11
.github/workflows/build-test.sh
vendored
Executable file
11
.github/workflows/build-test.sh
vendored
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
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
|
||||
40
.github/workflows/cpu.yml
vendored
40
.github/workflows/cpu.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: CPUs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-386:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: 'true'
|
||||
|
||||
- name: Set up
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Test
|
||||
run: GOARCH=386 go test -v ./...
|
||||
|
||||
test-arm:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: 'true'
|
||||
|
||||
- name: Set up
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Install QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Test
|
||||
run: GOARCH=arm64 go test -v ./...
|
||||
11
.github/workflows/cross.sh
vendored
11
.github/workflows/cross.sh
vendored
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo android ; GOOS=android GOARCH=amd64 go build .
|
||||
echo darwin ; GOOS=darwin GOARCH=amd64 go build .
|
||||
@@ -15,8 +16,12 @@ 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 darwin-flock ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_flock .
|
||||
echo darwin-nosys ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_nosys .
|
||||
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 freebsd-nosys ; GOOS=freebsd GOARCH=amd64 go build -tags sqlite3_nosys .
|
||||
echo solaris-flock ; GOOS=solaris GOARCH=amd64 go build -tags sqlite3_flock .
|
||||
7
.github/workflows/cross.yml
vendored
7
.github/workflows/cross.yml
vendored
@@ -9,11 +9,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Build
|
||||
run: .github/workflows/cross.sh
|
||||
|
||||
63
.github/workflows/go.yml
vendored
63
.github/workflows/go.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: 'true'
|
||||
|
||||
- name: Set up
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Format
|
||||
run: gofmt -s -w . && git diff --exit-code
|
||||
if: matrix.os != 'windows-latest'
|
||||
|
||||
- name: Tidy
|
||||
run: go mod tidy && git diff --exit-code
|
||||
|
||||
- name: Download
|
||||
run: go mod download
|
||||
|
||||
- name: Verify
|
||||
run: go mod verify
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Test no locks
|
||||
run: go test -v -tags sqlite3_nosys ./tests -run TestDB_nolock
|
||||
|
||||
- name: Test BSD locks
|
||||
run: go test -v -tags sqlite3_flock ./...
|
||||
if: matrix.os == 'macos-latest'
|
||||
|
||||
- name: Coverage report
|
||||
uses: ncruces/go-coverage-report@v0
|
||||
with:
|
||||
chart: true
|
||||
amend: true
|
||||
if: |
|
||||
github.event_name == 'push' &&
|
||||
matrix.os == 'ubuntu-latest'
|
||||
16
.github/workflows/repro.sh
vendored
16
.github/workflows/repro.sh
vendored
@@ -2,22 +2,22 @@
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$OSTYPE" == "linux"* ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/wasi-sdk-21.0-linux.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-linux.tar.gz"
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-linux.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-linux.tar.gz"
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/wasi-sdk-21.0-macos.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-macos.tar.gz"
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-macos.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-macos.tar.gz"
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/wasi-sdk-21.0.m-mingw.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-windows.tar.gz"
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0.m-mingw.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-windows.tar.gz"
|
||||
fi
|
||||
|
||||
# Download tools
|
||||
mkdir -p tools
|
||||
mkdir -p tools/
|
||||
[ -d "tools/wasi-sdk"* ] || curl -#L "$WASI_SDK" | tar xzC tools &
|
||||
[ -d "tools/binaryen-version"* ] || curl -#L "$BINARYEN" | tar xzC tools &
|
||||
wait
|
||||
|
||||
sqlite3/download.sh # Download SQLite
|
||||
embed/build.sh # Build WASM
|
||||
embed/build.sh # Build Wasm
|
||||
git diff --exit-code # Check diffs
|
||||
9
.github/workflows/repro.yml
vendored
9
.github/workflows/repro.yml
vendored
@@ -12,13 +12,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: 'true'
|
||||
|
||||
- name: Set up
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Build
|
||||
run: .github/workflows/repro.sh
|
||||
|
||||
174
.github/workflows/test.yml
vendored
Normal file
174
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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'
|
||||
|
||||
- name: Tidy
|
||||
run: go mod tidy && git diff --exit-code
|
||||
|
||||
- name: Download
|
||||
run: go mod download
|
||||
|
||||
- name: Verify
|
||||
run: go mod verify
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./... -bench . -benchtime=1x
|
||||
|
||||
- name: Test BSD locks
|
||||
run: go test -v -tags sqlite3_flock ./...
|
||||
if: matrix.os == 'macos-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 GORM
|
||||
run: gormlite/test.sh
|
||||
|
||||
- uses: ncruces/go-coverage-report@v0
|
||||
with:
|
||||
chart: true
|
||||
amend: true
|
||||
if: |
|
||||
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:
|
||||
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: freebsd
|
||||
TESTFLAGS: '-test.v'
|
||||
run: .github/workflows/build-test.sh
|
||||
|
||||
- name: Test
|
||||
uses: cross-platform-actions/action@v0.24.0
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '14.0'
|
||||
shell: bash
|
||||
run: . ./test.sh
|
||||
sync_files: runner-to-vm
|
||||
|
||||
test-qemu:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: actions/checkout@v4
|
||||
- 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 ./...
|
||||
|
||||
- name: Test arm64 (compiler)
|
||||
run: GOARCH=arm64 go test -v -short ./...
|
||||
|
||||
- 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 ./...
|
||||
|
||||
test-vm:
|
||||
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 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
|
||||
72
README.md
72
README.md
@@ -4,12 +4,13 @@
|
||||
[](https://goreportcard.com/report/github.com/ncruces/go-sqlite3)
|
||||
[](https://github.com/ncruces/go-sqlite3/wiki/Test-coverage-report)
|
||||
|
||||
Go module `github.com/ncruces/go-sqlite3` is `cgo`-free [SQLite](https://sqlite.org/) wrapper.\
|
||||
Go module `github.com/ncruces/go-sqlite3` is a `cgo`-free [SQLite](https://sqlite.org/) wrapper.\
|
||||
It provides a [`database/sql`](https://pkg.go.dev/database/sql) compatible driver,
|
||||
as well as direct access to most of the [C SQLite API](https://sqlite.org/cintro.html).
|
||||
|
||||
It wraps a [WASM](https://webassembly.org/) build 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.
|
||||
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].
|
||||
|
||||
### Packages
|
||||
|
||||
@@ -48,10 +49,14 @@ Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ run
|
||||
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/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
|
||||
maps multidimensional data to one dimension.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/memdb`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb)
|
||||
implements an in-memory VFS.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/readervfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs)
|
||||
implements a VFS for immutable databases.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)
|
||||
wraps a VFS to offer encryption at rest.
|
||||
|
||||
### Advanced features
|
||||
|
||||
@@ -65,55 +70,16 @@ Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ run
|
||||
- [math functions](https://sqlite.org/lang_mathfunc.html)
|
||||
- [full-text search](https://sqlite.org/fts5.html)
|
||||
- [geospatial search](https://sqlite.org/geopoly.html)
|
||||
- [encryption at rest](vfs/adiantum/README.md)
|
||||
- [and more…](embed/README.md)
|
||||
|
||||
### Caveats
|
||||
|
||||
This module replaces the SQLite [OS Interface](https://sqlite.org/vfs.html)
|
||||
(aka VFS) with a [pure Go](vfs/) implementation.
|
||||
This has benefits, but also comes with some drawbacks.
|
||||
(aka VFS) with a [pure Go](vfs/) implementation,
|
||||
which has advantages and disadvantages.
|
||||
|
||||
#### Write-Ahead Logging
|
||||
|
||||
Because WASM does not support shared memory,
|
||||
[WAL](https://sqlite.org/wal.html) support is [limited](https://sqlite.org/wal.html#noshm).
|
||||
|
||||
To work around this limitation, SQLite is [patched](sqlite3/locking_mode.patch)
|
||||
to always use `EXCLUSIVE` locking mode for WAL databases.
|
||||
|
||||
Because connection pooling is incompatible with `EXCLUSIVE` locking mode,
|
||||
to use the [`database/sql`](https://pkg.go.dev/database/sql) driver
|
||||
with WAL mode databases you should disable connection pooling by calling
|
||||
[`db.SetMaxOpenConns(1)`](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns).
|
||||
|
||||
#### File Locking
|
||||
|
||||
POSIX advisory locks, which SQLite uses on Unix, are
|
||||
[broken by design](https://sqlite.org/src/artifact/2e8b12?ln=1073-1161).
|
||||
|
||||
On Linux, macOS and illumos, this module uses
|
||||
[OFD locks](https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html)
|
||||
to synchronize access to database files.
|
||||
OFD locks are fully compatible with POSIX advisory locks.
|
||||
|
||||
On BSD Unixes, this module uses
|
||||
[BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2).
|
||||
On BSD Unixes, BSD locks are fully compatible with POSIX advisory locks.
|
||||
|
||||
On Windows, this module uses `LockFileEx` and `UnlockFileEx`,
|
||||
like SQLite.
|
||||
|
||||
On all other platforms, file locking is not supported, and you must use
|
||||
[`nolock=1`](https://sqlite.org/uri.html#urinolock)
|
||||
(or [`immutable=1`](https://sqlite.org/uri.html#uriimmutable))
|
||||
to open database files.
|
||||
|
||||
To use the [`database/sql`](https://pkg.go.dev/database/sql) driver
|
||||
with `nolock=1` you must disable connection pooling by calling
|
||||
[`db.SetMaxOpenConns(1)`](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns).
|
||||
|
||||
You can use [`vfs.SupportsFileLocking`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs#SupportsFileLocking)
|
||||
to check if your platform supports file locking.
|
||||
Read more about the Go VFS design [here](vfs/README.md).
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -121,16 +87,19 @@ 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.
|
||||
|
||||
The pure Go VFS is tested by running SQLite's
|
||||
[mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c)
|
||||
on Linux, macOS, Windows and FreeBSD.
|
||||
Every commit is [tested](.github/workflows/test.yml) on
|
||||
Linux (amd64/arm64/386/riscv64/s390x), macOS (amd64/arm64),
|
||||
Windows (amd64), FreeBSD (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).
|
||||
|
||||
### Performance
|
||||
|
||||
Perfomance of the [`database/sql`](https://pkg.go.dev/database/sql) driver is
|
||||
[competitive](https://github.com/cvilsmeier/go-sqlite-bench) with alternatives.
|
||||
|
||||
The WASM and VFS layers are also tested by running SQLite's
|
||||
The Wasm and VFS layers are also tested by running SQLite's
|
||||
[speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c).
|
||||
|
||||
### Alternatives
|
||||
@@ -139,3 +108,6 @@ The WASM and VFS layers are also tested by running SQLite's
|
||||
- [`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.
|
||||
|
||||
67
config.go
67
config.go
@@ -82,7 +82,7 @@ func (c *Conn) SetAuthorizer(cb func(action AuthorizerActionCode, name3rd, name4
|
||||
|
||||
}
|
||||
|
||||
func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action AuthorizerActionCode, zName3rd, zName4th, zSchema, zNameInner uint32) AuthorizerReturnCode {
|
||||
func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action AuthorizerActionCode, zName3rd, zName4th, zSchema, zNameInner uint32) (rc AuthorizerReturnCode) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.authorizer != nil {
|
||||
var name3rd, name4th, schema, nameInner string
|
||||
if zName3rd != 0 {
|
||||
@@ -97,7 +97,68 @@ func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action
|
||||
if zNameInner != 0 {
|
||||
nameInner = util.ReadString(mod, zNameInner, _MAX_NAME)
|
||||
}
|
||||
return c.authorizer(action, name3rd, name4th, schema, nameInner)
|
||||
rc = c.authorizer(action, name3rd, name4th, schema, nameInner)
|
||||
}
|
||||
return AUTH_OK
|
||||
return rc
|
||||
}
|
||||
|
||||
// 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) {
|
||||
defer c.arena.mark()()
|
||||
nLogPtr := c.arena.new(ptrlen)
|
||||
nCkptPtr := c.arena.new(ptrlen)
|
||||
schemaPtr := c.arena.string(schema)
|
||||
r := c.call("sqlite3_wal_checkpoint_v2",
|
||||
uint64(c.handle), uint64(schemaPtr), uint64(mode),
|
||||
uint64(nLogPtr), uint64(nCkptPtr))
|
||||
nLog = int(int32(util.ReadUint32(c.mod, nLogPtr)))
|
||||
nCkpt = int(int32(util.ReadUint32(c.mod, nCkptPtr)))
|
||||
return nLog, nCkpt, c.error(r)
|
||||
}
|
||||
|
||||
// WalAutoCheckpoint configures WAL auto-checkpoints.
|
||||
//
|
||||
// https://sqlite.org/c3ref/wal_autocheckpoint.html
|
||||
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
|
||||
// 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) {
|
||||
var enable uint64
|
||||
if cb != nil {
|
||||
enable = 1
|
||||
}
|
||||
c.call("sqlite3_wal_hook_go", uint64(c.handle), enable)
|
||||
c.wal = cb
|
||||
}
|
||||
|
||||
func walCallback(ctx context.Context, mod api.Module, _, pDB, zSchema uint32, pages int32) (rc uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.wal != nil {
|
||||
schema := util.ReadString(mod, zSchema, _MAX_NAME)
|
||||
err := c.wal(c, schema, int(pages))
|
||||
_, rc = errorCode(err, ERROR)
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
// AutoVacuumPages registers a autovacuum compaction amount callback.
|
||||
//
|
||||
// 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)
|
||||
r := c.call("sqlite3_autovacuum_pages_go", uint64(c.handle), uint64(funcPtr))
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
func autoVacuumCallback(ctx context.Context, mod api.Module, pApp, zSchema, nDbPage, nFreePage, nBytePerPage uint32) uint32 {
|
||||
fn := util.GetHandle(ctx, pApp).(func(schema string, dbPages, freePages, bytesPerPage uint) uint)
|
||||
schema := util.ReadString(mod, zSchema, _MAX_NAME)
|
||||
return uint32(fn(schema, uint(nDbPage), uint(nFreePage), uint(nBytePerPage)))
|
||||
}
|
||||
|
||||
118
conn.go
118
conn.go
@@ -2,12 +2,14 @@ package sqlite3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
@@ -20,12 +22,14 @@ type Conn struct {
|
||||
|
||||
interrupt context.Context
|
||||
pending *Stmt
|
||||
busy func(int) bool
|
||||
log func(xErrorCode, string)
|
||||
collation func(*Conn, string)
|
||||
authorizer func(AuthorizerActionCode, string, string, string, string) AuthorizerReturnCode
|
||||
update func(AuthorizerActionCode, string, string, int64)
|
||||
commit func() bool
|
||||
rollback func()
|
||||
wal func(*Conn, string, int) error
|
||||
arena arena
|
||||
|
||||
handle uint32
|
||||
@@ -98,15 +102,14 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
||||
pragmas.WriteString(`;`)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if errors.Is(err, ERROR) {
|
||||
if pragmas.Len() != 0 {
|
||||
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 {
|
||||
err = fmt.Errorf("sqlite3: invalid _pragma: %w", err)
|
||||
c.closeDB(handle)
|
||||
return 0, err
|
||||
}
|
||||
c.closeDB(handle)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
|
||||
@@ -171,7 +174,7 @@ func (c *Conn) Prepare(sql string) (stmt *Stmt, tail string, err error) {
|
||||
//
|
||||
// https://sqlite.org/c3ref/prepare.html
|
||||
func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail string, err error) {
|
||||
if len(sql) > _MAX_LENGTH {
|
||||
if len(sql) > _MAX_SQL_LENGTH {
|
||||
return nil, "", TOOBIG
|
||||
}
|
||||
|
||||
@@ -212,6 +215,20 @@ func (c *Conn) DBName(n int) string {
|
||||
return util.ReadString(c.mod, ptr, _MAX_NAME)
|
||||
}
|
||||
|
||||
// Filename returns the filename for a database.
|
||||
//
|
||||
// https://sqlite.org/c3ref/db_filename.html
|
||||
func (c *Conn) Filename(schema string) *vfs.Filename {
|
||||
var ptr uint32
|
||||
if schema != "" {
|
||||
defer c.arena.mark()()
|
||||
ptr = c.arena.string(schema)
|
||||
}
|
||||
|
||||
r := c.call("sqlite3_db_filename", uint64(c.handle), uint64(ptr))
|
||||
return vfs.OpenFilename(c.ctx, c.mod, uint32(r), vfs.OPEN_MAIN_DB)
|
||||
}
|
||||
|
||||
// ReadOnly determines if a database is read-only.
|
||||
//
|
||||
// https://sqlite.org/c3ref/db_readonly.html
|
||||
@@ -278,6 +295,12 @@ func (c *Conn) ReleaseMemory() error {
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
// GetInterrupt gets the context set with [Conn.SetInterrupt],
|
||||
// or nil if none was set.
|
||||
func (c *Conn) GetInterrupt() context.Context {
|
||||
return c.interrupt
|
||||
}
|
||||
|
||||
// SetInterrupt interrupts a long-running query when a context is done.
|
||||
//
|
||||
// Subsequent uses of the connection will return [INTERRUPT]
|
||||
@@ -322,28 +345,71 @@ func (c *Conn) checkInterrupt() {
|
||||
}
|
||||
}
|
||||
|
||||
func progressCallback(ctx context.Context, mod api.Module, _ uint32) uint32 {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok {
|
||||
if c.interrupt != nil && c.interrupt.Err() != nil {
|
||||
return 1
|
||||
}
|
||||
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
|
||||
}
|
||||
return 0
|
||||
return interrupt
|
||||
}
|
||||
|
||||
// Deprecated: executes a PRAGMA statement and returns results.
|
||||
func (c *Conn) Pragma(str string) ([]string, error) {
|
||||
stmt, _, err := c.Prepare(`PRAGMA ` + str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
// BusyTimeout sets a busy timeout.
|
||||
//
|
||||
// https://sqlite.org/c3ref/busy_timeout.html
|
||||
func (c *Conn) BusyTimeout(timeout time.Duration) error {
|
||||
ms := min((timeout+time.Millisecond-1)/time.Millisecond, math.MaxInt32)
|
||||
r := c.call("sqlite3_busy_timeout", uint64(c.handle), uint64(ms))
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
var pragmas []string
|
||||
for stmt.Step() {
|
||||
pragmas = append(pragmas, stmt.ColumnText(0))
|
||||
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)
|
||||
}
|
||||
|
||||
if delay = min(delay, tmout-prior); delay > 0 {
|
||||
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||
retry = 1
|
||||
}
|
||||
}
|
||||
return pragmas, stmt.Close()
|
||||
return retry
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var enable uint64
|
||||
if cb != nil {
|
||||
enable = 1
|
||||
}
|
||||
r := c.call("sqlite3_busy_handler_go", uint64(c.handle), enable)
|
||||
if err := c.error(r); err != nil {
|
||||
return err
|
||||
}
|
||||
c.busy = cb
|
||||
return nil
|
||||
}
|
||||
|
||||
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)) {
|
||||
retry = 1
|
||||
}
|
||||
}
|
||||
return retry
|
||||
}
|
||||
|
||||
func (c *Conn) error(rc uint64, sql ...string) error {
|
||||
|
||||
82
const.go
82
const.go
@@ -256,41 +256,41 @@ const (
|
||||
type AuthorizerActionCode uint32
|
||||
|
||||
const (
|
||||
/************************************************ 3rd ************ 4th ***********/
|
||||
CREATE_INDEX AuthorizerActionCode = 1 /* Index Name Table Name */
|
||||
CREATE_TABLE AuthorizerActionCode = 2 /* Table Name NULL */
|
||||
CREATE_TEMP_INDEX AuthorizerActionCode = 3 /* Index Name Table Name */
|
||||
CREATE_TEMP_TABLE AuthorizerActionCode = 4 /* Table Name NULL */
|
||||
CREATE_TEMP_TRIGGER AuthorizerActionCode = 5 /* Trigger Name Table Name */
|
||||
CREATE_TEMP_VIEW AuthorizerActionCode = 6 /* View Name NULL */
|
||||
CREATE_TRIGGER AuthorizerActionCode = 7 /* Trigger Name Table Name */
|
||||
CREATE_VIEW AuthorizerActionCode = 8 /* View Name NULL */
|
||||
DELETE AuthorizerActionCode = 9 /* Table Name NULL */
|
||||
DROP_INDEX AuthorizerActionCode = 10 /* Index Name Table Name */
|
||||
DROP_TABLE AuthorizerActionCode = 11 /* Table Name NULL */
|
||||
DROP_TEMP_INDEX AuthorizerActionCode = 12 /* Index Name Table Name */
|
||||
DROP_TEMP_TABLE AuthorizerActionCode = 13 /* Table Name NULL */
|
||||
DROP_TEMP_TRIGGER AuthorizerActionCode = 14 /* Trigger Name Table Name */
|
||||
DROP_TEMP_VIEW AuthorizerActionCode = 15 /* View Name NULL */
|
||||
DROP_TRIGGER AuthorizerActionCode = 16 /* Trigger Name Table Name */
|
||||
DROP_VIEW AuthorizerActionCode = 17 /* View Name NULL */
|
||||
INSERT AuthorizerActionCode = 18 /* Table Name NULL */
|
||||
PRAGMA AuthorizerActionCode = 19 /* Pragma Name 1st arg or NULL */
|
||||
READ AuthorizerActionCode = 20 /* Table Name Column Name */
|
||||
SELECT AuthorizerActionCode = 21 /* NULL NULL */
|
||||
TRANSACTION AuthorizerActionCode = 22 /* Operation NULL */
|
||||
UPDATE AuthorizerActionCode = 23 /* Table Name Column Name */
|
||||
ATTACH AuthorizerActionCode = 24 /* Filename NULL */
|
||||
DETACH AuthorizerActionCode = 25 /* Database Name NULL */
|
||||
ALTER_TABLE AuthorizerActionCode = 26 /* Database Name Table Name */
|
||||
REINDEX AuthorizerActionCode = 27 /* Index Name NULL */
|
||||
ANALYZE AuthorizerActionCode = 28 /* Table Name NULL */
|
||||
CREATE_VTABLE AuthorizerActionCode = 29 /* Table Name Module Name */
|
||||
DROP_VTABLE AuthorizerActionCode = 30 /* Table Name Module Name */
|
||||
FUNCTION AuthorizerActionCode = 31 /* NULL Function Name */
|
||||
SAVEPOINT AuthorizerActionCode = 32 /* Operation Savepoint Name */
|
||||
COPY AuthorizerActionCode = 0 /* No longer used */
|
||||
RECURSIVE AuthorizerActionCode = 33 /* NULL NULL */
|
||||
/***************************************************** 3rd ************ 4th ***********/
|
||||
AUTH_CREATE_INDEX AuthorizerActionCode = 1 /* Index Name Table Name */
|
||||
AUTH_CREATE_TABLE AuthorizerActionCode = 2 /* Table Name NULL */
|
||||
AUTH_CREATE_TEMP_INDEX AuthorizerActionCode = 3 /* Index Name Table Name */
|
||||
AUTH_CREATE_TEMP_TABLE AuthorizerActionCode = 4 /* Table Name NULL */
|
||||
AUTH_CREATE_TEMP_TRIGGER AuthorizerActionCode = 5 /* Trigger Name Table Name */
|
||||
AUTH_CREATE_TEMP_VIEW AuthorizerActionCode = 6 /* View Name NULL */
|
||||
AUTH_CREATE_TRIGGER AuthorizerActionCode = 7 /* Trigger Name Table Name */
|
||||
AUTH_CREATE_VIEW AuthorizerActionCode = 8 /* View Name NULL */
|
||||
AUTH_DELETE AuthorizerActionCode = 9 /* Table Name NULL */
|
||||
AUTH_DROP_INDEX AuthorizerActionCode = 10 /* Index Name Table Name */
|
||||
AUTH_DROP_TABLE AuthorizerActionCode = 11 /* Table Name NULL */
|
||||
AUTH_DROP_TEMP_INDEX AuthorizerActionCode = 12 /* Index Name Table Name */
|
||||
AUTH_DROP_TEMP_TABLE AuthorizerActionCode = 13 /* Table Name NULL */
|
||||
AUTH_DROP_TEMP_TRIGGER AuthorizerActionCode = 14 /* Trigger Name Table Name */
|
||||
AUTH_DROP_TEMP_VIEW AuthorizerActionCode = 15 /* View Name NULL */
|
||||
AUTH_DROP_TRIGGER AuthorizerActionCode = 16 /* Trigger Name Table Name */
|
||||
AUTH_DROP_VIEW AuthorizerActionCode = 17 /* View Name NULL */
|
||||
AUTH_INSERT AuthorizerActionCode = 18 /* Table Name NULL */
|
||||
AUTH_PRAGMA AuthorizerActionCode = 19 /* Pragma Name 1st arg or NULL */
|
||||
AUTH_READ AuthorizerActionCode = 20 /* Table Name Column Name */
|
||||
AUTH_SELECT AuthorizerActionCode = 21 /* NULL NULL */
|
||||
AUTH_TRANSACTION AuthorizerActionCode = 22 /* Operation NULL */
|
||||
AUTH_UPDATE AuthorizerActionCode = 23 /* Table Name Column Name */
|
||||
AUTH_ATTACH AuthorizerActionCode = 24 /* Filename NULL */
|
||||
AUTH_DETACH AuthorizerActionCode = 25 /* Database Name NULL */
|
||||
AUTH_ALTER_TABLE AuthorizerActionCode = 26 /* Database Name Table Name */
|
||||
AUTH_REINDEX AuthorizerActionCode = 27 /* Index Name NULL */
|
||||
AUTH_ANALYZE AuthorizerActionCode = 28 /* Table Name NULL */
|
||||
AUTH_CREATE_VTABLE AuthorizerActionCode = 29 /* Table Name Module Name */
|
||||
AUTH_DROP_VTABLE AuthorizerActionCode = 30 /* Table Name Module Name */
|
||||
AUTH_FUNCTION AuthorizerActionCode = 31 /* NULL Function Name */
|
||||
AUTH_SAVEPOINT AuthorizerActionCode = 32 /* Operation Savepoint Name */
|
||||
AUTH_COPY AuthorizerActionCode = 0 /* No longer used */
|
||||
AUTH_RECURSIVE AuthorizerActionCode = 33 /* NULL NULL */
|
||||
)
|
||||
|
||||
// AuthorizerReturnCode are the integer codes
|
||||
@@ -305,6 +305,18 @@ const (
|
||||
AUTH_IGNORE AuthorizerReturnCode = 2 /* Don't allow access, but don't generate an error */
|
||||
)
|
||||
|
||||
// CheckpointMode are all the checkpoint mode values.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_checkpoint_full.html
|
||||
type CheckpointMode uint32
|
||||
|
||||
const (
|
||||
CHECKPOINT_PASSIVE CheckpointMode = 0 /* Do as much as possible w/o blocking */
|
||||
CHECKPOINT_FULL CheckpointMode = 1 /* Wait for writers, then checkpoint */
|
||||
CHECKPOINT_RESTART CheckpointMode = 2 /* Like FULL but wait for readers */
|
||||
CHECKPOINT_TRUNCATE CheckpointMode = 3 /* Like RESTART but also truncate WAL */
|
||||
)
|
||||
|
||||
// TxnState are the allowed return values from [Conn.TxnState].
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_txn_none.html
|
||||
|
||||
@@ -63,39 +63,47 @@ var driverName = "sqlite3"
|
||||
|
||||
func init() {
|
||||
if driverName != "" {
|
||||
sql.Register(driverName, sqlite{})
|
||||
sql.Register(driverName, &SQLite{})
|
||||
}
|
||||
}
|
||||
|
||||
// Open opens the SQLite database specified by dataSourceName as a [database/sql.DB].
|
||||
//
|
||||
// The init function is called by the driver on new connections.
|
||||
// The conn can be used to execute queries, register functions, etc.
|
||||
// Any error return closes the conn and passes the error to [database/sql].
|
||||
// The [sqlite3.Conn] can be used to execute queries, register functions, etc.
|
||||
// Any error returned closes the connection and is returned to [database/sql].
|
||||
func Open(dataSourceName string, init func(*sqlite3.Conn) error) (*sql.DB, error) {
|
||||
c, err := newConnector(dataSourceName, init)
|
||||
c, err := (&SQLite{Init: init}).OpenConnector(dataSourceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sql.OpenDB(c), nil
|
||||
}
|
||||
|
||||
type sqlite struct{}
|
||||
// SQLite implements [database/sql/driver.Driver].
|
||||
type SQLite struct {
|
||||
// Init function is called by the driver on new connections.
|
||||
// The [sqlite3.Conn] can be used to execute queries, register functions, etc.
|
||||
// Any error returned closes the connection and is returned to [database/sql].
|
||||
Init func(*sqlite3.Conn) error
|
||||
}
|
||||
|
||||
func (sqlite) Open(name string) (driver.Conn, error) {
|
||||
c, err := newConnector(name, nil)
|
||||
// Open implements [database/sql/driver.Driver].
|
||||
func (d *SQLite) Open(name string) (driver.Conn, error) {
|
||||
c, err := d.newConnector(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Connect(context.Background())
|
||||
}
|
||||
|
||||
func (sqlite) OpenConnector(name string) (driver.Connector, error) {
|
||||
return newConnector(name, nil)
|
||||
// OpenConnector implements [database/sql/driver.DriverContext].
|
||||
func (d *SQLite) OpenConnector(name string) (driver.Connector, error) {
|
||||
return d.newConnector(name)
|
||||
}
|
||||
|
||||
func newConnector(name string, init func(*sqlite3.Conn) error) (*connector, error) {
|
||||
c := connector{name: name, init: init}
|
||||
func (d *SQLite) newConnector(name string) (*connector, error) {
|
||||
c := connector{driver: d, name: name}
|
||||
|
||||
var txlock, timefmt string
|
||||
if strings.HasPrefix(name, "file:") {
|
||||
@@ -137,7 +145,7 @@ func newConnector(name string, init func(*sqlite3.Conn) error) (*connector, erro
|
||||
}
|
||||
|
||||
type connector struct {
|
||||
init func(*sqlite3.Conn) error
|
||||
driver *SQLite
|
||||
name string
|
||||
txBegin string
|
||||
tmRead sqlite3.TimeFormat
|
||||
@@ -146,7 +154,7 @@ type connector struct {
|
||||
}
|
||||
|
||||
func (n *connector) Driver() driver.Driver {
|
||||
return sqlite{}
|
||||
return n.driver
|
||||
}
|
||||
|
||||
func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
@@ -170,18 +178,18 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
|
||||
if !n.pragmas {
|
||||
err = c.Conn.Exec(`PRAGMA busy_timeout=60000`)
|
||||
err = c.Conn.BusyTimeout(60 * time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if n.init != nil {
|
||||
err = n.init(c.Conn)
|
||||
if n.driver.Init != nil {
|
||||
err = n.driver.Init(c.Conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if n.pragmas || n.init != nil {
|
||||
if n.pragmas || n.driver.Init != nil {
|
||||
s, _, err := c.Conn.Prepare(`PRAGMA query_only`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -319,7 +327,7 @@ func (c *conn) ExecContext(ctx context.Context, query string, args []driver.Name
|
||||
return newResult(c.Conn), nil
|
||||
}
|
||||
|
||||
func (*conn) CheckNamedValue(arg *driver.NamedValue) error {
|
||||
func (c *conn) CheckNamedValue(arg *driver.NamedValue) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
)
|
||||
|
||||
func Test_Open_dir(t *testing.T) {
|
||||
@@ -79,6 +81,9 @@ func Test_Open_pragma_invalid(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_Open_txLock(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
db, err := sql.Open("sqlite3", "file:"+
|
||||
@@ -125,6 +130,9 @@ func Test_Open_txLock_invalid(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_BeginTx(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -153,7 +161,7 @@ func Test_BeginTx(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = tx1.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
_, err = tx1.Exec(`CREATE TABLE test (col)`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
@@ -310,7 +318,7 @@ func Test_time(t *testing.T) {
|
||||
|
||||
twosday := time.Date(2022, 2, 22, 22, 22, 22, 0, time.UTC)
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS test (at DATETIME)`)
|
||||
_, err = db.Exec(`CREATE TABLE test (at DATETIME)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build (linux || darwin || windows || freebsd || illumos) && !sqlite3_nosys
|
||||
|
||||
package driver_test
|
||||
|
||||
// Adapted from: https://go.dev/doc/tutorial/database-access
|
||||
|
||||
@@ -18,7 +18,7 @@ func Example_json() {
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
CREATE TABLE orders (
|
||||
cart_id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
cart TEXT
|
||||
|
||||
@@ -16,7 +16,7 @@ func ExampleSavepoint() {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
|
||||
_, err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# Embeddable WASM build of SQLite
|
||||
# Embeddable Wasm build of SQLite
|
||||
|
||||
This folder includes an embeddable WASM build of SQLite 3.45.0 for use with
|
||||
This folder includes an embeddable Wasm build of SQLite 3.46.0 for use with
|
||||
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
|
||||
|
||||
The following optional features are compiled in:
|
||||
- [math functions](https://sqlite.org/lang_mathfunc.html)
|
||||
- [FTS3/4](https://sqlite.org/fts3.html)/[5](https://sqlite.org/fts5.html)
|
||||
- [FTS5](https://sqlite.org/fts5.html)
|
||||
- [JSON](https://sqlite.org/json1.html)
|
||||
- [R*Tree](https://sqlite.org/rtree.html)
|
||||
- [GeoPoly](https://sqlite.org/geopoly.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)
|
||||
- [decimal](https://github.com/sqlite/sqlite/blob/master/ext/misc/decimal.c)
|
||||
- [ieee754](https://github.com/sqlite/sqlite/blob/master/ext/misc/ieee754.c)
|
||||
|
||||
@@ -4,11 +4,11 @@ set -euo pipefail
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
ROOT=../
|
||||
BINARYEN="$ROOT/tools/binaryen-version_116/bin"
|
||||
WASI_SDK="$ROOT/tools/wasi-sdk-21.0/bin"
|
||||
BINARYEN="$ROOT/tools/binaryen-version_117/bin"
|
||||
WASI_SDK="$ROOT/tools/wasi-sdk-22.0/bin"
|
||||
|
||||
"$WASI_SDK/clang" --target=wasm32-wasi -std=c17 -flto -g0 -O2 \
|
||||
-Wall -Wextra -Wno-unused-parameter \
|
||||
-Wall -Wextra -Wno-unused-parameter -Wno-unused-function \
|
||||
-o sqlite3.wasm "$ROOT/sqlite3/main.c" \
|
||||
-I"$ROOT/sqlite3" \
|
||||
-mexec-model=reactor \
|
||||
@@ -20,6 +20,7 @@ WASI_SDK="$ROOT/tools/wasi-sdk-21.0/bin"
|
||||
-Wl,--stack-first \
|
||||
-Wl,--import-undefined \
|
||||
-D_HAVE_SQLITE_CONFIG_H \
|
||||
-DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \
|
||||
$(awk '{print "-Wl,--export="$0}' exports.txt)
|
||||
|
||||
trap 'rm -f sqlite3.tmp' EXIT
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
aligned_alloc
|
||||
free
|
||||
malloc
|
||||
malloc_destructor
|
||||
sqlite3_anycollseq_init
|
||||
sqlite3_autovacuum_pages_go
|
||||
sqlite3_backup_finish
|
||||
sqlite3_backup_init
|
||||
sqlite3_backup_pagecount
|
||||
@@ -24,6 +26,8 @@ sqlite3_blob_open
|
||||
sqlite3_blob_read
|
||||
sqlite3_blob_reopen
|
||||
sqlite3_blob_write
|
||||
sqlite3_busy_handler_go
|
||||
sqlite3_busy_timeout
|
||||
sqlite3_changes64
|
||||
sqlite3_clear_bindings
|
||||
sqlite3_close
|
||||
@@ -32,10 +36,13 @@ sqlite3_collation_needed_go
|
||||
sqlite3_column_blob
|
||||
sqlite3_column_bytes
|
||||
sqlite3_column_count
|
||||
sqlite3_column_database_name
|
||||
sqlite3_column_decltype
|
||||
sqlite3_column_double
|
||||
sqlite3_column_int64
|
||||
sqlite3_column_name
|
||||
sqlite3_column_origin_name
|
||||
sqlite3_column_table_name
|
||||
sqlite3_column_text
|
||||
sqlite3_column_type
|
||||
sqlite3_column_value
|
||||
@@ -47,7 +54,9 @@ sqlite3_create_collation_go
|
||||
sqlite3_create_function_go
|
||||
sqlite3_create_module_go
|
||||
sqlite3_create_window_function_go
|
||||
sqlite3_database_file_object
|
||||
sqlite3_db_config
|
||||
sqlite3_db_filename
|
||||
sqlite3_db_name
|
||||
sqlite3_db_readonly
|
||||
sqlite3_db_release_memory
|
||||
@@ -57,6 +66,9 @@ sqlite3_errmsg
|
||||
sqlite3_error_offset
|
||||
sqlite3_errstr
|
||||
sqlite3_exec
|
||||
sqlite3_filename_database
|
||||
sqlite3_filename_journal
|
||||
sqlite3_filename_wal
|
||||
sqlite3_finalize
|
||||
sqlite3_get_autocommit
|
||||
sqlite3_get_auxdata
|
||||
@@ -112,4 +124,7 @@ sqlite3_vtab_in_first
|
||||
sqlite3_vtab_in_next
|
||||
sqlite3_vtab_nochange
|
||||
sqlite3_vtab_on_conflict
|
||||
sqlite3_vtab_rhs_value
|
||||
sqlite3_vtab_rhs_value
|
||||
sqlite3_wal_autocheckpoint
|
||||
sqlite3_wal_checkpoint_v2
|
||||
sqlite3_wal_hook_go
|
||||
Binary file not shown.
@@ -16,7 +16,7 @@ func Example() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
|
||||
err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/array"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func Example_driver() {
|
||||
|
||||
@@ -128,12 +128,12 @@ func getAuxBlob(ctx sqlite3.Context, arg []sqlite3.Value, write bool) (*sqlite3.
|
||||
return ctx.Conn().OpenBlob(db, table, column, row, write)
|
||||
}
|
||||
|
||||
func setAuxBlob(ctx sqlite3.Context, blob *sqlite3.Blob, writer bool) {
|
||||
func setAuxBlob(ctx sqlite3.Context, blob *sqlite3.Blob, open bool) {
|
||||
// This ensures the blob is closed if db, table, column or write change.
|
||||
ctx.SetAuxData(0, blob) // db
|
||||
ctx.SetAuxData(1, blob) // table
|
||||
ctx.SetAuxData(2, blob) // column
|
||||
if writer {
|
||||
if open {
|
||||
ctx.SetAuxData(4, blob) // write
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/array"
|
||||
"github.com/ncruces/go-sqlite3/ext/blobio"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
@@ -27,7 +28,7 @@ func Example() {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
_, err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -79,8 +80,8 @@ func Test_readblob(t *testing.T) {
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS test1 (col);
|
||||
CREATE TABLE IF NOT EXISTS test2 (col);
|
||||
CREATE TABLE test1 (col);
|
||||
CREATE TABLE test2 (col);
|
||||
INSERT INTO test1 VALUES (x'cafe');
|
||||
INSERT INTO test2 VALUES (x'babe');
|
||||
`)
|
||||
@@ -139,8 +140,8 @@ func Test_openblob(t *testing.T) {
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS test1 (col);
|
||||
CREATE TABLE IF NOT EXISTS test2 (col);
|
||||
CREATE TABLE test1 (col);
|
||||
CREATE TABLE test2 (col);
|
||||
INSERT INTO test1 VALUES (x'cafe');
|
||||
INSERT INTO test2 VALUES (x'babe');
|
||||
`)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/csv"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
@@ -20,7 +21,7 @@ func Example() {
|
||||
csv.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS eurofxref USING csv(
|
||||
CREATE VIRTUAL TABLE eurofxref USING csv(
|
||||
filename = 'testdata/eurofxref.csv',
|
||||
header = YES,
|
||||
columns = 42,
|
||||
|
||||
68
ext/fileio/coro.go
Normal file
68
ext/fileio/coro.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Adapted from: https://research.swtch.com/coro
|
||||
|
||||
const errCoroCanceled = util.ErrorString("coroutine canceled")
|
||||
|
||||
func coroNew[In, Out any](f func(In, func(Out) In) Out) (resume func(In) (Out, bool), cancel func()) {
|
||||
type msg[T any] struct {
|
||||
panic any
|
||||
val T
|
||||
}
|
||||
|
||||
cin := make(chan msg[In])
|
||||
cout := make(chan msg[Out])
|
||||
running := true
|
||||
resume = func(in In) (out Out, ok bool) {
|
||||
if !running {
|
||||
return
|
||||
}
|
||||
cin <- msg[In]{val: in}
|
||||
m := <-cout
|
||||
if m.panic != nil {
|
||||
panic(m.panic)
|
||||
}
|
||||
return m.val, running
|
||||
}
|
||||
cancel = func() {
|
||||
if !running {
|
||||
return
|
||||
}
|
||||
e := fmt.Errorf("%w", errCoroCanceled)
|
||||
cin <- msg[In]{panic: e}
|
||||
m := <-cout
|
||||
if m.panic != nil && m.panic != e {
|
||||
panic(m.panic)
|
||||
}
|
||||
}
|
||||
yield := func(out Out) In {
|
||||
cout <- msg[Out]{val: out}
|
||||
m := <-cin
|
||||
if m.panic != nil {
|
||||
panic(m.panic)
|
||||
}
|
||||
return m.val
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if running {
|
||||
running = false
|
||||
cout <- msg[Out]{panic: recover()}
|
||||
}
|
||||
}()
|
||||
var out Out
|
||||
m := <-cin
|
||||
if m.panic == nil {
|
||||
out = f(m.val, yield)
|
||||
}
|
||||
running = false
|
||||
cout <- msg[Out]{val: out}
|
||||
}()
|
||||
return resume, cancel
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func Register(db *sqlite3.Conn) {
|
||||
// and the table-valued function fsdir;
|
||||
// fsys will be used to read files and list directories.
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
db.CreateFunction("lsmode", 1, 0, lsmode)
|
||||
db.CreateFunction("lsmode", 1, sqlite3.DETERMINISTIC, lsmode)
|
||||
db.CreateFunction("readfile", 1, sqlite3.DIRECTONLY, readfile(fsys))
|
||||
if fsys == nil {
|
||||
db.CreateFunction("writefile", -1, sqlite3.DIRECTONLY, writefile)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/fileio"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func Test_lsmode(t *testing.T) {
|
||||
|
||||
@@ -53,12 +53,12 @@ func (d fsdir) Open() (sqlite3.VTabCursor, error) {
|
||||
|
||||
type cursor struct {
|
||||
fsdir
|
||||
curr entry
|
||||
next chan entry
|
||||
done chan struct{}
|
||||
base string
|
||||
rowID int64
|
||||
eof bool
|
||||
base string
|
||||
resume func(struct{}) (entry, bool)
|
||||
cancel func()
|
||||
curr entry
|
||||
eof bool
|
||||
rowID int64
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
@@ -68,12 +68,8 @@ type entry struct {
|
||||
}
|
||||
|
||||
func (c *cursor) Close() error {
|
||||
if c.done != nil {
|
||||
close(c.done)
|
||||
s := <-c.next
|
||||
c.done = nil
|
||||
c.next = nil
|
||||
return s.err
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -96,16 +92,25 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
c.base = base
|
||||
}
|
||||
|
||||
c.rowID = 0
|
||||
c.resume, c.cancel = coroNew(func(_ struct{}, yield func(entry) struct{}) entry {
|
||||
walkDir := func(path string, d fs.DirEntry, err error) error {
|
||||
yield(entry{d, err, path})
|
||||
return nil
|
||||
}
|
||||
if c.fsys != nil {
|
||||
fs.WalkDir(c.fsys, root, walkDir)
|
||||
} else {
|
||||
filepath.WalkDir(root, walkDir)
|
||||
}
|
||||
return entry{}
|
||||
})
|
||||
c.eof = false
|
||||
c.next = make(chan entry)
|
||||
c.done = make(chan struct{})
|
||||
go c.WalkDir(root)
|
||||
c.rowID = 0
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (c *cursor) Next() error {
|
||||
curr, ok := <-c.next
|
||||
curr, ok := c.resume(struct{}{})
|
||||
c.curr = curr
|
||||
c.eof = !ok
|
||||
c.rowID++
|
||||
@@ -165,22 +170,3 @@ func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) WalkDir(path string) {
|
||||
defer close(c.next)
|
||||
|
||||
if c.fsys != nil {
|
||||
fs.WalkDir(c.fsys, path, c.WalkDirFunc)
|
||||
} else {
|
||||
filepath.WalkDir(path, c.WalkDirFunc)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cursor) WalkDirFunc(path string, d fs.DirEntry, err error) error {
|
||||
select {
|
||||
case <-c.done:
|
||||
return fs.SkipAll
|
||||
case c.next <- entry{d, err, path}:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/fileio"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func Test_fsdir(t *testing.T) {
|
||||
@@ -28,7 +29,7 @@ func Test_fsdir(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`SELECT * FROM fsdir('.', '.') LIMIT 4`)
|
||||
rows, err := db.Query(`SELECT * FROM fsdir('.', '.')`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func Test_writefile(t *testing.T) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
_ "golang.org/x/crypto/blake2b"
|
||||
_ "golang.org/x/crypto/blake2s"
|
||||
_ "golang.org/x/crypto/md4"
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/lines"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
@@ -58,7 +59,7 @@ func Example() {
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// Expected output:
|
||||
// US: 141001
|
||||
// GB: 22560
|
||||
// CA: 11759
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/pivot"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
// https://antonz.org/sqlite-pivot-table/
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/statement"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
|
||||
@@ -41,7 +41,16 @@ https://sqlite.org/lang_aggfunc.html
|
||||
- [X] `RANK() OVER window`
|
||||
- [X] `DENSE_RANK() OVER window`
|
||||
- [X] `PERCENT_RANK() OVER window`
|
||||
- [ ] `PERCENTILE_CONT(percentile) OVER window`
|
||||
- [ ] `PERCENTILE_DISC(percentile) OVER window`
|
||||
|
||||
https://sqlite.org/windowfunctions.html#builtins
|
||||
https://sqlite.org/windowfunctions.html#builtins
|
||||
|
||||
## Boolean aggregates
|
||||
|
||||
- [X] `EVERY(boolean)`
|
||||
- [X] `SOME(boolean)`
|
||||
|
||||
## Additional aggregates
|
||||
|
||||
- [X] `MEDIAN(expression)`
|
||||
- [X] `PERCENTILE_CONT(expression, fraction)`
|
||||
- [X] `PERCENTILE_DISC(expression, fraction)`
|
||||
46
ext/stats/boolean.go
Normal file
46
ext/stats/boolean.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package stats
|
||||
|
||||
import "github.com/ncruces/go-sqlite3"
|
||||
|
||||
const (
|
||||
every = iota
|
||||
some
|
||||
)
|
||||
|
||||
func newBoolean(kind int) func() sqlite3.AggregateFunction {
|
||||
return func() sqlite3.AggregateFunction { return &boolean{kind: kind} }
|
||||
}
|
||||
|
||||
type boolean struct {
|
||||
count int
|
||||
total int
|
||||
kind int
|
||||
}
|
||||
|
||||
func (b *boolean) Value(ctx sqlite3.Context) {
|
||||
if b.kind == every {
|
||||
ctx.ResultBool(b.count == b.total)
|
||||
} else {
|
||||
ctx.ResultBool(b.count > 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *boolean) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if arg[0].Type() == sqlite3.NULL {
|
||||
return
|
||||
}
|
||||
if arg[0].Bool() {
|
||||
b.count++
|
||||
}
|
||||
b.total++
|
||||
}
|
||||
|
||||
func (b *boolean) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if arg[0].Type() == sqlite3.NULL {
|
||||
return
|
||||
}
|
||||
if arg[0].Bool() {
|
||||
b.count--
|
||||
}
|
||||
b.total--
|
||||
}
|
||||
74
ext/stats/boolean_test.go
Normal file
74
ext/stats/boolean_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package stats_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/stats"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestRegister_boolean(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stats.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE data (x)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO data (x) VALUES (4), (7.0), (13), (NULL), (16), (3.14)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`
|
||||
SELECT
|
||||
every(x > 0),
|
||||
every(x > 10),
|
||||
some(x > 10),
|
||||
some(x > 20)
|
||||
FROM data`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnBool(0); got != true {
|
||||
t.Errorf("got %v, want true", got)
|
||||
}
|
||||
if got := stmt.ColumnBool(1); got != false {
|
||||
t.Errorf("got %v, want false", got)
|
||||
}
|
||||
if got := stmt.ColumnBool(2); got != true {
|
||||
t.Errorf("got %v, want true", got)
|
||||
}
|
||||
if got := stmt.ColumnBool(3); got != false {
|
||||
t.Errorf("got %v, want false", got)
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
stmt, _, err = db.Prepare(`SELECT every(x > 10) OVER (ROWS 1 PRECEDING) FROM data`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := [...]bool{false, false, false, true, true, false}
|
||||
for i := 0; stmt.Step(); i++ {
|
||||
if got := stmt.ColumnBool(0); got != want[i] {
|
||||
t.Errorf("got %v, want %v", got, want[i])
|
||||
}
|
||||
if got := stmt.ColumnType(0); got != sqlite3.INTEGER {
|
||||
t.Errorf("got %v, want INTEGER", got)
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
}
|
||||
94
ext/stats/percentile.go
Normal file
94
ext/stats/percentile.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/sort/quick"
|
||||
)
|
||||
|
||||
const (
|
||||
median = iota
|
||||
percentile_cont
|
||||
percentile_disc
|
||||
)
|
||||
|
||||
func newPercentile(kind int) func() sqlite3.AggregateFunction {
|
||||
return func() sqlite3.AggregateFunction { return &percentile{kind: kind} }
|
||||
}
|
||||
|
||||
type percentile struct {
|
||||
nums []float64
|
||||
arg1 []byte
|
||||
kind int
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
if q.kind != median {
|
||||
q.arg1 = arg[1].Blob(q.arg1[:0])
|
||||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
func (q *percentile) Value(ctx sqlite3.Context) {
|
||||
if len(q.nums) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
float float64
|
||||
floats []float64
|
||||
)
|
||||
if q.kind == median {
|
||||
float, err = getPercentile(q.nums, 0.5, false)
|
||||
ctx.ResultFloat(float)
|
||||
} else if err = json.Unmarshal(q.arg1, &float); err == nil {
|
||||
float, err = getPercentile(q.nums, float, q.kind == percentile_disc)
|
||||
ctx.ResultFloat(float)
|
||||
} else if err = json.Unmarshal(q.arg1, &floats); err == nil {
|
||||
err = getPercentiles(q.nums, floats, q.kind == percentile_disc)
|
||||
ctx.ResultJSON(floats)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("percentile: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
func getPercentile(nums []float64, pos float64, disc bool) (float64, error) {
|
||||
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))
|
||||
|
||||
if f == 0 || disc {
|
||||
return m0, nil
|
||||
}
|
||||
|
||||
m1 := slices.Min(nums[int(i)+1:])
|
||||
return math.FMA(f, m1, -math.FMA(f, m0, -m0)), nil
|
||||
}
|
||||
|
||||
func getPercentiles(nums []float64, pos []float64, disc bool) error {
|
||||
for i := range pos {
|
||||
v, err := getPercentile(nums, pos[i], disc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pos[i] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
124
ext/stats/percentile_test.go
Normal file
124
ext/stats/percentile_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package stats_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/stats"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestRegister_percentile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stats.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE data (x)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO data (x) VALUES (4), (7.0), ('13'), (NULL), (16)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`
|
||||
SELECT
|
||||
median(x),
|
||||
percentile_disc(x, 0.5),
|
||||
percentile_cont(x, '[0.25, 0.5, 0.75]')
|
||||
FROM data`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 10 {
|
||||
t.Errorf("got %v, want 10", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(1); got != 7 {
|
||||
t.Errorf("got %v, want 7", got)
|
||||
}
|
||||
var got []float64
|
||||
if err := stmt.ColumnJSON(2, &got); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !slices.Equal(got, []float64{6.25, 10, 13.75}) {
|
||||
t.Errorf("got %v, want [6.25 10 13.75]", got)
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
stmt, _, err = db.Prepare(`
|
||||
SELECT
|
||||
median(x),
|
||||
percentile_disc(x, 0.5),
|
||||
percentile_cont(x, '[0.25, 0.5, 0.75]')
|
||||
FROM data
|
||||
WHERE x < 5`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 4 {
|
||||
t.Errorf("got %v, want 4", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(1); got != 4 {
|
||||
t.Errorf("got %v, want 4", got)
|
||||
}
|
||||
var got []float64
|
||||
if err := stmt.ColumnJSON(2, &got); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !slices.Equal(got, []float64{4, 4, 4}) {
|
||||
t.Errorf("got %v, want [4 4 4]", got)
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
stmt, _, err = db.Prepare(`
|
||||
SELECT
|
||||
median(x),
|
||||
percentile_disc(x, 0.5),
|
||||
percentile_cont(x, '[0.25, 0.5, 0.75]')
|
||||
FROM data
|
||||
WHERE x < 0`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnType(0); got != sqlite3.NULL {
|
||||
t.Error("want NULL")
|
||||
}
|
||||
if got := stmt.ColumnType(1); got != sqlite3.NULL {
|
||||
t.Error("want NULL")
|
||||
}
|
||||
if got := stmt.ColumnType(2); got != sqlite3.NULL {
|
||||
t.Error("want NULL")
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
stmt, _, err = db.Prepare(`
|
||||
SELECT
|
||||
percentile_disc(x, -2),
|
||||
percentile_cont(x, +2),
|
||||
percentile_cont(x, ''),
|
||||
percentile_cont(x, '[100]')
|
||||
FROM data`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
t.Error("want error")
|
||||
}
|
||||
stmt.Close()
|
||||
}
|
||||
@@ -18,6 +18,11 @@
|
||||
// - regr_slope: slope of the least-squares-fit linear equation
|
||||
// - regr_intercept: y-intercept of the least-squares-fit linear equation
|
||||
// - regr_json: all regr stats in a JSON object
|
||||
// - percentile_disc: discrete percentile
|
||||
// - percentile_cont: continuous percentile
|
||||
// - median: median value
|
||||
// - every: boolean and
|
||||
// - some: boolean or
|
||||
//
|
||||
// These join the [Built-in Aggregate Functions]:
|
||||
// - count: count rows/values
|
||||
@@ -26,9 +31,16 @@
|
||||
// - min: minimum value
|
||||
// - max: maximum value
|
||||
//
|
||||
// And the [Built-in Window Functions]:
|
||||
// - rank: rank of the current row with gaps
|
||||
// - dense_rank: rank of the current row without gaps
|
||||
// - percent_rank: relative rank of the row
|
||||
// - cume_dist: cumulative distribution
|
||||
//
|
||||
// See: [ANSI SQL Aggregate Functions]
|
||||
//
|
||||
// [Built-in Aggregate Functions]: https://sqlite.org/lang_aggfunc.html
|
||||
// [Built-in Window Functions]: https://sqlite.org/windowfunctions.html#builtins
|
||||
// [ANSI SQL Aggregate Functions]: https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
|
||||
package stats
|
||||
|
||||
@@ -54,6 +66,11 @@ func Register(db *sqlite3.Conn) {
|
||||
db.CreateWindowFunction("regr_intercept", 2, flags, newCovariance(regr_intercept))
|
||||
db.CreateWindowFunction("regr_count", 2, flags, newCovariance(regr_count))
|
||||
db.CreateWindowFunction("regr_json", 2, flags, newCovariance(regr_json))
|
||||
db.CreateWindowFunction("median", 1, flags, newPercentile(median))
|
||||
db.CreateWindowFunction("percentile_cont", 2, flags, newPercentile(percentile_cont))
|
||||
db.CreateWindowFunction("percentile_disc", 2, flags, newPercentile(percentile_disc))
|
||||
db.CreateWindowFunction("every", 1, flags, newBoolean(every))
|
||||
db.CreateWindowFunction("some", 1, flags, newBoolean(some))
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/stats"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestRegister_variance(t *testing.T) {
|
||||
@@ -20,7 +21,7 @@ func TestRegister_variance(t *testing.T) {
|
||||
|
||||
stats.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS data (x)`)
|
||||
err = db.Exec(`CREATE TABLE data (x)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -39,8 +40,6 @@ func TestRegister_variance(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 40 {
|
||||
t.Errorf("got %v, want 40", got)
|
||||
@@ -61,24 +60,23 @@ func TestRegister_variance(t *testing.T) {
|
||||
t.Errorf("got %v, want √22.5", got)
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
{
|
||||
stmt, _, err := db.Prepare(`SELECT var_samp(x) OVER (ROWS 1 PRECEDING) FROM data`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
stmt, _, err = db.Prepare(`SELECT var_samp(x) OVER (ROWS 1 PRECEDING) FROM data`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := [...]float64{0, 4.5, 18, 0, 0}
|
||||
for i := 0; stmt.Step(); i++ {
|
||||
if got := stmt.ColumnFloat(0); got != want[i] {
|
||||
t.Errorf("got %v, want %v", got, want[i])
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
want := [...]float64{0, 4.5, 18, 0, 0}
|
||||
for i := 0; stmt.Step(); i++ {
|
||||
if got := stmt.ColumnFloat(0); got != want[i] {
|
||||
t.Errorf("got %v, want %v", got, want[i])
|
||||
}
|
||||
if got := stmt.ColumnType(0); (got == sqlite3.FLOAT) != (want[i] != 0) {
|
||||
t.Errorf("got %v, want %v", got, want[i])
|
||||
}
|
||||
if got := stmt.ColumnType(0); (got == sqlite3.FLOAT) != (want[i] != 0) {
|
||||
t.Errorf("got %v, want %v", got, want[i])
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
}
|
||||
|
||||
func TestRegister_covariance(t *testing.T) {
|
||||
@@ -92,7 +90,7 @@ func TestRegister_covariance(t *testing.T) {
|
||||
|
||||
stats.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS data (y, x)`)
|
||||
err = db.Exec(`CREATE TABLE data (y, x)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -112,8 +110,6 @@ func TestRegister_covariance(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnFloat(0); got != 0.9881049293224639 {
|
||||
t.Errorf("got %v, want 0.9881049293224639", got)
|
||||
@@ -158,27 +154,29 @@ func TestRegister_covariance(t *testing.T) {
|
||||
t.Errorf("got %v, want 5", got)
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
{
|
||||
stmt, _, err := db.Prepare(`SELECT covar_samp(y, x) OVER (ROWS 1 PRECEDING) FROM data`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
stmt, _, err = db.Prepare(`SELECT covar_samp(y, x) OVER (ROWS 1 PRECEDING) FROM data`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := [...]float64{0, 10, 30, 75, 22.5}
|
||||
for i := 0; stmt.Step(); i++ {
|
||||
if got := stmt.ColumnFloat(0); got != want[i] {
|
||||
t.Errorf("got %v, want %v", got, want[i])
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
want := [...]float64{0, 10, 30, 75, 22.5}
|
||||
for i := 0; stmt.Step(); i++ {
|
||||
if got := stmt.ColumnFloat(0); got != want[i] {
|
||||
t.Errorf("got %v, want %v", got, want[i])
|
||||
}
|
||||
if got := stmt.ColumnType(0); (got == sqlite3.FLOAT) != (want[i] != 0) {
|
||||
t.Errorf("got %v, want %v", got, want[i])
|
||||
}
|
||||
if got := stmt.ColumnType(0); (got == sqlite3.FLOAT) != (want[i] != 0) {
|
||||
t.Errorf("got %v, want %v", got, want[i])
|
||||
}
|
||||
}
|
||||
stmt.Close()
|
||||
}
|
||||
|
||||
func Benchmark_average(b *testing.B) {
|
||||
sqlite3.Initialize()
|
||||
b.ResetTimer()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
@@ -210,6 +208,9 @@ func Benchmark_average(b *testing.B) {
|
||||
}
|
||||
|
||||
func Benchmark_variance(b *testing.B) {
|
||||
sqlite3.Initialize()
|
||||
b.ResetTimer()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
@@ -81,7 +82,7 @@ func TestRegister_collation(t *testing.T) {
|
||||
|
||||
Register(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
|
||||
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
58
ext/zorder/zorder.go
Normal file
58
ext/zorder/zorder.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Package zorder provides functions for z-order transformations.
|
||||
//
|
||||
// https://sqlite.org/src/doc/tip/ext/misc/zorder.c
|
||||
package zorder
|
||||
|
||||
import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Register registers the zorder and unzorder SQL functions.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
db.CreateFunction("zorder", -1, flags, zorder)
|
||||
db.CreateFunction("unzorder", 3, flags, unzorder)
|
||||
}
|
||||
|
||||
func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
var x [63]int64
|
||||
for i := range arg {
|
||||
x[i] = arg[i].Int64()
|
||||
}
|
||||
if len(arg) > len(x) {
|
||||
ctx.ResultError(util.ErrorString("zorder: too many parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
var z int64
|
||||
if len(arg) > 0 {
|
||||
for i := 0; i < 63; i++ {
|
||||
j := i % len(arg)
|
||||
z |= (x[j] & 1) << i
|
||||
x[j] >>= 1
|
||||
}
|
||||
}
|
||||
|
||||
for i := range arg {
|
||||
if x[i] != 0 {
|
||||
ctx.ResultError(util.ErrorString("zorder: parameter too large"))
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.ResultInt64(z)
|
||||
}
|
||||
|
||||
func unzorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
z := arg[0].Int64()
|
||||
n := arg[1].Int64()
|
||||
i := arg[2].Int64()
|
||||
|
||||
var k int
|
||||
var x int64
|
||||
for j := i; j < 63; j += n {
|
||||
x |= ((z >> j) & 1) << k
|
||||
k++
|
||||
}
|
||||
ctx.ResultInt64(x)
|
||||
}
|
||||
107
ext/zorder/zorder_test.go
Normal file
107
ext/zorder/zorder_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package zorder_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/zorder"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestRegister_zorder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
zorder.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var got int64
|
||||
err = db.QueryRow(`SELECT zorder(2, 3)`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != 14 {
|
||||
t.Errorf("got %d, want 14", got)
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT zorder(4, 5)`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != 50 {
|
||||
t.Errorf("got %d, want 14", got)
|
||||
}
|
||||
|
||||
var check bool
|
||||
err = db.QueryRow(`SELECT zorder(3, 4) BETWEEN zorder(2, 3) AND zorder(4, 5)`).Scan(&check)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !check {
|
||||
t.Error("want true")
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT zorder(2, 2) NOT BETWEEN zorder(2, 3) AND zorder(4, 5)`).Scan(&check)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !check {
|
||||
t.Error("want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_unzorder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
zorder.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var got int64
|
||||
err = db.QueryRow(`SELECT unzorder(zorder(3, 4), 2, 0)`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != 3 {
|
||||
t.Errorf("got %d, want 3", got)
|
||||
}
|
||||
|
||||
err = db.QueryRow(`SELECT unzorder(zorder(3, 4), 2, 1)`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != 4 {
|
||||
t.Errorf("got %d, want 4", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
zorder.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var got int64
|
||||
err = db.QueryRow(`SELECT zorder(1, 2, 3, 100000)`).Scan(&got)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/unicode"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func ExampleConn_CreateCollation() {
|
||||
@@ -18,7 +19,7 @@ func ExampleConn_CreateCollation() {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
|
||||
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -66,7 +67,7 @@ func ExampleConn_CreateFunction() {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
|
||||
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -111,7 +112,7 @@ func ExampleContext_SetAuxData() {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
|
||||
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func ExampleConn_CreateWindowFunction() {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
|
||||
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
13
go.mod
13
go.mod
@@ -5,11 +5,14 @@ go 1.21
|
||||
require (
|
||||
github.com/ncruces/julianday v1.0.0
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/tetratelabs/wazero v1.6.0
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/sys v0.16.0
|
||||
golang.org/x/text v0.14.0
|
||||
github.com/tetratelabs/wazero v1.7.2
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/text v0.16.0
|
||||
lukechampine.com/adiantum v1.1.1
|
||||
)
|
||||
|
||||
require github.com/ncruces/sort v0.1.2
|
||||
|
||||
retract v0.4.0 // tagged from the wrong branch
|
||||
|
||||
24
go.sum
24
go.sum
@@ -1,14 +1,18 @@
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/ncruces/sort v0.1.2 h1:zKQ9CA4fpHPF6xsUhRTfi5EEryspuBpe/QA4VWQOV1U=
|
||||
github.com/ncruces/sort v0.1.2/go.mod h1:vEJUTBJtebIuCMmXD18GKo5GJGhsay+xZFOoBEIXFmE=
|
||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||
github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g=
|
||||
github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
|
||||
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
var (
|
||||
sqliteSeparator = "`|\"|'|\t"
|
||||
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^CONSTRAINT [%v]?[\w-]+[%v]? UNIQUE (.*)$`, sqliteSeparator, sqliteSeparator))
|
||||
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))
|
||||
@@ -103,11 +104,24 @@ func parseDDL(strs ...string) (*ddl, error) {
|
||||
|
||||
for _, f := range result.fields {
|
||||
fUpper := strings.ToUpper(f)
|
||||
if strings.HasPrefix(fUpper, "CHECK") ||
|
||||
strings.HasPrefix(fUpper, "CONSTRAINT") {
|
||||
if strings.HasPrefix(fUpper, "CHECK") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(fUpper, "CONSTRAINT") {
|
||||
matches := uniqueRegexp.FindStringSubmatch(f)
|
||||
if len(matches) > 0 {
|
||||
if columns := getAllColumns(matches[1]); len(columns) == 1 {
|
||||
for idx, column := range result.columns {
|
||||
if column.NameValue.String == columns[0] {
|
||||
column.UniqueValue = sql.NullBool{Bool: true, Valid: true}
|
||||
result.columns[idx] = column
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(fUpper, "PRIMARY KEY") {
|
||||
for _, name := range getAllColumns(f) {
|
||||
for idx, column := range result.columns {
|
||||
@@ -159,14 +173,7 @@ func parseDDL(strs ...string) (*ddl, error) {
|
||||
}
|
||||
}
|
||||
} else if matches := indexRegexp.FindStringSubmatch(str); len(matches) > 0 {
|
||||
for _, column := range getAllColumns(matches[1]) {
|
||||
for idx, c := range result.columns {
|
||||
if c.NameValue.String == column {
|
||||
c.UniqueValue = sql.NullBool{Bool: strings.ToUpper(strings.Fields(str)[1]) == "UNIQUE", Valid: true}
|
||||
result.columns[idx] = c
|
||||
}
|
||||
}
|
||||
}
|
||||
// don't report Unique by UniqueIndex
|
||||
} else {
|
||||
return nil, errors.New("invalid DDL")
|
||||
}
|
||||
@@ -269,20 +276,6 @@ func (d *ddl) getColumns() []string {
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *ddl) alterColumn(name, sql string) bool {
|
||||
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
|
||||
|
||||
for i := 0; i < len(d.fields); i++ {
|
||||
if reg.MatchString(d.fields[i]) {
|
||||
d.fields[i] = sql
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
d.fields = append(d.fields, sql)
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *ddl) removeColumn(name string) bool {
|
||||
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
|
||||
|
||||
|
||||
@@ -16,11 +16,12 @@ func TestParseDDL(t *testing.T) {
|
||||
columns []migrator.ColumnType
|
||||
}{
|
||||
{"with_fk", []string{
|
||||
"CREATE TABLE `notes` (`id` integer NOT NULL,`text` varchar(500) DEFAULT \"hello\",`age` integer DEFAULT 18,`user_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))",
|
||||
"CREATE TABLE `notes` (" +
|
||||
"`id` integer NOT NULL,`text` varchar(500) DEFAULT \"hello\",`age` integer DEFAULT 18,`user_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))",
|
||||
"CREATE UNIQUE INDEX `idx_profiles_refer` ON `profiles`(`text`)",
|
||||
}, 6, []migrator.ColumnType{
|
||||
{NameValue: sql.NullString{String: "id", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, PrimaryKeyValue: sql.NullBool{Bool: true, Valid: true}, NullableValue: sql.NullBool{Valid: true}, UniqueValue: sql.NullBool{Valid: true}, DefaultValueValue: sql.NullString{Valid: false}},
|
||||
{NameValue: sql.NullString{String: "text", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 500, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(500)", Valid: true}, DefaultValueValue: sql.NullString{String: "hello", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Bool: true, Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
|
||||
{NameValue: sql.NullString{String: "text", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 500, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(500)", Valid: true}, DefaultValueValue: sql.NullString{String: "hello", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Bool: false, Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
|
||||
{NameValue: sql.NullString{String: "age", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, DefaultValueValue: sql.NullString{String: "18", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
|
||||
{NameValue: sql.NullString{String: "user_id", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
|
||||
},
|
||||
@@ -56,28 +57,54 @@ func TestParseDDL(t *testing.T) {
|
||||
ColumnTypeValue: sql.NullString{String: "int", Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: false, Valid: true},
|
||||
DefaultValueValue: sql.NullString{Valid: false},
|
||||
UniqueValue: sql.NullBool{Bool: true, Valid: true},
|
||||
UniqueValue: sql.NullBool{Bool: false, Valid: true},
|
||||
PrimaryKeyValue: sql.NullBool{Valid: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
}, {
|
||||
"unique index",
|
||||
[]string{
|
||||
"CREATE TABLE `test-b` (`field` integer NOT NULL)",
|
||||
"CREATE UNIQUE INDEX `idx_uq` ON `test-b`(`field`) WHERE field = 0",
|
||||
},
|
||||
1,
|
||||
[]migrator.ColumnType{
|
||||
{
|
||||
NameValue: sql.NullString{String: "field", Valid: true},
|
||||
DataTypeValue: sql.NullString{String: "integer", Valid: true},
|
||||
ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
|
||||
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
|
||||
UniqueValue: sql.NullBool{Bool: true, Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: false, Valid: true},
|
||||
},
|
||||
[]migrator.ColumnType{{
|
||||
NameValue: sql.NullString{String: "field", Valid: true},
|
||||
DataTypeValue: sql.NullString{String: "integer", Valid: true},
|
||||
ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
|
||||
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
|
||||
UniqueValue: sql.NullBool{Bool: false, Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: false, Valid: true},
|
||||
}},
|
||||
}, {
|
||||
"normal index",
|
||||
[]string{
|
||||
"CREATE TABLE `test-c` (`field` integer NOT NULL)",
|
||||
"CREATE INDEX `idx_uq` ON `test-c`(`field`)",
|
||||
},
|
||||
1,
|
||||
[]migrator.ColumnType{{
|
||||
NameValue: sql.NullString{String: "field", Valid: true},
|
||||
DataTypeValue: sql.NullString{String: "integer", Valid: true},
|
||||
ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
|
||||
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
|
||||
UniqueValue: sql.NullBool{Bool: false, Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: false, Valid: true},
|
||||
}},
|
||||
}, {
|
||||
"unique constraint",
|
||||
[]string{
|
||||
"CREATE TABLE `unique_struct` (`name` text,CONSTRAINT `uni_unique_struct_name` UNIQUE (`name`))",
|
||||
},
|
||||
2,
|
||||
[]migrator.ColumnType{{
|
||||
NameValue: sql.NullString{String: "name", Valid: true},
|
||||
DataTypeValue: sql.NullString{String: "text", Valid: true},
|
||||
ColumnTypeValue: sql.NullString{String: "text", Valid: true},
|
||||
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
|
||||
UniqueValue: sql.NullBool{Bool: true, Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: true, Valid: true},
|
||||
}},
|
||||
},
|
||||
{
|
||||
"non-unique index",
|
||||
|
||||
@@ -3,9 +3,9 @@ set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/master/ddlmod.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/master/ddlmod_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/master/error_translator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/master/migrator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/master/sqlite.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/master/sqlite_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/ddlmod.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/ddlmod_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/error_translator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/migrator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/sqlite.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/sqlite_test.go"
|
||||
@@ -3,14 +3,14 @@ module github.com/ncruces/go-sqlite3/gormlite
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/ncruces/go-sqlite3 v0.12.0
|
||||
gorm.io/gorm v1.25.5
|
||||
github.com/ncruces/go-sqlite3 v0.16.0
|
||||
gorm.io/gorm v1.25.10
|
||||
)
|
||||
|
||||
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.6.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
golang.org/x/sys 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.12.0 h1:ZqCziV5XxdwiDOIOChRjhIwJqmMOctSrYwxwyzYx5zw=
|
||||
github.com/ncruces/go-sqlite3 v0.12.0/go.mod h1:+8dWcBxb2Yar4EcCwav1a21MpKZbztwOYBLSRYt9bMY=
|
||||
github.com/ncruces/go-sqlite3 v0.16.0 h1:O7eULuEjvSBnS1QCN+dDL/ixLQZoUGWr466A02Gx1xc=
|
||||
github.com/ncruces/go-sqlite3 v0.16.0/go.mod h1:2TmAeD93ImsKXJRsUIKohfMvt17dZSbS6pzJ3k6YYFg=
|
||||
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.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g=
|
||||
github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
|
||||
@@ -79,14 +79,28 @@ func (m _Migrator) AlterColumn(value interface{}, name string) error {
|
||||
return m.RunWithoutForeignKey(func() error {
|
||||
return m.recreateTable(value, nil, func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
|
||||
if field := stmt.Schema.LookUpField(name); field != nil {
|
||||
if ddl.alterColumn(field.DBName, fmt.Sprintf("`%s` ?", field.DBName)) {
|
||||
return nil, nil, fmt.Errorf("field `%s` not found in origin ddl, ddl= '%s'", name, ddl.compile())
|
||||
var sqlArgs []interface{}
|
||||
for i, f := range ddl.fields {
|
||||
if matches := columnRegexp.FindStringSubmatch(f); len(matches) > 1 && matches[1] == field.DBName {
|
||||
ddl.fields[i] = fmt.Sprintf("`%v` ?", field.DBName)
|
||||
sqlArgs = []interface{}{m.FullDataTypeOf(field)}
|
||||
// table created by old version might look like `CREATE TABLE ? (? varchar(10) UNIQUE)`.
|
||||
// FullDataTypeOf doesn't contain UNIQUE, so we need to add unique constraint.
|
||||
if strings.Contains(strings.ToUpper(matches[3]), " UNIQUE") {
|
||||
uniName := m.DB.NamingStrategy.UniqueName(stmt.Table, field.DBName)
|
||||
uni, _ := m.GuessConstraintInterfaceAndTable(stmt, uniName)
|
||||
if uni != nil {
|
||||
uniSQL, uniArgs := uni.Build()
|
||||
ddl.addConstraint(uniName, uniSQL)
|
||||
sqlArgs = append(sqlArgs, uniArgs...)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ddl, []interface{}{m.FullDataTypeOf(field)}, nil
|
||||
return ddl, sqlArgs, nil
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("failed to alter field with name `%s`", name)
|
||||
return nil, nil, fmt.Errorf("failed to alter field with name %v", name)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -153,7 +167,7 @@ func (m _Migrator) DropColumn(value interface{}, name string) error {
|
||||
|
||||
func (m _Migrator) CreateConstraint(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, chk, table := m.GuessConstraintAndTable(stmt, name)
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
|
||||
return m.recreateTable(value, &table,
|
||||
func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
|
||||
@@ -164,12 +178,8 @@ func (m _Migrator) CreateConstraint(value interface{}, name string) error {
|
||||
)
|
||||
|
||||
if constraint != nil {
|
||||
constraintName = constraint.Name
|
||||
constraintSql, constraintValues = buildConstraint(constraint)
|
||||
} else if chk != nil {
|
||||
constraintName = chk.Name
|
||||
constraintSql = "CONSTRAINT ? CHECK (?)"
|
||||
constraintValues = []interface{}{clause.Column{Name: chk.Name}, clause.Expr{SQL: chk.Constraint}}
|
||||
constraintName = constraint.GetName()
|
||||
constraintSql, constraintValues = constraint.Build()
|
||||
} else {
|
||||
return nil, nil, nil
|
||||
}
|
||||
@@ -182,11 +192,9 @@ func (m _Migrator) CreateConstraint(value interface{}, name string) error {
|
||||
|
||||
func (m _Migrator) DropConstraint(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, chk, table := m.GuessConstraintAndTable(stmt, name)
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
if constraint != nil {
|
||||
name = constraint.Name
|
||||
} else if chk != nil {
|
||||
name = chk.Name
|
||||
name = constraint.GetName()
|
||||
}
|
||||
|
||||
return m.recreateTable(value, &table,
|
||||
@@ -200,11 +208,9 @@ func (m _Migrator) DropConstraint(value interface{}, name string) error {
|
||||
func (m _Migrator) HasConstraint(value interface{}, name string) bool {
|
||||
var count int64
|
||||
m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, chk, table := m.GuessConstraintAndTable(stmt, name)
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
if constraint != nil {
|
||||
name = constraint.Name
|
||||
} else if chk != nil {
|
||||
name = chk.Name
|
||||
name = constraint.GetName()
|
||||
}
|
||||
|
||||
m.DB.Raw(
|
||||
@@ -317,26 +323,44 @@ func (m _Migrator) DropIndex(value interface{}, name string) error {
|
||||
})
|
||||
}
|
||||
|
||||
func buildConstraint(constraint *schema.Constraint) (sql string, results []interface{}) {
|
||||
sql = "CONSTRAINT ? FOREIGN KEY ? REFERENCES ??"
|
||||
if constraint.OnDelete != "" {
|
||||
sql += " ON DELETE " + constraint.OnDelete
|
||||
}
|
||||
type _Index struct {
|
||||
Seq int
|
||||
Name string
|
||||
Unique bool
|
||||
Origin string
|
||||
Partial bool
|
||||
}
|
||||
|
||||
if constraint.OnUpdate != "" {
|
||||
sql += " ON UPDATE " + constraint.OnUpdate
|
||||
}
|
||||
|
||||
var foreignKeys, references []interface{}
|
||||
for _, field := range constraint.ForeignKeys {
|
||||
foreignKeys = append(foreignKeys, clause.Column{Name: field.DBName})
|
||||
}
|
||||
|
||||
for _, field := range constraint.References {
|
||||
references = append(references, clause.Column{Name: field.DBName})
|
||||
}
|
||||
results = append(results, clause.Table{Name: constraint.Name}, foreignKeys, clause.Table{Name: constraint.ReferenceSchema.Table}, references)
|
||||
return
|
||||
// GetIndexes return Indexes []gorm.Index and execErr error,
|
||||
// See the [doc]
|
||||
//
|
||||
// [doc]: https://sqlite.org/pragma.html#pragma_index_list
|
||||
func (m _Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) {
|
||||
indexes := make([]gorm.Index, 0)
|
||||
err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
rst := make([]*_Index, 0)
|
||||
if err := m.DB.Debug().Raw("SELECT * FROM PRAGMA_index_list(?)", stmt.Table).Scan(&rst).Error; err != nil { // alias `PRAGMA index_list(?)`
|
||||
return err
|
||||
}
|
||||
for _, index := range rst {
|
||||
if index.Origin == "u" { // skip the index was created by a UNIQUE constraint
|
||||
continue
|
||||
}
|
||||
var columns []string
|
||||
if err := m.DB.Raw("SELECT name FROM PRAGMA_index_info(?)", index.Name).Scan(&columns).Error; err != nil { // alias `PRAGMA index_info(?)`
|
||||
return err
|
||||
}
|
||||
indexes = append(indexes, &migrator.Index{
|
||||
TableName: stmt.Table,
|
||||
NameValue: index.Name,
|
||||
ColumnList: columns,
|
||||
PrimaryKeyValue: sql.NullBool{Bool: index.Origin == "pk", Valid: true}, // The exceptions are INTEGER PRIMARY KEY
|
||||
UniqueValue: sql.NullBool{Bool: index.Unique, Valid: true},
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return indexes, err
|
||||
}
|
||||
|
||||
func (m _Migrator) getRawDDL(table string) (string, error) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestDialector(t *testing.T) {
|
||||
|
||||
@@ -3,10 +3,11 @@ set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
rm -rf gorm/ tests/
|
||||
go work use -r .
|
||||
go test
|
||||
|
||||
rm -rf gorm/ tests/
|
||||
git clone --branch v1.25.5 --filter=blob:none https://github.com/go-gorm/gorm.git
|
||||
git clone --branch v1.25.10 --filter=blob:none https://github.com/go-gorm/gorm.git
|
||||
mv gorm/tests tests
|
||||
rm -rf gorm/
|
||||
|
||||
|
||||
9
internal/alloc/alloc_other.go
Normal file
9
internal/alloc/alloc_other.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !(unix || windows) || sqlite3_nosys
|
||||
|
||||
package alloc
|
||||
|
||||
import "github.com/tetratelabs/wazero/experimental"
|
||||
|
||||
func Virtual(cap, max uint64) experimental.LinearMemory {
|
||||
return Slice(cap, max)
|
||||
}
|
||||
24
internal/alloc/alloc_slice.go
Normal file
24
internal/alloc/alloc_slice.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//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
|
||||
}
|
||||
67
internal/alloc/alloc_unix.go
Normal file
67
internal/alloc/alloc_unix.go
Normal file
@@ -0,0 +1,67 @@
|
||||
//go:build unix && !sqlite3_nosys
|
||||
|
||||
package alloc
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func Virtual(_, max uint64) experimental.LinearMemory {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(unix.Getpagesize() - 1)
|
||||
max = (max + rnd) &^ rnd
|
||||
|
||||
if max > math.MaxInt {
|
||||
// This ensures int(max) overflows to a negative value,
|
||||
// and unix.Mmap returns EINVAL.
|
||||
max = math.MaxUint64
|
||||
}
|
||||
|
||||
// Reserve max bytes of address space, to ensure we won't need to move it.
|
||||
// A protected, private, anonymous mapping should not commit memory.
|
||||
b, err := unix.Mmap(-1, 0, int(max), unix.PROT_NONE, unix.MAP_PRIVATE|unix.MAP_ANON)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &mmappedMemory{buf: b[:0]}
|
||||
}
|
||||
|
||||
// The slice covers the entire mmapped memory:
|
||||
// - len(buf) is the already committed memory,
|
||||
// - cap(buf) is the reserved address space.
|
||||
type mmappedMemory struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (m *mmappedMemory) Reallocate(size uint64) []byte {
|
||||
com := uint64(len(m.buf))
|
||||
res := uint64(cap(m.buf))
|
||||
if com < size && size < res {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(unix.Getpagesize() - 1)
|
||||
new := (size + rnd) &^ rnd
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Update committed memory.
|
||||
m.buf = m.buf[:new]
|
||||
}
|
||||
// Limit returned capacity because bytes beyond
|
||||
// len(m.buf) have not yet been committed.
|
||||
return m.buf[:size:len(m.buf)]
|
||||
}
|
||||
|
||||
func (m *mmappedMemory) Free() {
|
||||
err := unix.Munmap(m.buf[:cap(m.buf)])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
m.buf = nil
|
||||
}
|
||||
76
internal/alloc/alloc_windows.go
Normal file
76
internal/alloc/alloc_windows.go
Normal file
@@ -0,0 +1,76 @@
|
||||
//go:build !sqlite3_nosys
|
||||
|
||||
package alloc
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func Virtual(_, max uint64) experimental.LinearMemory {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(windows.Getpagesize() - 1)
|
||||
max = (max + rnd) &^ rnd
|
||||
|
||||
if max > math.MaxInt {
|
||||
// This ensures uintptr(max) overflows to a large value,
|
||||
// and windows.VirtualAlloc returns an error.
|
||||
max = math.MaxUint64
|
||||
}
|
||||
|
||||
// Reserve max bytes of address space, to ensure we won't need to move it.
|
||||
// This does not commit memory.
|
||||
r, err := windows.VirtualAlloc(0, uintptr(max), windows.MEM_RESERVE, windows.PAGE_READWRITE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mem := virtualMemory{addr: r}
|
||||
// SliceHeader, although deprecated, avoids a go vet warning.
|
||||
sh := (*reflect.SliceHeader)(unsafe.Pointer(&mem.buf))
|
||||
sh.Cap = int(max)
|
||||
sh.Data = r
|
||||
return &mem
|
||||
}
|
||||
|
||||
// The slice covers the entire mmapped memory:
|
||||
// - len(buf) is the already committed memory,
|
||||
// - cap(buf) is the reserved address space.
|
||||
type virtualMemory struct {
|
||||
buf []byte
|
||||
addr uintptr
|
||||
}
|
||||
|
||||
func (m *virtualMemory) Reallocate(size uint64) []byte {
|
||||
com := uint64(len(m.buf))
|
||||
res := uint64(cap(m.buf))
|
||||
if com < size && size < res {
|
||||
// Round up to the page size.
|
||||
rnd := uint64(windows.Getpagesize() - 1)
|
||||
new := (size + rnd) &^ rnd
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Update committed memory.
|
||||
m.buf = m.buf[:new]
|
||||
}
|
||||
// Limit returned capacity because bytes beyond
|
||||
// len(m.buf) have not yet been committed.
|
||||
return m.buf[:size:len(m.buf)]
|
||||
}
|
||||
|
||||
func (m *virtualMemory) Free() {
|
||||
err := windows.VirtualFree(m.addr, 0, windows.MEM_RELEASE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
m.addr = 0
|
||||
}
|
||||
29
internal/testcfg/testcfg.go
Normal file
29
internal/testcfg/testcfg.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package testcfg
|
||||
|
||||
import (
|
||||
"math/bits"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/tetratelabs/wazero"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if bits.UintSize < 64 {
|
||||
return
|
||||
}
|
||||
|
||||
sqlite3.RuntimeConfig = wazero.NewRuntimeConfig().
|
||||
WithMemoryCapacityFromMax(true).
|
||||
WithMemoryLimitPages(1024)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package util
|
||||
|
||||
// https://sqlite.com/matrix/rescode.html
|
||||
// https://sqlite.com/rescode.html
|
||||
const (
|
||||
OK = 0 /* Successful result */
|
||||
|
||||
|
||||
@@ -3,21 +3,11 @@ package util
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
)
|
||||
|
||||
type handleKey struct{}
|
||||
type handleState struct {
|
||||
handles []any
|
||||
empty int
|
||||
}
|
||||
|
||||
func NewContext(ctx context.Context) context.Context {
|
||||
state := new(handleState)
|
||||
ctx = experimental.WithCloseNotifier(ctx, state)
|
||||
ctx = context.WithValue(ctx, handleKey{}, state)
|
||||
return ctx
|
||||
holes int
|
||||
}
|
||||
|
||||
func (s *handleState) CloseNotify(ctx context.Context, exitCode uint32) {
|
||||
@@ -27,14 +17,14 @@ func (s *handleState) CloseNotify(ctx context.Context, exitCode uint32) {
|
||||
}
|
||||
}
|
||||
s.handles = nil
|
||||
s.empty = 0
|
||||
s.holes = 0
|
||||
}
|
||||
|
||||
func GetHandle(ctx context.Context, id uint32) any {
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
s := ctx.Value(handleKey{}).(*handleState)
|
||||
s := ctx.Value(moduleKey{}).(*moduleState)
|
||||
return s.handles[^id]
|
||||
}
|
||||
|
||||
@@ -42,10 +32,10 @@ func DelHandle(ctx context.Context, id uint32) error {
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
s := ctx.Value(handleKey{}).(*handleState)
|
||||
s := ctx.Value(moduleKey{}).(*moduleState)
|
||||
a := s.handles[^id]
|
||||
s.handles[^id] = nil
|
||||
s.empty++
|
||||
s.holes++
|
||||
if c, ok := a.(io.Closer); ok {
|
||||
return c.Close()
|
||||
}
|
||||
@@ -56,13 +46,13 @@ func AddHandle(ctx context.Context, a any) (id uint32) {
|
||||
if a == nil {
|
||||
panic(NilErr)
|
||||
}
|
||||
s := ctx.Value(handleKey{}).(*handleState)
|
||||
s := ctx.Value(moduleKey{}).(*moduleState)
|
||||
|
||||
// Find an empty slot.
|
||||
if s.empty > cap(s.handles)-len(s.handles) {
|
||||
if s.holes > cap(s.handles)-len(s.handles) {
|
||||
for id, h := range s.handles {
|
||||
if h == nil {
|
||||
s.empty--
|
||||
s.holes--
|
||||
s.handles[id] = a
|
||||
return ^uint32(id)
|
||||
}
|
||||
|
||||
98
internal/util/mmap.go
Normal file
98
internal/util/mmap.go
Normal file
@@ -0,0 +1,98 @@
|
||||
//go:build unix && (amd64 || arm64 || riscv64) && !(sqlite3_noshm || sqlite3_nosys)
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/alloc"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func withAllocator(ctx context.Context) context.Context {
|
||||
return experimental.WithMemoryAllocator(ctx,
|
||||
experimental.MemoryAllocatorFunc(alloc.Virtual))
|
||||
}
|
||||
|
||||
type mmapState struct {
|
||||
regions []*MappedRegion
|
||||
}
|
||||
|
||||
func (s *mmapState) new(ctx context.Context, mod api.Module, size int32) *MappedRegion {
|
||||
// Find unused region.
|
||||
for _, r := range s.regions {
|
||||
if !r.used && r.size == size {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate page aligned memmory.
|
||||
alloc := mod.ExportedFunction("aligned_alloc")
|
||||
stack := [2]uint64{
|
||||
uint64(unix.Getpagesize()),
|
||||
uint64(size),
|
||||
}
|
||||
if err := alloc.CallWithStack(ctx, stack[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if stack[0] == 0 {
|
||||
panic(OOMErr)
|
||||
}
|
||||
|
||||
// Save the newly allocated region.
|
||||
ptr := uint32(stack[0])
|
||||
buf := View(mod, ptr, uint64(size))
|
||||
addr := uintptr(unsafe.Pointer(&buf[0]))
|
||||
s.regions = append(s.regions, &MappedRegion{
|
||||
Ptr: ptr,
|
||||
addr: addr,
|
||||
size: size,
|
||||
})
|
||||
return s.regions[len(s.regions)-1]
|
||||
}
|
||||
|
||||
type MappedRegion struct {
|
||||
addr uintptr
|
||||
Ptr uint32
|
||||
size int32
|
||||
used bool
|
||||
}
|
||||
|
||||
func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32, prot int) (*MappedRegion, error) {
|
||||
s := ctx.Value(moduleKey{}).(*moduleState)
|
||||
r := s.new(ctx, mod, size)
|
||||
err := r.mmap(f, offset, prot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *MappedRegion) Unmap() error {
|
||||
// We can't munmap the region, otherwise it could be remaped.
|
||||
// Instead, convert it to a protected, private, anonymous mapping.
|
||||
// If successful, it can be reused for a subsequent mmap.
|
||||
_, err := mmap(r.addr, uintptr(r.size),
|
||||
unix.PROT_NONE, unix.MAP_PRIVATE|unix.MAP_ANON|unix.MAP_FIXED,
|
||||
-1, 0)
|
||||
r.used = err != nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *MappedRegion) mmap(f *os.File, offset int64, prot int) error {
|
||||
_, err := mmap(r.addr, uintptr(r.size),
|
||||
prot, unix.MAP_SHARED|unix.MAP_FIXED,
|
||||
int(f.Fd()), offset)
|
||||
r.used = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// We need the low level mmap for MAP_FIXED to work.
|
||||
// Bind the syscall version hoping that it is more stable.
|
||||
|
||||
//go:linkname mmap syscall.mmap
|
||||
func mmap(addr, length uintptr, prot, flag, fd int, pos int64) (*byte, error)
|
||||
22
internal/util/mmap_other.go
Normal file
22
internal/util/mmap_other.go
Normal file
@@ -0,0 +1,22 @@
|
||||
//go:build !unix || !(amd64 || arm64 || riscv64) || sqlite3_noshm || sqlite3_nosys
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/alloc"
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
)
|
||||
|
||||
type mmapState struct{}
|
||||
|
||||
func withAllocator(ctx context.Context) context.Context {
|
||||
return experimental.WithMemoryAllocator(ctx,
|
||||
experimental.MemoryAllocatorFunc(func(cap, max uint64) experimental.LinearMemory {
|
||||
if cap == max {
|
||||
return alloc.Virtual(cap, max)
|
||||
}
|
||||
return alloc.Slice(cap, max)
|
||||
}))
|
||||
}
|
||||
21
internal/util/module.go
Normal file
21
internal/util/module.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
)
|
||||
|
||||
type moduleKey struct{}
|
||||
type moduleState struct {
|
||||
mmapState
|
||||
handleState
|
||||
}
|
||||
|
||||
func NewContext(ctx context.Context) context.Context {
|
||||
state := new(moduleState)
|
||||
ctx = withAllocator(ctx)
|
||||
ctx = experimental.WithCloseNotifier(ctx, state)
|
||||
ctx = context.WithValue(ctx, moduleKey{}, state)
|
||||
return ctx
|
||||
}
|
||||
33
sqlite.go
33
sqlite.go
@@ -15,19 +15,27 @@ import (
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
// Configure SQLite WASM.
|
||||
// Configure SQLite Wasm.
|
||||
//
|
||||
// Importing package embed initializes these
|
||||
// Importing package embed initializes [Binary]
|
||||
// with an appropriate build of SQLite:
|
||||
//
|
||||
// import _ "github.com/ncruces/go-sqlite3/embed"
|
||||
var (
|
||||
Binary []byte // WASM binary to load.
|
||||
Binary []byte // Wasm binary to load.
|
||||
Path string // Path to load the binary from.
|
||||
|
||||
RuntimeConfig wazero.RuntimeConfig
|
||||
)
|
||||
|
||||
// Initialize decodes and compiles the SQLite Wasm binary.
|
||||
// This is called implicitly when the first connection is openned,
|
||||
// but is potentially slow, so you may want to call it at a more convenient time.
|
||||
func Initialize() error {
|
||||
instance.once.Do(compileSQLite)
|
||||
return instance.err
|
||||
}
|
||||
|
||||
var instance struct {
|
||||
runtime wazero.Runtime
|
||||
compiled wazero.CompiledModule
|
||||
@@ -79,16 +87,15 @@ type sqlite struct {
|
||||
}
|
||||
|
||||
func instantiateSQLite() (sqlt *sqlite, err error) {
|
||||
instance.once.Do(compileSQLite)
|
||||
if instance.err != nil {
|
||||
return nil, instance.err
|
||||
if err := Initialize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlt = new(sqlite)
|
||||
sqlt.ctx = util.NewContext(context.Background())
|
||||
|
||||
sqlt.mod, err = instance.runtime.InstantiateModule(sqlt.ctx,
|
||||
instance.compiled, wazero.NewModuleConfig())
|
||||
instance.compiled, wazero.NewModuleConfig().WithName(""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,8 +106,8 @@ func instantiateSQLite() (sqlt *sqlite, err error) {
|
||||
}
|
||||
|
||||
sqlt.freer = util.ReadUint32(sqlt.mod, uint32(global.Get()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if sqlt.freer == 0 {
|
||||
return nil, util.BadBinaryErr
|
||||
}
|
||||
return sqlt, nil
|
||||
}
|
||||
@@ -274,7 +281,7 @@ func (a *arena) new(size uint64) uint32 {
|
||||
}
|
||||
|
||||
func (a *arena) bytes(b []byte) uint32 {
|
||||
if b == nil {
|
||||
if (*[0]byte)(b) == nil {
|
||||
return 0
|
||||
}
|
||||
ptr := a.new(uint64(len(b)))
|
||||
@@ -289,10 +296,14 @@ func (a *arena) string(s string) uint32 {
|
||||
}
|
||||
|
||||
func exportCallbacks(env wazero.HostModuleBuilder) wazero.HostModuleBuilder {
|
||||
util.ExportFuncII(env, "go_progress", progressCallback)
|
||||
util.ExportFuncII(env, "go_progress_handler", progressCallback)
|
||||
util.ExportFuncIIII(env, "go_busy_timeout", timeoutCallback)
|
||||
util.ExportFuncIII(env, "go_busy_handler", busyCallback)
|
||||
util.ExportFuncII(env, "go_commit_hook", commitCallback)
|
||||
util.ExportFuncVI(env, "go_rollback_hook", rollbackCallback)
|
||||
util.ExportFuncVIIIIJ(env, "go_update_hook", updateCallback)
|
||||
util.ExportFuncIIIII(env, "go_wal_hook", walCallback)
|
||||
util.ExportFuncIIIIII(env, "go_autovacuum_pages", autoVacuumCallback)
|
||||
util.ExportFuncIIIIIII(env, "go_authorizer", authorizerCallback)
|
||||
util.ExportFuncVIII(env, "go_log", logCallback)
|
||||
util.ExportFuncVI(env, "go_destroy", destroyCallback)
|
||||
|
||||
13
sqlite3/busy_timeout.patch
Normal file
13
sqlite3/busy_timeout.patch
Normal file
@@ -0,0 +1,13 @@
|
||||
# Replace sqliteDefaultBusyCallback.
|
||||
# This patch allows Go to handle (and interrupt) sqlite3_busy_timeout.
|
||||
--- sqlite3.c.orig
|
||||
+++ sqlite3.c
|
||||
@@ -181614,7 +181614,7 @@
|
||||
if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;
|
||||
#endif
|
||||
if( ms>0 ){
|
||||
- sqlite3_busy_handler(db, (int(*)(void*,int))sqliteDefaultBusyCallback,
|
||||
+ sqlite3_busy_handler(db, (int(*)(void*,int))sqliteBusyCallback,
|
||||
(void*)db);
|
||||
db->busyTimeout = ms;
|
||||
}else{
|
||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
curl -#OL "https://sqlite.org/2024/sqlite-amalgamation-3450000.zip"
|
||||
curl -#OL "https://sqlite.org/2024/sqlite-amalgamation-3460000.zip"
|
||||
unzip -d . sqlite-amalgamation-*.zip
|
||||
mv sqlite-amalgamation-*/sqlite3* .
|
||||
rm -rf sqlite-amalgamation-*
|
||||
@@ -12,25 +12,25 @@ cat *.patch | patch --no-backup-if-mismatch
|
||||
|
||||
mkdir -p ext/
|
||||
cd ext/
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/ext/misc/anycollseq.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/ext/misc/base64.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/ext/misc/decimal.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/ext/misc/ieee754.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/ext/misc/regexp.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/ext/misc/series.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/ext/misc/uint.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/ext/misc/uuid.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/anycollseq.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/base64.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/decimal.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/ieee754.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/regexp.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/series.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/uint.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/ext/misc/uuid.c"
|
||||
cd ~-
|
||||
|
||||
cd ../vfs/tests/mptest/testdata/
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/mptest/mptest.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/mptest/config01.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/mptest/config02.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/mptest/crash01.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/mptest/crash02.subtest"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/mptest/multiwrite01.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/mptest.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/config01.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/config02.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/crash01.test"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/crash02.subtest"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/mptest/multiwrite01.test"
|
||||
cd ~-
|
||||
|
||||
cd ../vfs/tests/speedtest1/testdata/
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.45.0/test/speedtest1.c"
|
||||
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.46.0/test/speedtest1.c"
|
||||
cd ~-
|
||||
@@ -2,19 +2,29 @@
|
||||
|
||||
#include "sqlite3.h"
|
||||
|
||||
int go_progress(void *);
|
||||
int go_progress_handler(void *);
|
||||
int go_busy_handler(void *, int);
|
||||
int go_busy_timeout(void *, int count, int tmout);
|
||||
|
||||
int go_commit_hook(void *);
|
||||
void go_rollback_hook(void *);
|
||||
void go_update_hook(void *, int, char const *, char const *, sqlite3_int64);
|
||||
int go_wal_hook(void *, sqlite3 *, const char *, int);
|
||||
|
||||
int go_authorizer(void *, int, const char *, const char *, const char *,
|
||||
const char *);
|
||||
|
||||
void go_log(void *, int, const char *);
|
||||
|
||||
unsigned int go_autovacuum_pages(void *, const char *, unsigned int,
|
||||
unsigned int, unsigned int);
|
||||
|
||||
void sqlite3_progress_handler_go(sqlite3 *db, int n) {
|
||||
sqlite3_progress_handler(db, n, go_progress, /*arg=*/db);
|
||||
sqlite3_progress_handler(db, n, go_progress_handler, /*arg=*/db);
|
||||
}
|
||||
|
||||
int sqlite3_busy_handler_go(sqlite3 *db, bool enable) {
|
||||
return sqlite3_busy_handler(db, enable ? go_busy_handler : NULL, /*arg=*/db);
|
||||
}
|
||||
|
||||
void sqlite3_commit_hook_go(sqlite3 *db, bool enable) {
|
||||
@@ -29,6 +39,10 @@ void sqlite3_update_hook_go(sqlite3 *db, bool enable) {
|
||||
sqlite3_update_hook(db, enable ? go_update_hook : NULL, /*arg=*/db);
|
||||
}
|
||||
|
||||
void sqlite3_wal_hook_go(sqlite3 *db, bool enable) {
|
||||
sqlite3_wal_hook(db, enable ? go_wal_hook : NULL, /*arg=*/NULL);
|
||||
}
|
||||
|
||||
int sqlite3_set_authorizer_go(sqlite3 *db, bool enable) {
|
||||
return sqlite3_set_authorizer(db, enable ? go_authorizer : NULL, /*arg=*/db);
|
||||
}
|
||||
@@ -36,4 +50,18 @@ int sqlite3_set_authorizer_go(sqlite3 *db, bool enable) {
|
||||
int sqlite3_config_log_go(bool enable) {
|
||||
return sqlite3_config(SQLITE_CONFIG_LOG, enable ? go_log : NULL,
|
||||
/*arg=*/NULL);
|
||||
}
|
||||
}
|
||||
|
||||
int sqlite3_autovacuum_pages_go(sqlite3 *db, go_handle app) {
|
||||
int rc = sqlite3_autovacuum_pages(db, go_autovacuum_pages, app, go_destroy);
|
||||
if (rc) go_destroy(app);
|
||||
return rc;
|
||||
}
|
||||
|
||||
#ifndef sqliteBusyCallback
|
||||
|
||||
static int sqliteBusyCallback(sqlite3 *db, int count) {
|
||||
return go_busy_timeout(db, count, db->busyTimeout);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
// https://github.com/JuliaLang/julia/blob/v1.9.4/src/julia.h#L67-L68
|
||||
#define container_of(ptr, type, member) \
|
||||
((type *)((char *)(ptr)-offsetof(type, member)))
|
||||
((type *)((char *)(ptr) - offsetof(type, member)))
|
||||
|
||||
typedef void *go_handle;
|
||||
void go_destroy(go_handle);
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
# ISO week date specifiers, backport from 3.46.
|
||||
--- sqlite3.c.orig
|
||||
+++ sqlite3.c
|
||||
@@ -25340,21 +25340,82 @@
|
||||
}
|
||||
|
||||
/*
|
||||
+** Compute the number of days after the most recent January 1.
|
||||
+**
|
||||
+** In other words, compute the zero-based day number for the
|
||||
+** current year:
|
||||
+**
|
||||
+** Jan01 = 0, Jan02 = 1, ..., Jan31 = 30, Feb01 = 31, ...
|
||||
+** Dec31 = 364 or 365.
|
||||
+*/
|
||||
+static int daysAfterJan01(DateTime *pDate){
|
||||
+ DateTime jan01 = *pDate;
|
||||
+ assert( jan01.validYMD );
|
||||
+ assert( jan01.validHMS );
|
||||
+ assert( pDate->validJD );
|
||||
+ jan01.validJD = 0;
|
||||
+ jan01.M = 1;
|
||||
+ jan01.D = 1;
|
||||
+ computeJD(&jan01);
|
||||
+ return (int)((pDate->iJD-jan01.iJD+43200000)/86400000);
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+** Return the number of days after the most recent Monday.
|
||||
+**
|
||||
+** In other words, return the day of the week according
|
||||
+** to this code:
|
||||
+**
|
||||
+** 0=Monday, 1=Tuesday, 2=Wednesday, ..., 6=Sunday.
|
||||
+*/
|
||||
+static int daysAfterMonday(DateTime *pDate){
|
||||
+ assert( pDate->validJD );
|
||||
+ return (int)((pDate->iJD+43200000)/86400000) % 7;
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+** Return the number of days after the most recent Sunday.
|
||||
+**
|
||||
+** In other words, return the day of the week according
|
||||
+** to this code:
|
||||
+**
|
||||
+** 0=Sunday, 1=Monday, 2=Tues, ..., 6=Saturday
|
||||
+*/
|
||||
+static int daysAfterSunday(DateTime *pDate){
|
||||
+ assert( pDate->validJD );
|
||||
+ return (int)((pDate->iJD+129600000)/86400000) % 7;
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
** strftime( FORMAT, TIMESTRING, MOD, MOD, ...)
|
||||
**
|
||||
** Return a string described by FORMAT. Conversions as follows:
|
||||
**
|
||||
-** %d day of month
|
||||
+** %d day of month 01-31
|
||||
+** %e day of month 1-31
|
||||
** %f ** fractional seconds SS.SSS
|
||||
+** %F ISO date. YYYY-MM-DD
|
||||
+** %G ISO year corresponding to %V 0000-9999.
|
||||
+** %g 2-digit ISO year corresponding to %V 00-99
|
||||
** %H hour 00-24
|
||||
-** %j day of year 000-366
|
||||
+** %k hour 0-24 (leading zero converted to space)
|
||||
+** %I hour 01-12
|
||||
+** %j day of year 001-366
|
||||
** %J ** julian day number
|
||||
+** %l hour 1-12 (leading zero converted to space)
|
||||
** %m month 01-12
|
||||
** %M minute 00-59
|
||||
+** %p "am" or "pm"
|
||||
+** %P "AM" or "PM"
|
||||
+** %R time as HH:MM
|
||||
** %s seconds since 1970-01-01
|
||||
** %S seconds 00-59
|
||||
-** %w day of week 0-6 Sunday==0
|
||||
-** %W week of year 00-53
|
||||
+** %T time as HH:MM:SS
|
||||
+** %u day of week 1-7 Monday==1, Sunday==7
|
||||
+** %w day of week 0-6 Sunday==0, Monday==1
|
||||
+** %U week of year 00-53 (First Sunday is start of week 01)
|
||||
+** %V week of year 01-53 (First week containing Thursday is week 01)
|
||||
+** %W week of year 00-53 (First Monday is start of week 01)
|
||||
** %Y year 0000-9999
|
||||
** %% %
|
||||
*/
|
||||
@@ -25391,7 +25452,7 @@
|
||||
sqlite3_str_appendf(&sRes, cf=='d' ? "%02d" : "%2d", x.D);
|
||||
break;
|
||||
}
|
||||
- case 'f': {
|
||||
+ case 'f': { /* Fractional seconds. (Non-standard) */
|
||||
double s = x.s;
|
||||
if( s>59.999 ) s = 59.999;
|
||||
sqlite3_str_appendf(&sRes, "%06.3f", s);
|
||||
@@ -25401,6 +25462,21 @@
|
||||
sqlite3_str_appendf(&sRes, "%04d-%02d-%02d", x.Y, x.M, x.D);
|
||||
break;
|
||||
}
|
||||
+ case 'G': /* Fall thru */
|
||||
+ case 'g': {
|
||||
+ DateTime y = x;
|
||||
+ assert( y.validJD );
|
||||
+ /* Move y so that it is the Thursday in the same week as x */
|
||||
+ y.iJD += (3 - daysAfterMonday(&x))*86400000;
|
||||
+ y.validYMD = 0;
|
||||
+ computeYMD(&y);
|
||||
+ if( cf=='g' ){
|
||||
+ sqlite3_str_appendf(&sRes, "%02d", y.Y%100);
|
||||
+ }else{
|
||||
+ sqlite3_str_appendf(&sRes, "%04d", y.Y);
|
||||
+ }
|
||||
+ break;
|
||||
+ }
|
||||
case 'H':
|
||||
case 'k': {
|
||||
sqlite3_str_appendf(&sRes, cf=='H' ? "%02d" : "%2d", x.h);
|
||||
@@ -25414,25 +25490,11 @@
|
||||
sqlite3_str_appendf(&sRes, cf=='I' ? "%02d" : "%2d", h);
|
||||
break;
|
||||
}
|
||||
- case 'W': /* Fall thru */
|
||||
- case 'j': {
|
||||
- int nDay; /* Number of days since 1st day of year */
|
||||
- DateTime y = x;
|
||||
- y.validJD = 0;
|
||||
- y.M = 1;
|
||||
- y.D = 1;
|
||||
- computeJD(&y);
|
||||
- nDay = (int)((x.iJD-y.iJD+43200000)/86400000);
|
||||
- if( cf=='W' ){
|
||||
- int wd; /* 0=Monday, 1=Tuesday, ... 6=Sunday */
|
||||
- wd = (int)(((x.iJD+43200000)/86400000)%7);
|
||||
- sqlite3_str_appendf(&sRes,"%02d",(nDay+7-wd)/7);
|
||||
- }else{
|
||||
- sqlite3_str_appendf(&sRes,"%03d",nDay+1);
|
||||
- }
|
||||
+ case 'j': { /* Day of year. Jan01==1, Jan02==2, and so forth */
|
||||
+ sqlite3_str_appendf(&sRes,"%03d",daysAfterJan01(&x)+1);
|
||||
break;
|
||||
}
|
||||
- case 'J': {
|
||||
+ case 'J': { /* Julian day number. (Non-standard) */
|
||||
sqlite3_str_appendf(&sRes,"%.16g",x.iJD/86400000.0);
|
||||
break;
|
||||
}
|
||||
@@ -25475,11 +25537,31 @@
|
||||
sqlite3_str_appendf(&sRes,"%02d:%02d:%02d", x.h, x.m, (int)x.s);
|
||||
break;
|
||||
}
|
||||
- case 'u': /* Fall thru */
|
||||
- case 'w': {
|
||||
- char c = (char)(((x.iJD+129600000)/86400000) % 7) + '0';
|
||||
+ case 'u': /* Day of week. 1 to 7. Monday==1, Sunday==7 */
|
||||
+ case 'w': { /* Day of week. 0 to 6. Sunday==0, Monday==1 */
|
||||
+ char c = (char)daysAfterSunday(&x) + '0';
|
||||
if( c=='0' && cf=='u' ) c = '7';
|
||||
sqlite3_str_appendchar(&sRes, 1, c);
|
||||
+ break;
|
||||
+ }
|
||||
+ case 'U': { /* Week num. 00-53. First Sun of the year is week 01 */
|
||||
+ sqlite3_str_appendf(&sRes,"%02d",
|
||||
+ (daysAfterJan01(&x)-daysAfterSunday(&x)+7)/7);
|
||||
+ break;
|
||||
+ }
|
||||
+ case 'V': { /* Week num. 01-53. First week with a Thur is week 01 */
|
||||
+ DateTime y = x;
|
||||
+ /* Adjust y so that is the Thursday in the same week as x */
|
||||
+ assert( y.validJD );
|
||||
+ y.iJD += (3 - daysAfterMonday(&x))*86400000;
|
||||
+ y.validYMD = 0;
|
||||
+ computeYMD(&y);
|
||||
+ sqlite3_str_appendf(&sRes,"%02d", daysAfterJan01(&y)/7+1);
|
||||
+ break;
|
||||
+ }
|
||||
+ case 'W': { /* Week num. 00-53. First Mon of the year is week 01 */
|
||||
+ sqlite3_str_appendf(&sRes,"%02d",
|
||||
+ (daysAfterJan01(&x)-daysAfterMonday(&x)+7)/7);
|
||||
break;
|
||||
}
|
||||
case 'Y': {
|
||||
@@ -1,14 +0,0 @@
|
||||
# Use exclusive locking mode for WAL databases with v1 VFSes.
|
||||
--- sqlite3.c.orig
|
||||
+++ sqlite3.c
|
||||
@@ -64209,7 +64209,9 @@
|
||||
SQLITE_PRIVATE int sqlite3PagerWalSupported(Pager *pPager){
|
||||
const sqlite3_io_methods *pMethods = pPager->fd->pMethods;
|
||||
if( pPager->noLock ) return 0;
|
||||
- return pPager->exclusiveMode || (pMethods->iVersion>=2 && pMethods->xShmMap);
|
||||
+ if( pMethods->iVersion>=2 && pMethods->xShmMap ) return 1;
|
||||
+ pPager->exclusiveMode = 1;
|
||||
+ return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -9,17 +9,23 @@
|
||||
#define HAVE_INT16_T 1
|
||||
#define HAVE_INT32_T 1
|
||||
#define HAVE_INT64_T 1
|
||||
#define HAVE_INTPTR_T 1
|
||||
#define HAVE_UINT8_T 1
|
||||
#define HAVE_UINT16_T 1
|
||||
#define HAVE_UINT32_T 1
|
||||
#define HAVE_UINT64_T 1
|
||||
#define HAVE_UINTPTR_T 1
|
||||
#define HAVE_STDINT_H 1
|
||||
#define HAVE_INTTYPES_H 1
|
||||
|
||||
#define LONGDOUBLE_TYPE double
|
||||
|
||||
#define HAVE_LOG2 1
|
||||
#define HAVE_LOG10 1
|
||||
#define HAVE_ISNAN 1
|
||||
|
||||
#define HAVE_STRCHRNUL 1
|
||||
|
||||
#define HAVE_USLEEP 1
|
||||
#define HAVE_NANOSLEEP 1
|
||||
|
||||
@@ -29,57 +35,14 @@
|
||||
#define HAVE_MALLOC_H 1
|
||||
#define HAVE_MALLOC_USABLE_SIZE 1
|
||||
|
||||
// Recommended Options
|
||||
|
||||
#define SQLITE_DQS 0
|
||||
#define SQLITE_THREADSAFE 0
|
||||
#define SQLITE_DEFAULT_MEMSTATUS 0
|
||||
#define SQLITE_DEFAULT_WAL_SYNCHRONOUS 1
|
||||
#define SQLITE_LIKE_DOESNT_MATCH_BLOBS
|
||||
#define SQLITE_MAX_EXPR_DEPTH 0
|
||||
#define SQLITE_USE_ALLOCA
|
||||
#define SQLITE_OMIT_DEPRECATED
|
||||
#define SQLITE_OMIT_SHARED_CACHE
|
||||
#define SQLITE_OMIT_AUTOINIT
|
||||
// #define SQLITE_OMIT_DECLTYPE
|
||||
// #define SQLITE_OMIT_PROGRESS_CALLBACK
|
||||
|
||||
// Other Options
|
||||
|
||||
#define SQLITE_ALLOW_URI_AUTHORITY
|
||||
#define SQLITE_TRUSTED_SCHEMA 0
|
||||
#define SQLITE_DEFAULT_FOREIGN_KEYS 1
|
||||
#define SQLITE_ENABLE_ATOMIC_WRITE
|
||||
#define SQLITE_ENABLE_BATCH_ATOMIC_WRITE
|
||||
|
||||
// Because WASM does not support shared memory,
|
||||
// SQLite disables WAL for WASM builds.
|
||||
// We patch SQLite to use exclusive locking mode instead.
|
||||
// https://sqlite.org/wal.html#noshm
|
||||
// Because Wasm does not support shared memory,
|
||||
// SQLite disables WAL for Wasm builds.
|
||||
#undef SQLITE_OMIT_WAL
|
||||
|
||||
// We have our own memdb VFS.
|
||||
// To avoid interactions between the two,
|
||||
// omit sqlite3_serialize/sqlite3_deserialize,
|
||||
// which we also don't wrap.
|
||||
#define SQLITE_OMIT_DESERIALIZE
|
||||
|
||||
// Amalgamated Extensions
|
||||
|
||||
#define SQLITE_ENABLE_MATH_FUNCTIONS 1
|
||||
#define SQLITE_ENABLE_JSON1 1
|
||||
#define SQLITE_ENABLE_FTS3 1
|
||||
#define SQLITE_ENABLE_FTS3_PARENTHESIS 1
|
||||
#define SQLITE_ENABLE_FTS4 1
|
||||
#define SQLITE_ENABLE_FTS5 1
|
||||
#define SQLITE_ENABLE_RTREE 1
|
||||
#define SQLITE_ENABLE_GEOPOLY 1
|
||||
|
||||
#define SQLITE_SOUNDEX
|
||||
|
||||
// Session Extension
|
||||
// #define SQLITE_ENABLE_SESSION
|
||||
// #define SQLITE_ENABLE_PREUPDATE_HOOK
|
||||
|
||||
// Implemented in vfs.c.
|
||||
int localtime_s(struct tm *const pTm, time_t const *const pTime);
|
||||
int localtime_s(struct tm *const pTm, time_t const *const pTime);
|
||||
|
||||
// Implemented in hooks.c.
|
||||
#ifndef sqliteBusyCallback
|
||||
static int sqliteBusyCallback(sqlite3 *, int);
|
||||
#endif
|
||||
44
sqlite3/sqlite_opt.h
Normal file
44
sqlite3/sqlite_opt.h
Normal file
@@ -0,0 +1,44 @@
|
||||
// Recommended Options
|
||||
|
||||
#define SQLITE_DQS 0
|
||||
#define SQLITE_THREADSAFE 0
|
||||
#define SQLITE_DEFAULT_MEMSTATUS 0
|
||||
#define SQLITE_DEFAULT_WAL_SYNCHRONOUS 1
|
||||
#define SQLITE_LIKE_DOESNT_MATCH_BLOBS
|
||||
#define SQLITE_MAX_EXPR_DEPTH 0
|
||||
#define SQLITE_STRICT_SUBTYPE 1
|
||||
#define SQLITE_USE_ALLOCA
|
||||
#define SQLITE_OMIT_DEPRECATED
|
||||
#define SQLITE_OMIT_SHARED_CACHE
|
||||
#define SQLITE_OMIT_AUTOINIT
|
||||
|
||||
// We need these:
|
||||
// #define SQLITE_OMIT_DECLTYPE
|
||||
// #define SQLITE_OMIT_PROGRESS_CALLBACK
|
||||
|
||||
// Other Options
|
||||
|
||||
#define SQLITE_ALLOW_URI_AUTHORITY
|
||||
#define SQLITE_TRUSTED_SCHEMA 0
|
||||
#define SQLITE_DEFAULT_FOREIGN_KEYS 1
|
||||
#define SQLITE_ENABLE_ATOMIC_WRITE
|
||||
#define SQLITE_ENABLE_BATCH_ATOMIC_WRITE
|
||||
#define SQLITE_ENABLE_COLUMN_METADATA
|
||||
#define SQLITE_ENABLE_STAT4 1
|
||||
|
||||
// We have our own memdb VFS.
|
||||
// To avoid interactions between the two,
|
||||
// omit sqlite3_serialize/sqlite3_deserialize,
|
||||
// which we also don't wrap.
|
||||
#define SQLITE_OMIT_DESERIALIZE
|
||||
|
||||
// Amalgamated Extensions
|
||||
|
||||
#define SQLITE_ENABLE_MATH_FUNCTIONS 1
|
||||
#define SQLITE_ENABLE_JSON1 1
|
||||
#define SQLITE_ENABLE_FTS5 1
|
||||
#define SQLITE_ENABLE_RTREE 1
|
||||
#define SQLITE_ENABLE_GEOPOLY 1
|
||||
|
||||
#define SQLITE_SOUNDEX
|
||||
#define SQLITE_UNTESTABLE
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
static int time_collation(void *pArg, int nKey1, const void *pKey1, int nKey2,
|
||||
const void *pKey2) {
|
||||
UNUSED_PARAMETER(pArg);
|
||||
|
||||
// Remove a Z suffix if one key is no longer than the other.
|
||||
// A Z suffix collates before any character but after the empty string.
|
||||
// This avoids making different keys equal.
|
||||
@@ -27,63 +29,10 @@ static int time_collation(void *pArg, int nKey1, const void *pKey1, int nKey2,
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void json_time_func(sqlite3_context *context, int argc,
|
||||
sqlite3_value **argv) {
|
||||
DateTime x;
|
||||
if (isDate(context, argc, argv, &x)) return;
|
||||
if (x.tzSet && x.tz) {
|
||||
x.iJD += x.tz * 60000;
|
||||
if (!validJulianDay(x.iJD)) return;
|
||||
x.validYMD = 0;
|
||||
x.validHMS = 0;
|
||||
}
|
||||
computeYMD_HMS(&x);
|
||||
|
||||
sqlite3 *db = sqlite3_context_db_handle(context);
|
||||
sqlite3_str *res = sqlite3_str_new(db);
|
||||
|
||||
sqlite3_str_appendf(res, "%04d-%02d-%02dT%02d:%02d:%02d", //
|
||||
x.Y, x.M, x.D, //
|
||||
x.h, x.m, (int)(x.iJD / 1000 % 60));
|
||||
|
||||
if (x.useSubsec) {
|
||||
int rem = x.iJD % 1000;
|
||||
if (rem) {
|
||||
sqlite3_str_appendchar(res, 1, '.');
|
||||
sqlite3_str_appendchar(res, 1, '0' + rem / 100);
|
||||
if ((rem %= 100)) {
|
||||
sqlite3_str_appendchar(res, 1, '0' + rem / 10);
|
||||
if ((rem %= 10)) {
|
||||
sqlite3_str_appendchar(res, 1, '0' + rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (x.tz) {
|
||||
sqlite3_str_appendf(res, "%+03d:%02d", x.tz / 60, abs(x.tz) % 60);
|
||||
} else {
|
||||
sqlite3_str_appendchar(res, 1, 'Z');
|
||||
}
|
||||
|
||||
int rc = sqlite3_str_errcode(res);
|
||||
if (rc) {
|
||||
sqlite3_result_error_code(context, rc);
|
||||
return;
|
||||
}
|
||||
|
||||
int n = sqlite3_str_length(res);
|
||||
sqlite3_result_text(context, sqlite3_str_finish(res), n, sqlite3_free);
|
||||
}
|
||||
|
||||
int sqlite3_time_init(sqlite3 *db, char **pzErrMsg,
|
||||
const sqlite3_api_routines *pApi) {
|
||||
UNUSED_PARAMETER2(pzErrMsg, pApi);
|
||||
sqlite3_create_collation_v2(db, "time", SQLITE_UTF8, /*arg=*/NULL,
|
||||
time_collation,
|
||||
/*destroy=*/NULL);
|
||||
sqlite3_create_function_v2(
|
||||
db, "json_time", /*nArg=*/-1,
|
||||
SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS, /*arg=*/NULL,
|
||||
json_time_func, /*step=*/NULL, /*final=*/NULL, /*destroy=*/NULL);
|
||||
time_collation, /*destroy=*/NULL);
|
||||
return SQLITE_OK;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
# Set UTC timezone, compute local offset.
|
||||
--- sqlite3.c.orig
|
||||
+++ sqlite3.c
|
||||
@@ -340,6 +340,7 @@ static int setDateTimeToCurrent(sqlite3_context *context, DateTime *p){
|
||||
p->iJD = sqlite3StmtCurrentTime(context);
|
||||
if( p->iJD>0 ){
|
||||
p->validJD = 1;
|
||||
+ p->tzSet = 1;
|
||||
return 0;
|
||||
}else{
|
||||
return 1;
|
||||
@@ -355,6 +356,7 @@ static int setDateTimeToCurrent(sqlite3_context *context, DateTime *p){
|
||||
static void setRawDateNumber(DateTime *p, double r){
|
||||
p->s = r;
|
||||
p->rawS = 1;
|
||||
+ p->tzSet = 1;
|
||||
if( r>=0.0 && r<5373484.5 ){
|
||||
p->iJD = (sqlite3_int64)(r*86400000.0 + 0.5);
|
||||
p->validJD = 1;
|
||||
@@ -731,7 +733,16 @@ static int parseModifier(
|
||||
** show local time.
|
||||
*/
|
||||
if( sqlite3_stricmp(z, "localtime")==0 && sqlite3NotPureFunc(pCtx) ){
|
||||
- rc = toLocaltime(p, pCtx);
|
||||
+ if( p->tzSet!=0 || p->tz==0 ) {
|
||||
+ rc = toLocaltime(p, pCtx);
|
||||
+ i64 iOrigJD = p->iJD;
|
||||
+ p->tzSet = 0;
|
||||
+ computeJD(p);
|
||||
+ p->tz = (p->iJD-iOrigJD)/60000;
|
||||
+ if( abs(p->tz)>= 900 ) p->tz = 0;
|
||||
+ } else {
|
||||
+ rc = 0;
|
||||
+ }
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -781,6 +792,7 @@ static int parseModifier(
|
||||
p->validJD = 1;
|
||||
p->tzSet = 1;
|
||||
}
|
||||
+ p->tz = 0;
|
||||
rc = SQLITE_OK;
|
||||
}
|
||||
#endif
|
||||
@@ -5,15 +5,15 @@
|
||||
#include "include.h"
|
||||
#include "sqlite3.h"
|
||||
|
||||
int go_localtime(struct tm *, sqlite3_int64);
|
||||
int go_vfs_find(const char *zVfsName);
|
||||
int go_localtime(struct tm *, sqlite3_int64);
|
||||
|
||||
int go_randomness(sqlite3_vfs *, int nByte, char *zOut);
|
||||
int go_sleep(sqlite3_vfs *, int microseconds);
|
||||
int go_current_time_64(sqlite3_vfs *, sqlite3_int64 *);
|
||||
|
||||
int go_open(sqlite3_vfs *, sqlite3_filename zName, sqlite3_file *, int flags,
|
||||
int *pOutFlags);
|
||||
int *pOutFlags, int *pOutVFS);
|
||||
int go_delete(sqlite3_vfs *, const char *zName, int syncDir);
|
||||
int go_access(sqlite3_vfs *, const char *zName, int flags, int *pResOut);
|
||||
int go_full_pathname(sqlite3_vfs *, const char *zName, int nOut, char *zOut);
|
||||
@@ -32,29 +32,55 @@ int go_lock(sqlite3_file *, int eLock);
|
||||
int go_unlock(sqlite3_file *, int eLock);
|
||||
int go_check_reserved_lock(sqlite3_file *, int *pResOut);
|
||||
|
||||
int go_shm_map(sqlite3_file *, int iPg, int pgsz, int, void volatile **);
|
||||
int go_shm_lock(sqlite3_file *, int offset, int n, int flags);
|
||||
int go_shm_unmap(sqlite3_file *, int deleteFlag);
|
||||
void go_shm_barrier(sqlite3_file *);
|
||||
|
||||
static int go_open_wrapper(sqlite3_vfs *vfs, sqlite3_filename zName,
|
||||
sqlite3_file *file, int flags, int *pOutFlags) {
|
||||
static const sqlite3_io_methods os_io = {
|
||||
.iVersion = 1,
|
||||
.xClose = go_close,
|
||||
.xRead = go_read,
|
||||
.xWrite = go_write,
|
||||
.xTruncate = go_truncate,
|
||||
.xSync = go_sync,
|
||||
.xFileSize = go_file_size,
|
||||
.xLock = go_lock,
|
||||
.xUnlock = go_unlock,
|
||||
.xCheckReservedLock = go_check_reserved_lock,
|
||||
.xFileControl = go_file_control,
|
||||
.xSectorSize = go_sector_size,
|
||||
.xDeviceCharacteristics = go_device_characteristics,
|
||||
};
|
||||
static const sqlite3_io_methods go_io[2] = {
|
||||
{
|
||||
.iVersion = 1,
|
||||
.xClose = go_close,
|
||||
.xRead = go_read,
|
||||
.xWrite = go_write,
|
||||
.xTruncate = go_truncate,
|
||||
.xSync = go_sync,
|
||||
.xFileSize = go_file_size,
|
||||
.xLock = go_lock,
|
||||
.xUnlock = go_unlock,
|
||||
.xCheckReservedLock = go_check_reserved_lock,
|
||||
.xFileControl = go_file_control,
|
||||
.xSectorSize = go_sector_size,
|
||||
.xDeviceCharacteristics = go_device_characteristics,
|
||||
},
|
||||
{
|
||||
.iVersion = 2,
|
||||
.xClose = go_close,
|
||||
.xRead = go_read,
|
||||
.xWrite = go_write,
|
||||
.xTruncate = go_truncate,
|
||||
.xSync = go_sync,
|
||||
.xFileSize = go_file_size,
|
||||
.xLock = go_lock,
|
||||
.xUnlock = go_unlock,
|
||||
.xCheckReservedLock = go_check_reserved_lock,
|
||||
.xFileControl = go_file_control,
|
||||
.xSectorSize = go_sector_size,
|
||||
.xDeviceCharacteristics = go_device_characteristics,
|
||||
.xShmMap = go_shm_map,
|
||||
.xShmLock = go_shm_lock,
|
||||
.xShmBarrier = go_shm_barrier,
|
||||
.xShmUnmap = go_shm_unmap,
|
||||
}};
|
||||
int vfsID = 0;
|
||||
memset(file, 0, vfs->szOsFile);
|
||||
int rc = go_open(vfs, zName, file, flags, pOutFlags);
|
||||
int rc = go_open(vfs, zName, file, flags, pOutFlags, &vfsID);
|
||||
if (rc) {
|
||||
return rc;
|
||||
}
|
||||
file->pMethods = &os_io;
|
||||
file->pMethods = &go_io[vfsID];
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Wrap sqlite3_vfs_find.
|
||||
# This patch allows Go VFSes to be (un)registered.
|
||||
--- sqlite3.c.orig
|
||||
+++ sqlite3.c
|
||||
@@ -26089,7 +26089,7 @@
|
||||
@@ -26396,7 +26396,7 @@
|
||||
** Locate a VFS by name. If no name is given, simply return the
|
||||
** first VFS on the list.
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#define SQLITE_VTAB_RENAMER_GO /******/ 0x08
|
||||
#define SQLITE_VTAB_OVERLOADER_GO /***/ 0x10
|
||||
#define SQLITE_VTAB_CHECKER_GO /******/ 0x20
|
||||
#define SQLITE_VTAB_TX_GO /***********/ 0x40
|
||||
#define SQLITE_VTAB_TXN_GO /**********/ 0x40
|
||||
#define SQLITE_VTAB_SAVEPOINTER_GO /**/ 0x80
|
||||
|
||||
int go_vtab_create(sqlite3_module *, int argc, const char *const *argv,
|
||||
@@ -72,6 +72,8 @@ static void go_mod_destroy(void *pAux) {
|
||||
static int go_vtab_create_wrapper(sqlite3 *db, void *pAux, int argc,
|
||||
const char *const *argv,
|
||||
sqlite3_vtab **ppVTab, char **pzErr) {
|
||||
UNUSED_PARAMETER(db);
|
||||
|
||||
struct go_vtab *vtab = calloc(1, sizeof(struct go_vtab));
|
||||
if (vtab == NULL) return SQLITE_NOMEM;
|
||||
*ppVTab = &vtab->base;
|
||||
@@ -88,6 +90,8 @@ static int go_vtab_create_wrapper(sqlite3 *db, void *pAux, int argc,
|
||||
static int go_vtab_connect_wrapper(sqlite3 *db, void *pAux, int argc,
|
||||
const char *const *argv,
|
||||
sqlite3_vtab **ppVTab, char **pzErr) {
|
||||
UNUSED_PARAMETER(db);
|
||||
|
||||
struct go_vtab *vtab = calloc(1, sizeof(struct go_vtab));
|
||||
if (vtab == NULL) return SQLITE_NOMEM;
|
||||
*ppVTab = &vtab->base;
|
||||
@@ -193,7 +197,7 @@ int sqlite3_create_module_go(sqlite3 *db, const char *zName, int flags,
|
||||
if (flags & SQLITE_VTAB_CHECKER_GO) {
|
||||
mod->base.xIntegrity = go_vtab_integrity_wrapper;
|
||||
}
|
||||
if (flags & SQLITE_VTAB_TX_GO) {
|
||||
if (flags & SQLITE_VTAB_TXN_GO) {
|
||||
mod->base.xBegin = go_vtab_begin;
|
||||
mod->base.xSync = go_vtab_sync;
|
||||
mod->base.xCommit = go_vtab_commit;
|
||||
@@ -218,6 +222,7 @@ int sqlite3_vtab_config_go(sqlite3 *db, int op, int constraint) {
|
||||
return sqlite3_vtab_config(db, op, constraint);
|
||||
}
|
||||
|
||||
static_assert(offsetof(struct sqlite3_vtab, zErrMsg) == 8, "Unexpected offset");
|
||||
static_assert(offsetof(struct go_module, base) == 4, "Unexpected offset");
|
||||
static_assert(offsetof(struct go_vtab, base) == 4, "Unexpected offset");
|
||||
static_assert(offsetof(struct go_cursor, base) == 4, "Unexpected offset");
|
||||
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/tetratelabs/wazero"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Path = "./embed/sqlite3.wasm"
|
||||
RuntimeConfig = wazero.NewRuntimeConfig().WithMemoryLimitPages(1024)
|
||||
}
|
||||
|
||||
func Test_sqlite_error_OOM(t *testing.T) {
|
||||
|
||||
68
stmt.go
68
stmt.go
@@ -120,7 +120,7 @@ func (s *Stmt) Status(op StmtStatus, reset bool) int {
|
||||
}
|
||||
r := s.c.call("sqlite3_stmt_status", uint64(s.handle),
|
||||
uint64(op), i)
|
||||
return int(r)
|
||||
return int(int32(r))
|
||||
}
|
||||
|
||||
// ClearBindings resets all bindings on the prepared statement.
|
||||
@@ -137,7 +137,7 @@ func (s *Stmt) ClearBindings() error {
|
||||
func (s *Stmt) BindCount() int {
|
||||
r := s.c.call("sqlite3_bind_parameter_count",
|
||||
uint64(s.handle))
|
||||
return int(r)
|
||||
return int(int32(r))
|
||||
}
|
||||
|
||||
// BindIndex returns the index of a parameter in the prepared statement
|
||||
@@ -149,7 +149,7 @@ func (s *Stmt) BindIndex(name string) int {
|
||||
namePtr := s.c.arena.string(name)
|
||||
r := s.c.call("sqlite3_bind_parameter_index",
|
||||
uint64(s.handle), uint64(namePtr))
|
||||
return int(r)
|
||||
return int(int32(r))
|
||||
}
|
||||
|
||||
// BindName returns the name of a parameter in the prepared statement.
|
||||
@@ -357,7 +357,7 @@ func (s *Stmt) BindValue(param int, value Value) error {
|
||||
func (s *Stmt) ColumnCount() int {
|
||||
r := s.c.call("sqlite3_column_count",
|
||||
uint64(s.handle))
|
||||
return int(r)
|
||||
return int(int32(r))
|
||||
}
|
||||
|
||||
// ColumnName returns the name of the result column.
|
||||
@@ -367,12 +367,10 @@ func (s *Stmt) ColumnCount() int {
|
||||
func (s *Stmt) ColumnName(col int) string {
|
||||
r := s.c.call("sqlite3_column_name",
|
||||
uint64(s.handle), uint64(col))
|
||||
|
||||
ptr := uint32(r)
|
||||
if ptr == 0 {
|
||||
if r == 0 {
|
||||
panic(util.OOMErr)
|
||||
}
|
||||
return util.ReadString(s.c.mod, ptr, _MAX_NAME)
|
||||
return util.ReadString(s.c.mod, uint32(r), _MAX_NAME)
|
||||
}
|
||||
|
||||
// ColumnType returns the initial [Datatype] of the result column.
|
||||
@@ -398,15 +396,57 @@ func (s *Stmt) ColumnDeclType(col int) string {
|
||||
return util.ReadString(s.c.mod, uint32(r), _MAX_NAME)
|
||||
}
|
||||
|
||||
// ColumnDatabaseName returns the name of the database
|
||||
// that is the origin of a particular result column.
|
||||
// The leftmost column of the result set has the index 0.
|
||||
//
|
||||
// https://sqlite.org/c3ref/column_database_name.html
|
||||
func (s *Stmt) ColumnDatabaseName(col int) string {
|
||||
r := s.c.call("sqlite3_column_database_name",
|
||||
uint64(s.handle), uint64(col))
|
||||
if r == 0 {
|
||||
return ""
|
||||
}
|
||||
return util.ReadString(s.c.mod, uint32(r), _MAX_NAME)
|
||||
}
|
||||
|
||||
// ColumnTableName returns the name of the table
|
||||
// that is the origin of a particular result column.
|
||||
// The leftmost column of the result set has the index 0.
|
||||
//
|
||||
// https://sqlite.org/c3ref/column_database_name.html
|
||||
func (s *Stmt) ColumnTableName(col int) string {
|
||||
r := s.c.call("sqlite3_column_table_name",
|
||||
uint64(s.handle), uint64(col))
|
||||
if r == 0 {
|
||||
return ""
|
||||
}
|
||||
return util.ReadString(s.c.mod, uint32(r), _MAX_NAME)
|
||||
}
|
||||
|
||||
// ColumnOriginName returns the name of the table column
|
||||
// that is the origin of a particular result column.
|
||||
// The leftmost column of the result set has the index 0.
|
||||
//
|
||||
// https://sqlite.org/c3ref/column_database_name.html
|
||||
func (s *Stmt) ColumnOriginName(col int) string {
|
||||
r := s.c.call("sqlite3_column_origin_name",
|
||||
uint64(s.handle), uint64(col))
|
||||
if r == 0 {
|
||||
return ""
|
||||
}
|
||||
return util.ReadString(s.c.mod, uint32(r), _MAX_NAME)
|
||||
}
|
||||
|
||||
// ColumnBool returns the value of the result column as a bool.
|
||||
// The leftmost column of the result set has the index 0.
|
||||
// SQLite does not have a separate boolean storage class.
|
||||
// Instead, boolean values are retrieved as integers,
|
||||
// Instead, boolean values are retrieved as numbers,
|
||||
// with 0 converted to false and any other value to true.
|
||||
//
|
||||
// https://sqlite.org/c3ref/column_blob.html
|
||||
func (s *Stmt) ColumnBool(col int) bool {
|
||||
return s.ColumnInt64(col) != 0
|
||||
return s.ColumnFloat(col) != 0
|
||||
}
|
||||
|
||||
// ColumnInt returns the value of the result column as an int.
|
||||
@@ -553,6 +593,14 @@ func (s *Stmt) ColumnValue(col int) Value {
|
||||
}
|
||||
}
|
||||
|
||||
// Columns populates result columns into the provided slice.
|
||||
// The slice must have [Stmt.ColumnCount] length.
|
||||
//
|
||||
// [INTEGER] columns will be retrieved as int64 values,
|
||||
// [FLOAT] as float64, [NULL] as nil,
|
||||
// [TEXT] as string, and [BLOB] as []byte.
|
||||
// Any []byte are owned by SQLite and may be invalidated by
|
||||
// subsequent calls to [Stmt] methods.
|
||||
func (s *Stmt) Columns(dest []any) error {
|
||||
defer s.c.arena.mark()()
|
||||
count := uint64(len(dest))
|
||||
|
||||
@@ -6,9 +6,14 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
)
|
||||
|
||||
func TestBackup(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
backupName := filepath.Join(t.TempDir(), "backup.db")
|
||||
@@ -20,7 +25,7 @@ func TestBackup(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
|
||||
err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestBlob(t *testing.T) {
|
||||
@@ -22,7 +23,7 @@ func TestBlob(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -97,7 +98,7 @@ func TestBlob_large(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -158,7 +159,7 @@ func TestBlob_overflow(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -217,7 +218,7 @@ func TestBlob_invalid(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -242,7 +243,7 @@ func TestBlob_Write_readonly(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -273,7 +274,7 @@ func TestBlob_Read_expired(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -309,7 +310,7 @@ func TestBlob_Seek(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -358,7 +359,7 @@ func TestBlob_Reopen(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build (linux || darwin || windows || freebsd || illumos) && !sqlite3_nosys
|
||||
|
||||
package bradfitz
|
||||
|
||||
// Adapted from: https://github.com/bradfitz/go-sql-test
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
type Tester interface {
|
||||
|
||||
@@ -6,12 +6,13 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func TestConn_Open_dir(t *testing.T) {
|
||||
@@ -111,41 +112,6 @@ func TestConn_Close_BUSY(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_Pragma(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open("file::memory:?_pragma=busy_timeout(1000)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
got, err := db.Pragma("busy_timeout")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{"1000"}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
|
||||
var serr *sqlite3.Error
|
||||
_, err = db.Pragma("+")
|
||||
if err == nil {
|
||||
t.Error("want: error")
|
||||
}
|
||||
if !errors.As(err, &serr) {
|
||||
t.Fatalf("got %T, want sqlite3.Error", err)
|
||||
}
|
||||
if rc := serr.Code(); rc != sqlite3.ERROR {
|
||||
t.Errorf("got %d, want sqlite3.ERROR", rc)
|
||||
}
|
||||
if got := err.Error(); got != `sqlite3: SQL logic error: near "+": syntax error` {
|
||||
t.Error("got message:", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_SetInterrupt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -173,7 +139,7 @@ func TestConn_SetInterrupt(t *testing.T) {
|
||||
SELECT 0, 1
|
||||
UNION ALL
|
||||
SELECT next, curr + next FROM fibonacci
|
||||
LIMIT 1e6
|
||||
LIMIT 1e7
|
||||
)
|
||||
SELECT min(curr) FROM fibonacci
|
||||
`)
|
||||
@@ -450,6 +416,48 @@ func TestConn_SetLastInsertRowID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_Filename(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file := filepath.Join(t.TempDir(), "test.db")
|
||||
db, err := sqlite3.Open(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
n := db.Filename("")
|
||||
if n.String() != file {
|
||||
t.Errorf("got %v", n)
|
||||
}
|
||||
if n.Database() != file {
|
||||
t.Errorf("got %v", n)
|
||||
}
|
||||
if n.DatabaseFile() == nil {
|
||||
t.Errorf("got %v", n)
|
||||
}
|
||||
|
||||
n = db.Filename("xpto")
|
||||
if n != nil {
|
||||
t.Errorf("got %v", n)
|
||||
}
|
||||
if n.String() != "" {
|
||||
t.Errorf("got %v", n)
|
||||
}
|
||||
if n.Database() != "" {
|
||||
t.Errorf("got %v", n)
|
||||
}
|
||||
if n.Journal() != "" {
|
||||
t.Errorf("got %v", n)
|
||||
}
|
||||
if n.WAL() != "" {
|
||||
t.Errorf("got %v", n)
|
||||
}
|
||||
if n.DatabaseFile() != nil {
|
||||
t.Errorf("got %v", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_ReadOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -485,3 +493,35 @@ func TestConn_DBName(t *testing.T) {
|
||||
t.Errorf("got %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_AutoVacuumPages(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open("file:test.db?vfs=memdb&_pragma=auto_vacuum(full)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.AutoVacuumPages(func(schema string, dbPages, freePages, bytesPerPage uint) uint {
|
||||
return freePages
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO test VALUES (zeroblob(1024*1024))`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`DROP TABLE test`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,17 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/adiantum"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
//go:embed testdata/wal.db
|
||||
var waldb []byte
|
||||
var walDB []byte
|
||||
|
||||
//go:embed testdata/utf16be.db
|
||||
var utf16db []byte
|
||||
var utf16DB []byte
|
||||
|
||||
func TestDB_memory(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -24,21 +27,22 @@ func TestDB_memory(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDB_file(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
testDB(t, filepath.Join(t.TempDir(), "test.db"))
|
||||
}
|
||||
|
||||
func TestDB_nolock(t *testing.T) {
|
||||
t.Parallel()
|
||||
testDB(t, "file:"+
|
||||
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db"))+
|
||||
"?nolock=1")
|
||||
}
|
||||
|
||||
func TestDB_wal(t *testing.T) {
|
||||
if !vfs.SupportsSharedMemory {
|
||||
t.Skip("skipping without shared memory")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
tmp := filepath.Join(t.TempDir(), "test.db")
|
||||
err := os.WriteFile(tmp, waldb, 0666)
|
||||
err := os.WriteFile(tmp, walDB, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -46,19 +50,37 @@ func TestDB_wal(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDB_utf16(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
tmp := filepath.Join(t.TempDir(), "test.db")
|
||||
err := os.WriteFile(tmp, utf16db, 0666)
|
||||
err := os.WriteFile(tmp, utf16DB, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testDB(t, tmp)
|
||||
}
|
||||
|
||||
func TestDB_vfs(t *testing.T) {
|
||||
func TestDB_memdb(t *testing.T) {
|
||||
t.Parallel()
|
||||
testDB(t, "file:test.db?vfs=memdb")
|
||||
}
|
||||
|
||||
func TestDB_adiantum(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := filepath.Join(t.TempDir(), "test.db")
|
||||
testDB(t, "file:"+filepath.ToSlash(tmp)+"?nolock=1"+
|
||||
"&vfs=adiantum&textkey=correct+horse+battery+staple")
|
||||
}
|
||||
|
||||
func TestDB_nolock(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmp := filepath.Join(t.TempDir(), "test.db")
|
||||
testDB(t, "file:"+filepath.ToSlash(tmp)+"?nolock=1")
|
||||
}
|
||||
|
||||
func testDB(t testing.TB, name string) {
|
||||
db, err := sqlite3.Open(name)
|
||||
if err != 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"
|
||||
)
|
||||
|
||||
func TestDriver(t *testing.T) {
|
||||
@@ -27,7 +28,7 @@ func TestDriver(t *testing.T) {
|
||||
defer conn.Close()
|
||||
|
||||
res, err := conn.ExecContext(ctx,
|
||||
`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
|
||||
`CREATE TABLE users (id INT, name VARCHAR(10))`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
75
tests/endian_test.go
Normal file
75
tests/endian_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
func Test_endianness(t *testing.T) {
|
||||
big := binary.BigEndian.AppendUint64(nil, 0x1234567890ABCDEF)
|
||||
little := binary.LittleEndian.AppendUint64(nil, 0x1234567890ABCDEF)
|
||||
native := binary.NativeEndian.AppendUint64(nil, 0x1234567890ABCDEF)
|
||||
switch {
|
||||
case bytes.Equal(big, native):
|
||||
t.Log("Platform is big endian")
|
||||
case bytes.Equal(little, native):
|
||||
t.Log("Platform is little endian")
|
||||
default:
|
||||
t.Fatal("Platform is middle endian")
|
||||
}
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
const value int64 = -9223372036854775808
|
||||
{
|
||||
stmt, _, err := db.Prepare(`INSERT INTO test VALUES (?)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
err = stmt.BindInt64(1, value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = stmt.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
{
|
||||
stmt, _, err := db.Prepare(`SELECT * FROM test`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnInt64(0); got != value {
|
||||
t.Errorf("got %d, want %d", got, value)
|
||||
}
|
||||
if got := stmt.ColumnText(0); got != strconv.FormatInt(value, 10) {
|
||||
t.Errorf("got %s, want %d", got, value)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func Test_base64(t *testing.T) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestCreateFunction(t *testing.T) {
|
||||
@@ -196,7 +197,7 @@ func TestAnyCollationNeeded(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
|
||||
err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/julianday"
|
||||
)
|
||||
|
||||
@@ -31,7 +32,7 @@ func TestJSON(t *testing.T) {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
_, err = conn.ExecContext(ctx, `CREATE TABLE test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,15 +6,23 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/adiantum"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func TestParallel(t *testing.T) {
|
||||
func Test_parallel(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
|
||||
var iter int
|
||||
if testing.Short() {
|
||||
iter = 1000
|
||||
@@ -31,7 +39,21 @@ func TestParallel(t *testing.T) {
|
||||
testIntegrity(t, name)
|
||||
}
|
||||
|
||||
func TestMemory(t *testing.T) {
|
||||
func Test_wal(t *testing.T) {
|
||||
if !vfs.SupportsSharedMemory {
|
||||
t.Skip("skipping without shared memory")
|
||||
}
|
||||
|
||||
name := "file:" +
|
||||
filepath.Join(t.TempDir(), "test.db") +
|
||||
"?_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(wal)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
testParallel(t, name, 1000)
|
||||
testIntegrity(t, name)
|
||||
}
|
||||
|
||||
func Test_memdb(t *testing.T) {
|
||||
var iter int
|
||||
if testing.Short() {
|
||||
iter = 1000
|
||||
@@ -39,15 +61,37 @@ func TestMemory(t *testing.T) {
|
||||
iter = 5000
|
||||
}
|
||||
|
||||
name := "file:/test.db?vfs=memdb" +
|
||||
"&_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(memory)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
memdb.Delete("test.db")
|
||||
memdb.Create("test.db", nil)
|
||||
name := "file:/test.db?vfs=memdb"
|
||||
testParallel(t, name, iter)
|
||||
testIntegrity(t, name)
|
||||
}
|
||||
|
||||
func Test_adiantum(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
|
||||
var iter int
|
||||
if testing.Short() {
|
||||
iter = 1000
|
||||
} else {
|
||||
iter = 5000
|
||||
}
|
||||
|
||||
name := "file:" +
|
||||
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db")) +
|
||||
"?vfs=adiantum" +
|
||||
"&_pragma=hexkey(e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855)"
|
||||
testParallel(t, name, iter)
|
||||
testIntegrity(t, name)
|
||||
}
|
||||
|
||||
func TestMultiProcess(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
if testing.Short() {
|
||||
t.Skip("skipping in short mode")
|
||||
}
|
||||
@@ -98,12 +142,13 @@ func TestChildProcess(t *testing.T) {
|
||||
testParallel(t, name, 1000)
|
||||
}
|
||||
|
||||
func BenchmarkMemory(b *testing.B) {
|
||||
func Benchmark_memdb(b *testing.B) {
|
||||
sqlite3.Initialize()
|
||||
b.ResetTimer()
|
||||
|
||||
memdb.Delete("test.db")
|
||||
name := "file:/test.db?vfs=memdb" +
|
||||
"&_pragma=busy_timeout(10000)" +
|
||||
"&_pragma=journal_mode(memory)" +
|
||||
"&_pragma=synchronous(off)"
|
||||
memdb.Create("test.db", nil)
|
||||
name := "file:/test.db?vfs=memdb"
|
||||
testParallel(b, name, b.N)
|
||||
}
|
||||
|
||||
@@ -115,6 +160,14 @@ func testParallel(t testing.TB, name string, n int) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.BusyHandler(func(count int) (retry bool) {
|
||||
time.Sleep(time.Millisecond)
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -135,7 +188,7 @@ func testParallel(t testing.TB, name string, n int) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`PRAGMA busy_timeout=10000`)
|
||||
err = db.BusyTimeout(10 * time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package tests
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"math/bits"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func TestStmt(t *testing.T) {
|
||||
@@ -19,7 +21,7 @@ func TestStmt(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
err = db.Exec(`CREATE TABLE test (col ANY) STRICT`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -135,7 +137,7 @@ func TestStmt(t *testing.T) {
|
||||
}
|
||||
|
||||
// The table should have: 0, 1, 2, π, NULL, "", "text", "", "blob", NULL, "\0\0\0\0", "true", NULL
|
||||
stmt, _, err = db.Prepare(`SELECT col FROM test`)
|
||||
stmt, _, err = db.Prepare(`SELECT col AS c FROM test`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -144,6 +146,21 @@ func TestStmt(t *testing.T) {
|
||||
if got := stmt.ReadOnly(); got != true {
|
||||
t.Error("got false, want true")
|
||||
}
|
||||
if got := stmt.ColumnName(0); got != "c" {
|
||||
t.Errorf(`got %q, want "c"`, got)
|
||||
}
|
||||
if got := stmt.ColumnDeclType(0); got != "ANY" {
|
||||
t.Errorf(`got %q, want "ANY"`, got)
|
||||
}
|
||||
if got := stmt.ColumnOriginName(0); got != "col" {
|
||||
t.Errorf(`got %q, want "col"`, got)
|
||||
}
|
||||
if got := stmt.ColumnTableName(0); got != "test" {
|
||||
t.Errorf(`got %q, want "test"`, got)
|
||||
}
|
||||
if got := stmt.ColumnDatabaseName(0); got != "main" {
|
||||
t.Errorf(`got %q, want "main"`, got)
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnType(0); got != sqlite3.INTEGER {
|
||||
@@ -601,6 +618,9 @@ func TestStmt_ColumnTime(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStmt_Error(t *testing.T) {
|
||||
if bits.UintSize < 64 {
|
||||
t.Skip("skipping on 32-bit")
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
|
||||
BIN
tests/testdata/f2fs.img.gz
vendored
Normal file
BIN
tests/testdata/f2fs.img.gz
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user