mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-12 22:19:14 +00:00
Compare commits
276 Commits
v0.6.1
...
gormlite/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
031087327d | ||
|
|
c9cc893ed7 | ||
|
|
99ad7ff766 | ||
|
|
019c71fb55 | ||
|
|
88cf845651 | ||
|
|
354242e528 | ||
|
|
3d906d47dd | ||
|
|
9df3488964 | ||
|
|
d998b5f36c | ||
|
|
9f58a5d669 | ||
|
|
7d52cb259b | ||
|
|
35bbd8a0b0 | ||
|
|
bce66299ab | ||
|
|
bc840dcefb | ||
|
|
c822fa95c7 | ||
|
|
1b2c267b2b | ||
|
|
3d99af86bf | ||
|
|
145bc228af | ||
|
|
6b0c2c0554 | ||
|
|
97f2b73701 | ||
|
|
cb1e33a32d | ||
|
|
ee48dd5c96 | ||
|
|
af42af2978 | ||
|
|
d48a92fcdf | ||
|
|
69937fbee5 | ||
|
|
2fb325b223 | ||
|
|
f0c583a581 | ||
|
|
17ce949c55 | ||
|
|
ae850191c8 | ||
|
|
fab70ddbec | ||
|
|
a3c5f47d79 | ||
|
|
16b5d80ef7 | ||
|
|
7e5a143214 | ||
|
|
92d75f7446 | ||
|
|
d56ee4ac2c | ||
|
|
e944d5d8e7 | ||
|
|
fde2277b4a | ||
|
|
1ebdeed565 | ||
|
|
89202629ec | ||
|
|
cb62771a45 | ||
|
|
0bb1cd5e2e | ||
|
|
7bbd4f1e3c | ||
|
|
ed4a3a894b | ||
|
|
f1b00a9944 | ||
|
|
9281948f57 | ||
|
|
b0b27439b5 | ||
|
|
c938577763 | ||
|
|
ebbb969cd7 | ||
|
|
0171743e88 | ||
|
|
c68413bd53 | ||
|
|
3f8b480ba0 | ||
|
|
9866067701 | ||
|
|
964a42c76d | ||
|
|
0b093b7c0e | ||
|
|
32a824cb6c | ||
|
|
2e1c65147a | ||
|
|
86cc08e4d6 | ||
|
|
05077b8845 | ||
|
|
6e8d5e5be6 | ||
|
|
c99fbcea6f | ||
|
|
831a34a4c4 | ||
|
|
7c820ede3c | ||
|
|
089a0c0670 | ||
|
|
8b45cac16b | ||
|
|
06d2ff6752 | ||
|
|
987f0f13a2 | ||
|
|
cd40213898 | ||
|
|
8a0baedc10 | ||
|
|
c667a1f469 | ||
|
|
9c562f5d8b | ||
|
|
d862f47d95 | ||
|
|
a9e32fd3f0 | ||
|
|
b262f5cd01 | ||
|
|
4160b9a4bb | ||
|
|
dbaf2d99cd | ||
|
|
3f05115cd7 | ||
|
|
9bf14becaf | ||
|
|
997e197f54 | ||
|
|
b81fe284b6 | ||
|
|
269306c5c8 | ||
|
|
8a18243830 | ||
|
|
fcd6cc91d8 | ||
|
|
c1838fc0bc | ||
|
|
dc0d8236bf | ||
|
|
ba18facb0d | ||
|
|
5653efa70e | ||
|
|
1acb95917a | ||
|
|
e31a42fb22 | ||
|
|
aec69acca9 | ||
|
|
f2d6bdb8b7 | ||
|
|
9bb01d1f8b | ||
|
|
83c15f2ddc | ||
|
|
97d4248176 | ||
|
|
22d1ae0068 | ||
|
|
ae1d696cf3 | ||
|
|
5992403052 | ||
|
|
f212b6712d | ||
|
|
a49cc084f3 | ||
|
|
2c2f825bc5 | ||
|
|
787086b8c1 | ||
|
|
314098addb | ||
|
|
4bf8a79c1d | ||
|
|
90628ab8aa | ||
|
|
aa02c14430 | ||
|
|
1dc06bff49 | ||
|
|
4e9173661b | ||
|
|
591480cd39 | ||
|
|
828788912e | ||
|
|
6f8645cd2e | ||
|
|
c00927e8bb | ||
|
|
6b28be6d0e | ||
|
|
310b4ff29d | ||
|
|
e82cf16b11 | ||
|
|
24c9b57c56 | ||
|
|
24b965ac7e | ||
|
|
446168c572 | ||
|
|
a9e2cbbfc5 | ||
|
|
a7c00eb150 | ||
|
|
0bcdb712ba | ||
|
|
2157d0f325 | ||
|
|
6353160619 | ||
|
|
501d157279 | ||
|
|
4db18a7b9a | ||
|
|
a9dddaa86c | ||
|
|
b25936dbec | ||
|
|
bf23041e46 | ||
|
|
d60fceac92 | ||
|
|
61da30f44a | ||
|
|
d4ff605983 | ||
|
|
8d0c654178 | ||
|
|
728e59951b | ||
|
|
f7b16bad5c | ||
|
|
db3e6da31a | ||
|
|
3f443b2ecc | ||
|
|
eec45ea684 | ||
|
|
f6d77f3cf4 | ||
|
|
d5d7cd1f2d | ||
|
|
a33a187d48 | ||
|
|
70c6ee15c6 | ||
|
|
994d9b1812 | ||
|
|
b19bd28ed3 | ||
|
|
e66bd51845 | ||
|
|
f5614bc2ed | ||
|
|
d9fcf60b7d | ||
|
|
ac6dd1aa5f | ||
|
|
b1495bd6cb | ||
|
|
2d91760295 | ||
|
|
38d4254bc4 | ||
|
|
c0aa734786 | ||
|
|
fa845dbd3d | ||
|
|
fed315ab79 | ||
|
|
726d7316f7 | ||
|
|
ddb387b021 | ||
|
|
d0f19507f5 | ||
|
|
9d997552ad | ||
|
|
9d75c39dcc | ||
|
|
746a84965e | ||
|
|
312d3b58f2 | ||
|
|
b71cd295c2 | ||
|
|
5b3b61a304 | ||
|
|
d661d15723 | ||
|
|
1e38165ad0 | ||
|
|
58a32d7c9d | ||
|
|
6765e883c1 | ||
|
|
18fc608433 | ||
|
|
77f37893b9 | ||
|
|
f1e36e2581 | ||
|
|
772b9153c7 | ||
|
|
4b280a3a7e | ||
|
|
19b6098bf6 | ||
|
|
2aa685320f | ||
|
|
9941be05c2 | ||
|
|
a0a9ab7737 | ||
|
|
a77727a1ce | ||
|
|
47fe032078 | ||
|
|
bdfe279444 | ||
|
|
a86937a54e | ||
|
|
6ef422fbde | ||
|
|
ff0cb6fb88 | ||
|
|
72db90efdf | ||
|
|
5a3fdef3c5 | ||
|
|
ff34b0cae1 | ||
|
|
f064492bb1 | ||
|
|
1427d30541 | ||
|
|
d3730341f0 | ||
|
|
78ac2386f6 | ||
|
|
632ea933b3 | ||
|
|
0f7fa6ebc9 | ||
|
|
6f7f776488 | ||
|
|
f6d7c5e9c5 | ||
|
|
1cc7ecfe8d | ||
|
|
3844e81404 | ||
|
|
fec1f8d32a | ||
|
|
31572e6095 | ||
|
|
4aee38b957 | ||
|
|
232a7705b5 | ||
|
|
a6c2fccd74 | ||
|
|
6a982559cd | ||
|
|
c7904d30de | ||
|
|
ce4386604d | ||
|
|
26b62c520d | ||
|
|
738714bf32 | ||
|
|
41b020bafc | ||
|
|
d0e720272b | ||
|
|
76171da12b | ||
|
|
dcc845d684 | ||
|
|
f1b42c26d5 | ||
|
|
1e94407ae7 | ||
|
|
eb8d9b95fd | ||
|
|
04037a75ed | ||
|
|
2472ceb0a0 | ||
|
|
bfe9bfde2e | ||
|
|
f07e82e361 | ||
|
|
fbbbe5a631 | ||
|
|
5ea603ed78 | ||
|
|
401cb77e38 | ||
|
|
6511175011 | ||
|
|
f7d987fdf1 | ||
|
|
00ba681bb5 | ||
|
|
d4d4533a41 | ||
|
|
ec9533b13f | ||
|
|
8fe77a065c | ||
|
|
7bf5312bd4 | ||
|
|
ae7b74d858 | ||
|
|
9a8de3ad13 | ||
|
|
05737e6025 | ||
|
|
ac2836bb82 | ||
|
|
d0d4b0e1a2 | ||
|
|
dc3dc6853d | ||
|
|
830240c368 | ||
|
|
dedec8682b | ||
|
|
a33b828e13 | ||
|
|
8b2e96dedc | ||
|
|
f1c46db512 | ||
|
|
7ca9d79424 | ||
|
|
254d473546 | ||
|
|
5639fc1ff8 | ||
|
|
ae4954d09b | ||
|
|
45937d9749 | ||
|
|
eee71e06aa | ||
|
|
9e7b6bb8ea | ||
|
|
597178f80d | ||
|
|
cc2d16ac83 | ||
|
|
cfb69e4ce7 | ||
|
|
e6969432e3 | ||
|
|
2b3da350cc |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
||||
custom: https://www.paypal.com/donate/buttons/manage/33P59ELZWGMK6
|
||||
custom: https://www.paypal.com/donate?hosted_button_id=33P59ELZWGMK6
|
||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -9,3 +9,7 @@ updates:
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "github-actions" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
10
.github/workflows/bsd.sh
vendored
Executable file
10
.github/workflows/bsd.sh
vendored
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo 'set -euo pipefail' > test.sh
|
||||
|
||||
for p in $(go list ./...); do
|
||||
dir=".${p#github.com/ncruces/go-sqlite3}"
|
||||
name="$(basename "$p").test"
|
||||
(cd ${dir}; GOOS=freebsd go test -c)
|
||||
[ -f "${dir}/${name}" ] && echo "(cd ${dir}; ./${name} -test.v)" >> test.sh
|
||||
done
|
||||
76
.github/workflows/codeql.yml
vendored
76
.github/workflows/codeql.yml
vendored
@@ -1,76 +0,0 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '15 18 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
23
.github/workflows/cross.sh
vendored
Executable file
23
.github/workflows/cross.sh
vendored
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo android ; GOOS=android GOARCH=amd64 go build .
|
||||
echo darwin ; GOOS=darwin GOARCH=amd64 go build .
|
||||
echo dragonfly ; GOOS=dragonfly GOARCH=amd64 go build .
|
||||
echo freebsd ; GOOS=freebsd GOARCH=amd64 go build .
|
||||
echo illumos ; GOOS=illumos GOARCH=amd64 go build .
|
||||
echo ios ; GOOS=ios GOARCH=amd64 go build .
|
||||
echo linux ; GOOS=linux GOARCH=amd64 go build .
|
||||
echo netbsd ; GOOS=netbsd GOARCH=amd64 go build .
|
||||
echo openbsd ; GOOS=openbsd GOARCH=amd64 go build .
|
||||
echo plan9 ; GOOS=plan9 GOARCH=amd64 go build .
|
||||
echo solaris ; GOOS=solaris GOARCH=amd64 go build .
|
||||
echo windows ; GOOS=windows GOARCH=amd64 go build .
|
||||
echo aix ; GOOS=aix GOARCH=ppc64 go build .
|
||||
echo js ; GOOS=js GOARCH=wasm go build .
|
||||
echo wasip1 ; GOOS=wasip1 GOARCH=wasm go build .
|
||||
echo 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-nosys ; GOOS=linux 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 .
|
||||
18
.github/workflows/cross.yml
vendored
Normal file
18
.github/workflows/cross.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Cross compile
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Build
|
||||
run: .github/workflows/cross.sh
|
||||
62
.github/workflows/go.yml
vendored
62
.github/workflows/go.yml
vendored
@@ -1,62 +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@v3
|
||||
with:
|
||||
lfs: 'true'
|
||||
|
||||
- name: Set up
|
||||
uses: actions/setup-go@v4
|
||||
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 ./...
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Test BSD locks
|
||||
run: go test -v -tags sqlite3_bsd ./...
|
||||
if: matrix.os == 'macos-latest'
|
||||
|
||||
- name: Coverage report
|
||||
uses: ncruces/go-coverage-report@v0
|
||||
with:
|
||||
chart: 'true'
|
||||
amend: 'true'
|
||||
if: |
|
||||
matrix.os == 'ubuntu-latest' &&
|
||||
github.event_name == 'push'
|
||||
continue-on-error: true
|
||||
23
.github/workflows/repro.sh
vendored
Executable file
23
.github/workflows/repro.sh
vendored
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
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_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_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_117/binaryen-version_117-x86_64-windows.tar.gz"
|
||||
fi
|
||||
|
||||
# Download tools
|
||||
mkdir -p tools
|
||||
[ -d "tools/wasi-sdk"* ] || curl -#L "$WASI_SDK" | tar xzC tools &
|
||||
[ -d "tools/binaryen-version"* ] || curl -#L "$BINARYEN" | tar xzC tools &
|
||||
wait
|
||||
|
||||
sqlite3/download.sh # Download SQLite
|
||||
embed/build.sh # Build WASM
|
||||
git diff --exit-code # Check diffs
|
||||
23
.github/workflows/repro.yml
vendored
Normal file
23
.github/workflows/repro.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Reproducible build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: 'true'
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Build
|
||||
run: .github/workflows/repro.sh
|
||||
129
.github/workflows/test.yml
vendored
Normal file
129
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
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
|
||||
with: { lfs: 'true' }
|
||||
|
||||
- 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 BSD locks
|
||||
run: go test -v -tags sqlite3_flock ./...
|
||||
if: matrix.os == 'macos-latest'
|
||||
|
||||
- name: Test no locks
|
||||
run: go test -v -tags sqlite3_nosys ./tests -run TestDB_nolock
|
||||
|
||||
- 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-bsd:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { lfs: 'true' }
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Build
|
||||
run: .github/workflows/bsd.sh
|
||||
|
||||
- name: Test
|
||||
uses: cross-platform-actions/action@v0.23.0
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '14.0'
|
||||
shell: bash
|
||||
run: source test.sh
|
||||
sync_files: runner-to-vm
|
||||
|
||||
test-m1:
|
||||
runs-on: macos-14
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { lfs: 'true' }
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
test-386:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { lfs: 'true' }
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Test
|
||||
run: GOARCH=386 go test -v -short ./...
|
||||
|
||||
test-arm:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { lfs: 'true' }
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Test
|
||||
run: GOARCH=arm64 go test -v -short ./...
|
||||
153
README.md
153
README.md
@@ -4,82 +4,139 @@
|
||||
[](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` wraps a [WASM](https://webassembly.org/) build of [SQLite](https://sqlite.org/),
|
||||
and uses [wazero](https://wazero.io/) to provide `cgo`-free SQLite bindings.
|
||||
Go module `github.com/ncruces/go-sqlite3` is `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).
|
||||
|
||||
- Package [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
|
||||
wraps the [C SQLite API](https://www.sqlite.org/cintro.html)
|
||||
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-package)).
|
||||
- Package [`github.com/ncruces/go-sqlite3/driver`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver)
|
||||
provides a [`database/sql`](https://pkg.go.dev/database/sql) driver
|
||||
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package)).
|
||||
- Package [`github.com/ncruces/go-sqlite3/embed`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/embed)
|
||||
embeds a build of SQLite into your application.
|
||||
- Package [`github.com/ncruces/go-sqlite3/sqlite3vfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/sqlite3vfs)
|
||||
wraps the [C SQLite VFS API](https://www.sqlite.org/vfs.html) and provides a pure Go implementation.
|
||||
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.
|
||||
|
||||
### Packages
|
||||
|
||||
- [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
|
||||
wraps the [C SQLite API](https://sqlite.org/cintro.html)
|
||||
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-package)).
|
||||
- [`github.com/ncruces/go-sqlite3/driver`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver)
|
||||
provides a [`database/sql`](https://pkg.go.dev/database/sql) driver
|
||||
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package)).
|
||||
- [`github.com/ncruces/go-sqlite3/embed`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/embed)
|
||||
embeds a build of SQLite into your application.
|
||||
- [`github.com/ncruces/go-sqlite3/vfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs)
|
||||
wraps the [C SQLite VFS API](https://sqlite.org/vfs.html) and provides a pure Go implementation.
|
||||
- [`github.com/ncruces/go-sqlite3/gormlite`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/gormlite)
|
||||
provides a [GORM](https://gorm.io) driver.
|
||||
|
||||
### Extensions
|
||||
|
||||
- [`github.com/ncruces/go-sqlite3/ext/array`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/array)
|
||||
provides the [`array`](https://sqlite.org/carray.html) table-valued function.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/blobio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blobio)
|
||||
simplifies [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/csv`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/csv)
|
||||
reads [comma-separated values](https://sqlite.org/csv.html).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/fileio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/fileio)
|
||||
reads, writes and lists files.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/hash`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/hash)
|
||||
provides cryptographic hash functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/lines`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/lines)
|
||||
reads data [line-by-line](https://github.com/asg017/sqlite-lines).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/pivot`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/pivot)
|
||||
creates [pivot tables](https://github.com/jakethaw/pivot_vtab).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/statement`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/statement)
|
||||
creates [parameterized views](https://github.com/0x09/sqlite-statement-vtab).
|
||||
- [`github.com/ncruces/go-sqlite3/ext/stats`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
|
||||
provides [statistics](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html) functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
|
||||
provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions.
|
||||
- [`github.com/ncruces/go-sqlite3/ext/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.
|
||||
|
||||
### Advanced features
|
||||
|
||||
- [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html)
|
||||
- [nested transactions](https://sqlite.org/lang_savepoint.html)
|
||||
- [custom functions](https://sqlite.org/c3ref/create_function.html)
|
||||
- [virtual tables](https://sqlite.org/vtab.html)
|
||||
- [custom VFSes](https://sqlite.org/vfs.html)
|
||||
- [online backup](https://sqlite.org/backup.html)
|
||||
- [JSON support](https://sqlite.org/json1.html)
|
||||
- [math functions](https://sqlite.org/lang_mathfunc.html)
|
||||
- [full-text search](https://sqlite.org/fts5.html)
|
||||
- [geospatial search](https://sqlite.org/geopoly.html)
|
||||
- [and more…](embed/README.md)
|
||||
|
||||
### Caveats
|
||||
|
||||
This module replaces the SQLite [OS Interface](https://www.sqlite.org/vfs.html) (aka VFS)
|
||||
with a [pure Go](sqlite3vfs/) implementation.
|
||||
This has numerous benefits, but also comes with some drawbacks.
|
||||
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.
|
||||
|
||||
#### Write-Ahead Logging
|
||||
|
||||
Because WASM does not support shared memory,
|
||||
[WAL](https://www.sqlite.org/wal.html) support is [limited](https://www.sqlite.org/wal.html#noshm).
|
||||
[WAL](https://sqlite.org/wal.html) support is [limited](https://sqlite.org/wal.html#noshm).
|
||||
|
||||
To work around this limitation, SQLite is compiled with
|
||||
[`SQLITE_DEFAULT_LOCKING_MODE=1`](https://www.sqlite.org/compile.html#default_locking_mode),
|
||||
making `EXCLUSIVE` the default locking mode.
|
||||
For non-WAL databases, `NORMAL` locking mode can be activated with
|
||||
[`PRAGMA locking_mode=NORMAL`](https://www.sqlite.org/pragma.html#pragma_locking_mode).
|
||||
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,
|
||||
the `database/sql` driver defaults to `NORMAL` locking mode.
|
||||
To open WAL databases, or use `EXCLUSIVE` locking mode,
|
||||
disable connection pooling by calling
|
||||
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).
|
||||
|
||||
#### POSIX Advisory Locks
|
||||
#### File Locking
|
||||
|
||||
POSIX advisory locks, which SQLite uses, are
|
||||
[broken by design](https://www.sqlite.org/src/artifact/90c4fa?ln=1073-1161).
|
||||
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 process-associated POSIX advisory locks.
|
||||
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).
|
||||
BSD locks may _not_ be compatible with process-associated POSIX advisory locks.
|
||||
On BSD Unixes, BSD locks are fully compatible with POSIX advisory locks.
|
||||
|
||||
#### Testing
|
||||
On Windows, this module uses `LockFileEx` and `UnlockFileEx`,
|
||||
like SQLite.
|
||||
|
||||
The pure Go VFS is stress tested by running an unmodified build of SQLite's
|
||||
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.
|
||||
You can use [`vfs.SupportsFileLocking`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs#SupportsFileLocking)
|
||||
to check if your platform supports file locking.
|
||||
|
||||
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).
|
||||
|
||||
### Testing
|
||||
|
||||
This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3/wiki/Test-coverage-report).
|
||||
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
|
||||
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
|
||||
|
||||
The pure Go VFS is tested by running SQLite's
|
||||
[mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c)
|
||||
on Linux, macOS and Windows.
|
||||
Performance is tested by running
|
||||
on Linux, macOS, Windows and FreeBSD.
|
||||
|
||||
### 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
|
||||
[speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c).
|
||||
|
||||
### Roadmap
|
||||
|
||||
- [ ] advanced SQLite features
|
||||
- [x] nested transactions
|
||||
- [x] incremental BLOB I/O
|
||||
- [x] online backup
|
||||
- [ ] session extension
|
||||
- [ ] custom SQL functions
|
||||
- [ ] custom VFSes
|
||||
- [ ] in-memory VFS
|
||||
- [ ] read-only VFS, wrapping an [`io.ReaderAt`](https://pkg.go.dev/io#ReaderAt)
|
||||
- [ ] cloud-based VFS, based on [Cloud Backed SQLite](https://sqlite.org/cloudsqlite/doc/trunk/www/index.wiki)
|
||||
- [x] custom VFS API
|
||||
|
||||
### Alternatives
|
||||
|
||||
- [`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite)
|
||||
- [`crawshaw.io/sqlite`](https://pkg.go.dev/crawshaw.io/sqlite)
|
||||
- [`github.com/mattn/go-sqlite3`](https://pkg.go.dev/github.com/mattn/go-sqlite3)
|
||||
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)
|
||||
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)
|
||||
|
||||
46
backup.go
46
backup.go
@@ -2,7 +2,7 @@ package sqlite3
|
||||
|
||||
// Backup is an handle to an ongoing online backup operation.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup.html
|
||||
// https://sqlite.org/c3ref/backup.html
|
||||
type Backup struct {
|
||||
c *Conn
|
||||
handle uint32
|
||||
@@ -15,7 +15,7 @@ type Backup struct {
|
||||
// and blocks until the entire backup is complete.
|
||||
// Use [Conn.BackupInit] for incremental backup.
|
||||
//
|
||||
// https://www.sqlite.org/backup.html
|
||||
// https://sqlite.org/backup.html
|
||||
func (src *Conn) Backup(srcDB, dstURI string) error {
|
||||
b, err := src.BackupInit(srcDB, dstURI)
|
||||
if err != nil {
|
||||
@@ -31,7 +31,7 @@ func (src *Conn) Backup(srcDB, dstURI string) error {
|
||||
// Restore opens the SQLite database file srcURI,
|
||||
// and blocks until the entire restore is complete.
|
||||
//
|
||||
// https://www.sqlite.org/backup.html
|
||||
// https://sqlite.org/backup.html
|
||||
func (dst *Conn) Restore(dstDB, srcURI string) error {
|
||||
src, err := dst.openDB(srcURI, OPEN_READONLY|OPEN_URI)
|
||||
if err != nil {
|
||||
@@ -52,7 +52,7 @@ func (dst *Conn) Restore(dstDB, srcURI string) error {
|
||||
// then initializes a backup that copies the contents of srcDB on the src connection
|
||||
// to the "main" database in dstURI.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
|
||||
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
|
||||
func (src *Conn) BackupInit(srcDB, dstURI string) (*Backup, error) {
|
||||
dst, err := src.openDB(dstURI, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
|
||||
if err != nil {
|
||||
@@ -62,7 +62,7 @@ func (src *Conn) BackupInit(srcDB, dstURI string) (*Backup, error) {
|
||||
}
|
||||
|
||||
func (c *Conn) backupInit(dst uint32, dstName string, src uint32, srcName string) (*Backup, error) {
|
||||
defer c.arena.reset()
|
||||
defer c.arena.mark()()
|
||||
dstPtr := c.arena.string(dstName)
|
||||
srcPtr := c.arena.string(srcName)
|
||||
|
||||
@@ -71,19 +71,19 @@ func (c *Conn) backupInit(dst uint32, dstName string, src uint32, srcName string
|
||||
other = src
|
||||
}
|
||||
|
||||
r := c.call(c.api.backupInit,
|
||||
r := c.call("sqlite3_backup_init",
|
||||
uint64(dst), uint64(dstPtr),
|
||||
uint64(src), uint64(srcPtr))
|
||||
if r[0] == 0 {
|
||||
if r == 0 {
|
||||
defer c.closeDB(other)
|
||||
r = c.call(c.api.errcode, uint64(dst))
|
||||
return nil, c.module.error(r[0], dst)
|
||||
r = c.call("sqlite3_errcode", uint64(dst))
|
||||
return nil, c.sqlite.error(r, dst)
|
||||
}
|
||||
|
||||
return &Backup{
|
||||
c: c,
|
||||
otherc: other,
|
||||
handle: uint32(r[0]),
|
||||
handle: uint32(r),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -91,44 +91,44 @@ func (c *Conn) backupInit(dst uint32, dstName string, src uint32, srcName string
|
||||
//
|
||||
// It is safe to close a nil, zero or closed Backup.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish
|
||||
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish
|
||||
func (b *Backup) Close() error {
|
||||
if b == nil || b.handle == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
r := b.c.call(b.c.api.backupFinish, uint64(b.handle))
|
||||
r := b.c.call("sqlite3_backup_finish", uint64(b.handle))
|
||||
b.c.closeDB(b.otherc)
|
||||
b.handle = 0
|
||||
return b.c.error(r[0])
|
||||
return b.c.error(r)
|
||||
}
|
||||
|
||||
// Step copies up to nPage pages between the source and destination databases.
|
||||
// If nPage is negative, all remaining source pages are copied.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
|
||||
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
|
||||
func (b *Backup) Step(nPage int) (done bool, err error) {
|
||||
r := b.c.call(b.c.api.backupStep, uint64(b.handle), uint64(nPage))
|
||||
if r[0] == _DONE {
|
||||
r := b.c.call("sqlite3_backup_step", uint64(b.handle), uint64(nPage))
|
||||
if r == _DONE {
|
||||
return true, nil
|
||||
}
|
||||
return false, b.c.error(r[0])
|
||||
return false, b.c.error(r)
|
||||
}
|
||||
|
||||
// Remaining returns the number of pages still to be backed up
|
||||
// at the conclusion of the most recent [Backup.Step].
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupremaining
|
||||
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backupremaining
|
||||
func (b *Backup) Remaining() int {
|
||||
r := b.c.call(b.c.api.backupRemaining, uint64(b.handle))
|
||||
return int(r[0])
|
||||
r := b.c.call("sqlite3_backup_remaining", uint64(b.handle))
|
||||
return int(int32(r))
|
||||
}
|
||||
|
||||
// PageCount returns the total number of pages in the source database
|
||||
// at the conclusion of the most recent [Backup.Step].
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backuppagecount
|
||||
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backuppagecount
|
||||
func (b *Backup) PageCount() int {
|
||||
r := b.c.call(b.c.api.backupFinish, uint64(b.handle))
|
||||
return int(r[0])
|
||||
r := b.c.call("sqlite3_backup_pagecount", uint64(b.handle))
|
||||
return int(int32(r))
|
||||
}
|
||||
|
||||
69
blob.go
69
blob.go
@@ -15,7 +15,7 @@ type ZeroBlob int64
|
||||
//
|
||||
// It implements [io.ReadWriteSeeker] for incremental BLOB I/O.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob.html
|
||||
// https://sqlite.org/c3ref/blob.html
|
||||
type Blob struct {
|
||||
c *Conn
|
||||
bytes int64
|
||||
@@ -27,10 +27,10 @@ var _ io.ReadWriteSeeker = &Blob{}
|
||||
|
||||
// OpenBlob opens a BLOB for incremental I/O.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob_open.html
|
||||
// https://sqlite.org/c3ref/blob_open.html
|
||||
func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob, error) {
|
||||
c.checkInterrupt()
|
||||
defer c.arena.reset()
|
||||
defer c.arena.mark()()
|
||||
blobPtr := c.arena.new(ptrlen)
|
||||
dbPtr := c.arena.string(db)
|
||||
tablePtr := c.arena.string(table)
|
||||
@@ -41,17 +41,17 @@ func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob,
|
||||
flags = 1
|
||||
}
|
||||
|
||||
r := c.call(c.api.blobOpen, uint64(c.handle),
|
||||
r := c.call("sqlite3_blob_open", uint64(c.handle),
|
||||
uint64(dbPtr), uint64(tablePtr), uint64(columnPtr),
|
||||
uint64(row), flags, uint64(blobPtr))
|
||||
|
||||
if err := c.error(r[0]); err != nil {
|
||||
if err := c.error(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blob := Blob{c: c}
|
||||
blob.handle = util.ReadUint32(c.mod, blobPtr)
|
||||
blob.bytes = int64(c.call(c.api.blobBytes, uint64(blob.handle))[0])
|
||||
blob.bytes = int64(c.call("sqlite3_blob_bytes", uint64(blob.handle)))
|
||||
return &blob, nil
|
||||
}
|
||||
|
||||
@@ -59,28 +59,28 @@ func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob,
|
||||
//
|
||||
// It is safe to close a nil, zero or closed Blob.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob_close.html
|
||||
// https://sqlite.org/c3ref/blob_close.html
|
||||
func (b *Blob) Close() error {
|
||||
if b == nil || b.handle == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
r := b.c.call(b.c.api.blobClose, uint64(b.handle))
|
||||
r := b.c.call("sqlite3_blob_close", uint64(b.handle))
|
||||
|
||||
b.handle = 0
|
||||
return b.c.error(r[0])
|
||||
return b.c.error(r)
|
||||
}
|
||||
|
||||
// Size returns the size of the BLOB in bytes.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob_bytes.html
|
||||
// https://sqlite.org/c3ref/blob_bytes.html
|
||||
func (b *Blob) Size() int64 {
|
||||
return b.bytes
|
||||
}
|
||||
|
||||
// Read implements the [io.Reader] interface.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob_read.html
|
||||
// https://sqlite.org/c3ref/blob_read.html
|
||||
func (b *Blob) Read(p []byte) (n int, err error) {
|
||||
if b.offset >= b.bytes {
|
||||
return 0, io.EOF
|
||||
@@ -92,12 +92,12 @@ func (b *Blob) Read(p []byte) (n int, err error) {
|
||||
want = avail
|
||||
}
|
||||
|
||||
defer b.c.arena.reset()
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.new(uint64(want))
|
||||
|
||||
r := b.c.call(b.c.api.blobRead, uint64(b.handle),
|
||||
r := b.c.call("sqlite3_blob_read", uint64(b.handle),
|
||||
uint64(ptr), uint64(want), uint64(b.offset))
|
||||
err = b.c.error(r[0])
|
||||
err = b.c.error(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -112,25 +112,25 @@ func (b *Blob) Read(p []byte) (n int, err error) {
|
||||
|
||||
// WriteTo implements the [io.WriterTo] interface.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob_read.html
|
||||
// https://sqlite.org/c3ref/blob_read.html
|
||||
func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
|
||||
if b.offset >= b.bytes {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
want := int64(1024 * 1024)
|
||||
avail := b.bytes - b.offset
|
||||
want := int64(65536)
|
||||
if want > avail {
|
||||
want = avail
|
||||
}
|
||||
|
||||
ptr := b.c.new(uint64(want))
|
||||
defer b.c.free(ptr)
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.new(uint64(want))
|
||||
|
||||
for want > 0 {
|
||||
r := b.c.call(b.c.api.blobRead, uint64(b.handle),
|
||||
r := b.c.call("sqlite3_blob_read", uint64(b.handle),
|
||||
uint64(ptr), uint64(want), uint64(b.offset))
|
||||
err = b.c.error(r[0])
|
||||
err = b.c.error(r)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
@@ -156,14 +156,14 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
|
||||
|
||||
// Write implements the [io.Writer] interface.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob_write.html
|
||||
// https://sqlite.org/c3ref/blob_write.html
|
||||
func (b *Blob) Write(p []byte) (n int, err error) {
|
||||
defer b.c.arena.reset()
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.bytes(p)
|
||||
|
||||
r := b.c.call(b.c.api.blobWrite, uint64(b.handle),
|
||||
r := b.c.call("sqlite3_blob_write", uint64(b.handle),
|
||||
uint64(ptr), uint64(len(p)), uint64(b.offset))
|
||||
err = b.c.error(r[0])
|
||||
err = b.c.error(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -173,10 +173,13 @@ func (b *Blob) Write(p []byte) (n int, err error) {
|
||||
|
||||
// ReadFrom implements the [io.ReaderFrom] interface.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob_write.html
|
||||
// https://sqlite.org/c3ref/blob_write.html
|
||||
func (b *Blob) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
want := int64(1024 * 1024)
|
||||
avail := b.bytes - b.offset
|
||||
want := int64(65536)
|
||||
if l, ok := r.(*io.LimitedReader); ok && want > l.N {
|
||||
want = l.N
|
||||
}
|
||||
if want > avail {
|
||||
want = avail
|
||||
}
|
||||
@@ -184,16 +187,16 @@ func (b *Blob) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
want = 1
|
||||
}
|
||||
|
||||
ptr := b.c.new(uint64(want))
|
||||
defer b.c.free(ptr)
|
||||
defer b.c.arena.mark()()
|
||||
ptr := b.c.arena.new(uint64(want))
|
||||
|
||||
for {
|
||||
mem := util.View(b.c.mod, ptr, uint64(want))
|
||||
m, err := r.Read(mem[:want])
|
||||
if m > 0 {
|
||||
r := b.c.call(b.c.api.blobWrite, uint64(b.handle),
|
||||
r := b.c.call("sqlite3_blob_write", uint64(b.handle),
|
||||
uint64(ptr), uint64(m), uint64(b.offset))
|
||||
err := b.c.error(r[0])
|
||||
err := b.c.error(r)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
@@ -238,10 +241,10 @@ func (b *Blob) Seek(offset int64, whence int) (int64, error) {
|
||||
|
||||
// Reopen moves a BLOB handle to a new row of the same database table.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob_reopen.html
|
||||
// https://sqlite.org/c3ref/blob_reopen.html
|
||||
func (b *Blob) Reopen(row int64) error {
|
||||
err := b.c.error(b.c.call(b.c.api.blobReopen, uint64(b.handle), uint64(row))[0])
|
||||
b.bytes = int64(b.c.call(b.c.api.blobBytes, uint64(b.handle))[0])
|
||||
err := b.c.error(b.c.call("sqlite3_blob_reopen", uint64(b.handle), uint64(row)))
|
||||
b.bytes = int64(b.c.call("sqlite3_blob_bytes", uint64(b.handle)))
|
||||
b.offset = 0
|
||||
return err
|
||||
}
|
||||
|
||||
103
config.go
Normal file
103
config.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
// Config makes configuration changes to a database connection.
|
||||
// Only boolean configuration options are supported.
|
||||
// Called with no arg reads the current configuration value,
|
||||
// called with one arg sets and returns the new value.
|
||||
//
|
||||
// https://sqlite.org/c3ref/db_config.html
|
||||
func (c *Conn) Config(op DBConfig, arg ...bool) (bool, error) {
|
||||
defer c.arena.mark()()
|
||||
argsPtr := c.arena.new(2 * ptrlen)
|
||||
|
||||
var flag int
|
||||
switch {
|
||||
case len(arg) == 0:
|
||||
flag = -1
|
||||
case arg[0]:
|
||||
flag = 1
|
||||
}
|
||||
|
||||
util.WriteUint32(c.mod, argsPtr+0*ptrlen, uint32(flag))
|
||||
util.WriteUint32(c.mod, argsPtr+1*ptrlen, argsPtr)
|
||||
|
||||
r := c.call("sqlite3_db_config", uint64(c.handle),
|
||||
uint64(op), uint64(argsPtr))
|
||||
return util.ReadUint32(c.mod, argsPtr) != 0, c.error(r)
|
||||
}
|
||||
|
||||
// ConfigLog sets up the error logging callback for the connection.
|
||||
//
|
||||
// https://sqlite.org/errlog.html
|
||||
func (c *Conn) ConfigLog(cb func(code ExtendedErrorCode, msg string)) error {
|
||||
var enable uint64
|
||||
if cb != nil {
|
||||
enable = 1
|
||||
}
|
||||
r := c.call("sqlite3_config_log_go", enable)
|
||||
if err := c.error(r); err != nil {
|
||||
return err
|
||||
}
|
||||
c.log = cb
|
||||
return nil
|
||||
}
|
||||
|
||||
func logCallback(ctx context.Context, mod api.Module, _, iCode, zMsg uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.log != nil {
|
||||
msg := util.ReadString(mod, zMsg, _MAX_LENGTH)
|
||||
c.log(xErrorCode(iCode), msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Limit allows the size of various constructs to be
|
||||
// limited on a connection by connection basis.
|
||||
//
|
||||
// https://sqlite.org/c3ref/limit.html
|
||||
func (c *Conn) Limit(id LimitCategory, value int) int {
|
||||
r := c.call("sqlite3_limit", uint64(c.handle), uint64(id), uint64(value))
|
||||
return int(int32(r))
|
||||
}
|
||||
|
||||
// SetAuthorizer registers an authorizer callback with the database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/set_authorizer.html
|
||||
func (c *Conn) SetAuthorizer(cb func(action AuthorizerActionCode, name3rd, name4th, schema, nameInner string) AuthorizerReturnCode) error {
|
||||
var enable uint64
|
||||
if cb != nil {
|
||||
enable = 1
|
||||
}
|
||||
r := c.call("sqlite3_set_authorizer_go", uint64(c.handle), enable)
|
||||
if err := c.error(r); err != nil {
|
||||
return err
|
||||
}
|
||||
c.authorizer = cb
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action AuthorizerActionCode, zName3rd, zName4th, zSchema, zNameInner uint32) AuthorizerReturnCode {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.authorizer != nil {
|
||||
var name3rd, name4th, schema, nameInner string
|
||||
if zName3rd != 0 {
|
||||
name3rd = util.ReadString(mod, zName3rd, _MAX_NAME)
|
||||
}
|
||||
if zName4th != 0 {
|
||||
name4th = util.ReadString(mod, zName4th, _MAX_NAME)
|
||||
}
|
||||
if zSchema != 0 {
|
||||
schema = util.ReadString(mod, zSchema, _MAX_NAME)
|
||||
}
|
||||
if zNameInner != 0 {
|
||||
nameInner = util.ReadString(mod, zNameInner, _MAX_NAME)
|
||||
}
|
||||
return c.authorizer(action, name3rd, name4th, schema, nameInner)
|
||||
}
|
||||
return AUTH_OK
|
||||
}
|
||||
294
conn.go
294
conn.go
@@ -2,29 +2,34 @@ package sqlite3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
// Conn is a database connection handle.
|
||||
// A Conn is not safe for concurrent use by multiple goroutines.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/sqlite3.html
|
||||
// https://sqlite.org/c3ref/sqlite3.html
|
||||
type Conn struct {
|
||||
*module
|
||||
*sqlite
|
||||
|
||||
interrupt context.Context
|
||||
waiter chan struct{}
|
||||
pending *Stmt
|
||||
arena arena
|
||||
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()
|
||||
arena arena
|
||||
|
||||
handle uint32
|
||||
}
|
||||
@@ -39,9 +44,9 @@ func Open(filename string) (*Conn, error) {
|
||||
// If none of the required flags is used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
|
||||
// If a URI filename is used, PRAGMA statements to execute can be specified using "_pragma":
|
||||
//
|
||||
// sqlite3.Open("file:demo.db?_pragma=busy_timeout(10000)&_pragma=locking_mode(normal)")
|
||||
// sqlite3.Open("file:demo.db?_pragma=busy_timeout(10000)")
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/open.html
|
||||
// https://sqlite.org/c3ref/open.html
|
||||
func OpenFlags(filename string, flags OpenFlag) (*Conn, error) {
|
||||
if flags&(OPEN_READONLY|OPEN_READWRITE|OPEN_CREATE) == 0 {
|
||||
flags |= OPEN_READWRITE | OPEN_CREATE
|
||||
@@ -49,21 +54,22 @@ func OpenFlags(filename string, flags OpenFlag) (*Conn, error) {
|
||||
return newConn(filename, flags)
|
||||
}
|
||||
|
||||
type connKey struct{}
|
||||
|
||||
func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
|
||||
mod, err := instantiateModule()
|
||||
sqlite, err := instantiateSQLite()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if conn == nil {
|
||||
mod.close()
|
||||
} else {
|
||||
runtime.SetFinalizer(conn, util.Finalizer[Conn](3))
|
||||
sqlite.close()
|
||||
}
|
||||
}()
|
||||
|
||||
c := &Conn{module: mod}
|
||||
c := &Conn{sqlite: sqlite}
|
||||
c.arena = c.newArena(1024)
|
||||
c.ctx = context.WithValue(c.ctx, connKey{}, c)
|
||||
c.handle, err = c.openDB(filename, flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -72,15 +78,15 @@ func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
|
||||
}
|
||||
|
||||
func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
||||
defer c.arena.reset()
|
||||
defer c.arena.mark()()
|
||||
connPtr := c.arena.new(ptrlen)
|
||||
namePtr := c.arena.string(filename)
|
||||
|
||||
flags |= OPEN_EXRESCODE
|
||||
r := c.call(c.api.open, uint64(namePtr), uint64(connPtr), uint64(flags), 0)
|
||||
r := c.call("sqlite3_open_v2", uint64(namePtr), uint64(connPtr), uint64(flags), 0)
|
||||
|
||||
handle := util.ReadUint32(c.mod, connPtr)
|
||||
if err := c.module.error(r[0], handle); err != nil {
|
||||
if err := c.sqlite.error(r, handle); err != nil {
|
||||
c.closeDB(handle)
|
||||
return 0, err
|
||||
}
|
||||
@@ -92,14 +98,13 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
||||
for _, p := range query["_pragma"] {
|
||||
pragmas.WriteString(`PRAGMA `)
|
||||
pragmas.WriteString(p)
|
||||
pragmas.WriteByte(';')
|
||||
pragmas.WriteString(`;`)
|
||||
}
|
||||
}
|
||||
|
||||
c.arena.reset()
|
||||
pragmaPtr := c.arena.string(pragmas.String())
|
||||
r := c.call(c.api.exec, uint64(handle), uint64(pragmaPtr), 0, 0, 0)
|
||||
if err := c.module.error(r[0], handle, pragmas.String()); err != nil {
|
||||
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) {
|
||||
err = fmt.Errorf("sqlite3: invalid _pragma: %w", err)
|
||||
}
|
||||
@@ -107,13 +112,13 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
func (c *Conn) closeDB(handle uint32) {
|
||||
r := c.call(c.api.closeZombie, uint64(handle))
|
||||
if err := c.module.error(r[0], handle); err != nil {
|
||||
r := c.call("sqlite3_close_v2", uint64(handle))
|
||||
if err := c.sqlite.error(r, handle); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -126,37 +131,35 @@ func (c *Conn) closeDB(handle uint32) {
|
||||
//
|
||||
// It is safe to close a nil, zero or closed Conn.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/close.html
|
||||
// https://sqlite.org/c3ref/close.html
|
||||
func (c *Conn) Close() error {
|
||||
if c == nil || c.handle == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.SetInterrupt(context.Background())
|
||||
c.pending.Close()
|
||||
c.pending = nil
|
||||
|
||||
r := c.call(c.api.close, uint64(c.handle))
|
||||
if err := c.error(r[0]); err != nil {
|
||||
r := c.call("sqlite3_close", uint64(c.handle))
|
||||
if err := c.error(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.handle = 0
|
||||
runtime.SetFinalizer(c, nil)
|
||||
return c.module.close()
|
||||
return c.close()
|
||||
}
|
||||
|
||||
// Exec is a convenience function that allows an application to run
|
||||
// multiple statements of SQL without having to use a lot of code.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/exec.html
|
||||
// https://sqlite.org/c3ref/exec.html
|
||||
func (c *Conn) Exec(sql string) error {
|
||||
c.checkInterrupt()
|
||||
defer c.arena.reset()
|
||||
defer c.arena.mark()()
|
||||
sqlPtr := c.arena.string(sql)
|
||||
|
||||
r := c.call(c.api.exec, uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
|
||||
return c.error(r[0])
|
||||
r := c.call("sqlite3_exec", uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
|
||||
return c.error(r, sql)
|
||||
}
|
||||
|
||||
// Prepare calls [Conn.PrepareFlags] with no flags.
|
||||
@@ -169,60 +172,113 @@ func (c *Conn) Prepare(sql string) (stmt *Stmt, tail string, err error) {
|
||||
// If the input text contains no SQL (if the input is an empty string or a comment),
|
||||
// both stmt and err will be nil.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/prepare.html
|
||||
// https://sqlite.org/c3ref/prepare.html
|
||||
func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail string, err error) {
|
||||
if emptyStatement(sql) {
|
||||
return nil, "", nil
|
||||
if len(sql) > _MAX_LENGTH {
|
||||
return nil, "", TOOBIG
|
||||
}
|
||||
|
||||
defer c.arena.reset()
|
||||
defer c.arena.mark()()
|
||||
stmtPtr := c.arena.new(ptrlen)
|
||||
tailPtr := c.arena.new(ptrlen)
|
||||
sqlPtr := c.arena.string(sql)
|
||||
|
||||
r := c.call(c.api.prepare, uint64(c.handle),
|
||||
r := c.call("sqlite3_prepare_v3", uint64(c.handle),
|
||||
uint64(sqlPtr), uint64(len(sql)+1), uint64(flags),
|
||||
uint64(stmtPtr), uint64(tailPtr))
|
||||
|
||||
stmt = &Stmt{c: c}
|
||||
stmt.handle = util.ReadUint32(c.mod, stmtPtr)
|
||||
i := util.ReadUint32(c.mod, tailPtr)
|
||||
tail = sql[i-sqlPtr:]
|
||||
if sql := sql[util.ReadUint32(c.mod, tailPtr)-sqlPtr:]; sql != "" {
|
||||
tail = sql
|
||||
}
|
||||
|
||||
if err := c.error(r[0], sql); err != nil {
|
||||
if err := c.error(r, sql); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if stmt.handle == 0 {
|
||||
return nil, "", nil
|
||||
}
|
||||
return
|
||||
return stmt, tail, nil
|
||||
}
|
||||
|
||||
// DBName returns the schema name for n-th database on the database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/db_name.html
|
||||
func (c *Conn) DBName(n int) string {
|
||||
r := c.call("sqlite3_db_name", uint64(c.handle), uint64(n))
|
||||
|
||||
ptr := uint32(r)
|
||||
if ptr == 0 {
|
||||
return ""
|
||||
}
|
||||
return util.ReadString(c.mod, ptr, _MAX_NAME)
|
||||
}
|
||||
|
||||
// ReadOnly determines if a database is read-only.
|
||||
//
|
||||
// https://sqlite.org/c3ref/db_readonly.html
|
||||
func (c *Conn) ReadOnly(schema string) (ro bool, ok bool) {
|
||||
var ptr uint32
|
||||
if schema != "" {
|
||||
defer c.arena.mark()()
|
||||
ptr = c.arena.string(schema)
|
||||
}
|
||||
r := c.call("sqlite3_db_readonly", uint64(c.handle), uint64(ptr))
|
||||
return int32(r) > 0, int32(r) < 0
|
||||
}
|
||||
|
||||
// GetAutocommit tests the connection for auto-commit mode.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/get_autocommit.html
|
||||
// https://sqlite.org/c3ref/get_autocommit.html
|
||||
func (c *Conn) GetAutocommit() bool {
|
||||
r := c.call(c.api.autocommit, uint64(c.handle))
|
||||
return r[0] != 0
|
||||
r := c.call("sqlite3_get_autocommit", uint64(c.handle))
|
||||
return r != 0
|
||||
}
|
||||
|
||||
// LastInsertRowID returns the rowid of the most recent successful INSERT
|
||||
// on the database connection.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/last_insert_rowid.html
|
||||
// https://sqlite.org/c3ref/last_insert_rowid.html
|
||||
func (c *Conn) LastInsertRowID() int64 {
|
||||
r := c.call(c.api.lastRowid, uint64(c.handle))
|
||||
return int64(r[0])
|
||||
r := c.call("sqlite3_last_insert_rowid", uint64(c.handle))
|
||||
return int64(r)
|
||||
}
|
||||
|
||||
// SetLastInsertRowID allows the application to set the value returned by
|
||||
// [Conn.LastInsertRowID].
|
||||
//
|
||||
// https://sqlite.org/c3ref/set_last_insert_rowid.html
|
||||
func (c *Conn) SetLastInsertRowID(id int64) {
|
||||
c.call("sqlite3_set_last_insert_rowid", uint64(c.handle), uint64(id))
|
||||
}
|
||||
|
||||
// Changes returns the number of rows modified, inserted or deleted
|
||||
// by the most recently completed INSERT, UPDATE or DELETE statement
|
||||
// on the database connection.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/changes.html
|
||||
// https://sqlite.org/c3ref/changes.html
|
||||
func (c *Conn) Changes() int64 {
|
||||
r := c.call(c.api.changes, uint64(c.handle))
|
||||
return int64(r[0])
|
||||
r := c.call("sqlite3_changes64", uint64(c.handle))
|
||||
return int64(r)
|
||||
}
|
||||
|
||||
// TotalChanges returns the number of rows modified, inserted or deleted
|
||||
// by all INSERT, UPDATE or DELETE statements completed
|
||||
// since the database connection was opened.
|
||||
//
|
||||
// https://sqlite.org/c3ref/total_changes.html
|
||||
func (c *Conn) TotalChanges() int64 {
|
||||
r := c.call("sqlite3_total_changes64", uint64(c.handle))
|
||||
return int64(r)
|
||||
}
|
||||
|
||||
// ReleaseMemory frees memory used by a database connection.
|
||||
//
|
||||
// https://sqlite.org/c3ref/db_release_memory.html
|
||||
func (c *Conn) ReleaseMemory() error {
|
||||
r := c.call("sqlite3_db_release_memory", uint64(c.handle))
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
// SetInterrupt interrupts a long-running query when a context is done.
|
||||
@@ -238,72 +294,81 @@ func (c *Conn) Changes() int64 {
|
||||
//
|
||||
// SetInterrupt returns the old context assigned to the connection.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/interrupt.html
|
||||
// https://sqlite.org/c3ref/interrupt.html
|
||||
func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
|
||||
// Is a waiter running?
|
||||
if c.waiter != nil {
|
||||
c.waiter <- struct{}{} // Cancel the waiter.
|
||||
<-c.waiter // Wait for it to finish.
|
||||
c.waiter = nil
|
||||
// Is it the same context?
|
||||
if ctx == c.interrupt {
|
||||
return ctx
|
||||
}
|
||||
// Reset the pending statement.
|
||||
if c.pending != nil {
|
||||
c.pending.Reset()
|
||||
|
||||
// A busy SQL statement prevents SQLite from ignoring an interrupt
|
||||
// that comes before any other statements are started.
|
||||
if c.pending == nil {
|
||||
c.pending, _, _ = c.Prepare(`WITH RECURSIVE c(x) AS (VALUES(0) UNION ALL SELECT x FROM c) SELECT x FROM c`)
|
||||
}
|
||||
|
||||
old = c.interrupt
|
||||
c.interrupt = ctx
|
||||
if ctx == nil || ctx.Done() == nil {
|
||||
return old
|
||||
|
||||
if old != nil && old.Done() != nil && (ctx == nil || ctx.Err() == nil) {
|
||||
c.pending.Reset()
|
||||
}
|
||||
|
||||
// Creating an uncompleted SQL statement prevents SQLite from ignoring
|
||||
// an interrupt that comes before any other statements are started.
|
||||
if c.pending == nil {
|
||||
c.pending, _, _ = c.Prepare(`SELECT 1 UNION ALL SELECT 2`)
|
||||
if ctx != nil && ctx.Done() != nil {
|
||||
c.pending.Step()
|
||||
}
|
||||
c.pending.Step()
|
||||
|
||||
// Don't create the goroutine if we're already interrupted.
|
||||
// This happens frequently while restoring to a previously interrupted state.
|
||||
if c.checkInterrupt() {
|
||||
return old
|
||||
}
|
||||
|
||||
waiter := make(chan struct{})
|
||||
c.waiter = waiter
|
||||
go func() {
|
||||
select {
|
||||
case <-waiter: // Waiter was cancelled.
|
||||
break
|
||||
|
||||
case <-ctx.Done(): // Done was closed.
|
||||
const isInterruptedOffset = 280
|
||||
buf := util.View(c.mod, c.handle+isInterruptedOffset, 4)
|
||||
(*atomic.Uint32)(unsafe.Pointer(&buf[0])).Store(1)
|
||||
// Wait for the next call to SetInterrupt.
|
||||
<-waiter
|
||||
}
|
||||
|
||||
// Signal that the waiter has finished.
|
||||
waiter <- struct{}{}
|
||||
}()
|
||||
return old
|
||||
}
|
||||
|
||||
func (c *Conn) checkInterrupt() bool {
|
||||
if c.interrupt == nil || c.interrupt.Err() == nil {
|
||||
return false
|
||||
func (c *Conn) checkInterrupt() {
|
||||
if c.interrupt != nil && c.interrupt.Err() != nil {
|
||||
c.call("sqlite3_interrupt", uint64(c.handle))
|
||||
}
|
||||
const isInterruptedOffset = 280
|
||||
buf := util.View(c.mod, c.handle+isInterruptedOffset, 4)
|
||||
(*atomic.Uint32)(unsafe.Pointer(&buf[0])).Store(1)
|
||||
return true
|
||||
}
|
||||
|
||||
// Pragma executes a PRAGMA statement and returns any results.
|
||||
func progressCallback(ctx context.Context, mod api.Module, pDB uint32) uint32 {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.commit != nil {
|
||||
if c.interrupt != nil && c.interrupt.Err() != nil {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// BusyTimeout sets a busy timeout.
|
||||
//
|
||||
// https://www.sqlite.org/pragma.html
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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, count uint32) uint32 {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil {
|
||||
if retry := c.busy(int(count)); retry {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -319,23 +384,14 @@ func (c *Conn) Pragma(str string) ([]string, error) {
|
||||
}
|
||||
|
||||
func (c *Conn) error(rc uint64, sql ...string) error {
|
||||
return c.module.error(rc, c.handle, sql...)
|
||||
return c.sqlite.error(rc, c.handle, sql...)
|
||||
}
|
||||
|
||||
// DriverConn is implemented by the SQLite [database/sql] driver connection.
|
||||
//
|
||||
// It can be used to access advanced SQLite features like
|
||||
// [savepoints] and [incremental BLOB I/O].
|
||||
// It can be used to access SQLite features like [online backup].
|
||||
//
|
||||
// [savepoints]: https://www.sqlite.org/lang_savepoint.html
|
||||
// [incremental BLOB I/O]: https://www.sqlite.org/c3ref/blob_open.html
|
||||
// [online backup]: https://sqlite.org/backup.html
|
||||
type DriverConn interface {
|
||||
driver.ConnBeginTx
|
||||
driver.ExecerContext
|
||||
driver.ConnPrepareContext
|
||||
|
||||
Savepoint() Savepoint
|
||||
OpenBlob(db, table, column string, row int64, write bool) (*Blob, error)
|
||||
|
||||
SetInterrupt(ctx context.Context) (old context.Context)
|
||||
Raw() *Conn
|
||||
}
|
||||
|
||||
171
const.go
171
const.go
@@ -9,16 +9,18 @@ const (
|
||||
|
||||
_UTF8 = 1
|
||||
|
||||
_MAX_STRING = 512 // Used for short strings: names, error messages…
|
||||
|
||||
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
|
||||
_MAX_LENGTH = 1e9
|
||||
_MAX_SQL_LENGTH = 1e9
|
||||
_MAX_ALLOCATION_SIZE = 0x7ffffeff
|
||||
_MAX_FUNCTION_ARG = 100
|
||||
|
||||
ptrlen = 4
|
||||
)
|
||||
|
||||
// ErrorCode is a result code that [Error.Code] might return.
|
||||
//
|
||||
// https://www.sqlite.org/rescode.html
|
||||
// https://sqlite.org/rescode.html
|
||||
type ErrorCode uint8
|
||||
|
||||
const (
|
||||
@@ -54,7 +56,7 @@ const (
|
||||
|
||||
// ExtendedErrorCode is a result code that [Error.ExtendedCode] might return.
|
||||
//
|
||||
// https://www.sqlite.org/rescode.html
|
||||
// https://sqlite.org/rescode.html
|
||||
type (
|
||||
ExtendedErrorCode uint16
|
||||
xErrorCode = ExtendedErrorCode
|
||||
@@ -97,6 +99,7 @@ const (
|
||||
IOERR_ROLLBACK_ATOMIC ExtendedErrorCode = xErrorCode(IOERR) | (31 << 8)
|
||||
IOERR_DATA ExtendedErrorCode = xErrorCode(IOERR) | (32 << 8)
|
||||
IOERR_CORRUPTFS ExtendedErrorCode = xErrorCode(IOERR) | (33 << 8)
|
||||
IOERR_IN_PAGE ExtendedErrorCode = xErrorCode(IOERR) | (34 << 8)
|
||||
LOCKED_SHAREDCACHE ExtendedErrorCode = xErrorCode(LOCKED) | (1 << 8)
|
||||
LOCKED_VTAB ExtendedErrorCode = xErrorCode(LOCKED) | (2 << 8)
|
||||
BUSY_RECOVERY ExtendedErrorCode = xErrorCode(BUSY) | (1 << 8)
|
||||
@@ -139,7 +142,7 @@ const (
|
||||
|
||||
// OpenFlag is a flag for the [OpenFlags] function.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/c_open_autoproxy.html
|
||||
// https://sqlite.org/c3ref/c_open_autoproxy.html
|
||||
type OpenFlag uint32
|
||||
|
||||
const (
|
||||
@@ -158,7 +161,7 @@ const (
|
||||
|
||||
// PrepareFlag is a flag that can be passed to [Conn.PrepareFlags].
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/c_prepare_normalize.html
|
||||
// https://sqlite.org/c3ref/c_prepare_normalize.html
|
||||
type PrepareFlag uint32
|
||||
|
||||
const (
|
||||
@@ -167,9 +170,155 @@ const (
|
||||
PREPARE_NO_VTAB PrepareFlag = 0x04
|
||||
)
|
||||
|
||||
// FunctionFlag is a flag that can be passed to
|
||||
// [Conn.CreateFunction] and [Conn.CreateWindowFunction].
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_deterministic.html
|
||||
type FunctionFlag uint32
|
||||
|
||||
const (
|
||||
DETERMINISTIC FunctionFlag = 0x000000800
|
||||
DIRECTONLY FunctionFlag = 0x000080000
|
||||
SUBTYPE FunctionFlag = 0x000100000
|
||||
INNOCUOUS FunctionFlag = 0x000200000
|
||||
RESULT_SUBTYPE FunctionFlag = 0x001000000
|
||||
)
|
||||
|
||||
// StmtStatus name counter values associated with the [Stmt.Status] method.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_stmtstatus_counter.html
|
||||
type StmtStatus uint32
|
||||
|
||||
const (
|
||||
STMTSTATUS_FULLSCAN_STEP StmtStatus = 1
|
||||
STMTSTATUS_SORT StmtStatus = 2
|
||||
STMTSTATUS_AUTOINDEX StmtStatus = 3
|
||||
STMTSTATUS_VM_STEP StmtStatus = 4
|
||||
STMTSTATUS_REPREPARE StmtStatus = 5
|
||||
STMTSTATUS_RUN StmtStatus = 6
|
||||
STMTSTATUS_FILTER_MISS StmtStatus = 7
|
||||
STMTSTATUS_FILTER_HIT StmtStatus = 8
|
||||
STMTSTATUS_MEMUSED StmtStatus = 99
|
||||
)
|
||||
|
||||
// DBConfig are the available database connection configuration options.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_dbconfig_defensive.html
|
||||
type DBConfig uint32
|
||||
|
||||
const (
|
||||
// DBCONFIG_MAINDBNAME DBConfig = 1000
|
||||
// DBCONFIG_LOOKASIDE DBConfig = 1001
|
||||
DBCONFIG_ENABLE_FKEY DBConfig = 1002
|
||||
DBCONFIG_ENABLE_TRIGGER DBConfig = 1003
|
||||
DBCONFIG_ENABLE_FTS3_TOKENIZER DBConfig = 1004
|
||||
DBCONFIG_ENABLE_LOAD_EXTENSION DBConfig = 1005
|
||||
DBCONFIG_NO_CKPT_ON_CLOSE DBConfig = 1006
|
||||
DBCONFIG_ENABLE_QPSG DBConfig = 1007
|
||||
DBCONFIG_TRIGGER_EQP DBConfig = 1008
|
||||
DBCONFIG_RESET_DATABASE DBConfig = 1009
|
||||
DBCONFIG_DEFENSIVE DBConfig = 1010
|
||||
DBCONFIG_WRITABLE_SCHEMA DBConfig = 1011
|
||||
DBCONFIG_LEGACY_ALTER_TABLE DBConfig = 1012
|
||||
DBCONFIG_DQS_DML DBConfig = 1013
|
||||
DBCONFIG_DQS_DDL DBConfig = 1014
|
||||
DBCONFIG_ENABLE_VIEW DBConfig = 1015
|
||||
DBCONFIG_LEGACY_FILE_FORMAT DBConfig = 1016
|
||||
DBCONFIG_TRUSTED_SCHEMA DBConfig = 1017
|
||||
DBCONFIG_STMT_SCANSTATUS DBConfig = 1018
|
||||
DBCONFIG_REVERSE_SCANORDER DBConfig = 1019
|
||||
)
|
||||
|
||||
// LimitCategory are the available run-time limit categories.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_limit_attached.html
|
||||
type LimitCategory uint32
|
||||
|
||||
const (
|
||||
LIMIT_LENGTH LimitCategory = 0
|
||||
LIMIT_SQL_LENGTH LimitCategory = 1
|
||||
LIMIT_COLUMN LimitCategory = 2
|
||||
LIMIT_EXPR_DEPTH LimitCategory = 3
|
||||
LIMIT_COMPOUND_SELECT LimitCategory = 4
|
||||
LIMIT_VDBE_OP LimitCategory = 5
|
||||
LIMIT_FUNCTION_ARG LimitCategory = 6
|
||||
LIMIT_ATTACHED LimitCategory = 7
|
||||
LIMIT_LIKE_PATTERN_LENGTH LimitCategory = 8
|
||||
LIMIT_VARIABLE_NUMBER LimitCategory = 9
|
||||
LIMIT_TRIGGER_DEPTH LimitCategory = 10
|
||||
LIMIT_WORKER_THREADS LimitCategory = 11
|
||||
)
|
||||
|
||||
// AuthorizerActionCode are the integer action codes
|
||||
// that the authorizer callback may be passed.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_alter_table.html
|
||||
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 */
|
||||
)
|
||||
|
||||
// AuthorizerReturnCode are the integer codes
|
||||
// that the authorizer callback may return.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_deny.html
|
||||
type AuthorizerReturnCode uint32
|
||||
|
||||
const (
|
||||
AUTH_OK AuthorizerReturnCode = 0
|
||||
AUTH_DENY AuthorizerReturnCode = 1 /* Abort the SQL statement with an error */
|
||||
AUTH_IGNORE AuthorizerReturnCode = 2 /* Don't allow access, but don't generate an error */
|
||||
)
|
||||
|
||||
// TxnState are the allowed return values from [Conn.TxnState].
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_txn_none.html
|
||||
type TxnState uint32
|
||||
|
||||
const (
|
||||
TXN_NONE TxnState = 0
|
||||
TXN_READ TxnState = 1
|
||||
TXN_WRITE TxnState = 2
|
||||
)
|
||||
|
||||
// Datatype is a fundamental datatype of SQLite.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/c_blob.html
|
||||
// https://sqlite.org/c3ref/c_blob.html
|
||||
type Datatype uint32
|
||||
|
||||
const (
|
||||
@@ -182,18 +331,18 @@ const (
|
||||
|
||||
// String implements the [fmt.Stringer] interface.
|
||||
func (t Datatype) String() string {
|
||||
const name = "INTEGERFLOATTEXTBLOBNULL"
|
||||
const name = "INTEGERFLOATEXTBLOBNULL"
|
||||
switch t {
|
||||
case INTEGER:
|
||||
return name[0:7]
|
||||
case FLOAT:
|
||||
return name[7:12]
|
||||
case TEXT:
|
||||
return name[12:16]
|
||||
return name[11:15]
|
||||
case BLOB:
|
||||
return name[16:20]
|
||||
return name[15:19]
|
||||
case NULL:
|
||||
return name[20:24]
|
||||
return name[19:23]
|
||||
}
|
||||
return strconv.FormatUint(uint64(t), 10)
|
||||
}
|
||||
|
||||
229
context.go
Normal file
229
context.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Context is the context in which an SQL function executes.
|
||||
// An SQLite [Context] is in no way related to a Go [context.Context].
|
||||
//
|
||||
// https://sqlite.org/c3ref/context.html
|
||||
type Context struct {
|
||||
c *Conn
|
||||
handle uint32
|
||||
}
|
||||
|
||||
// Conn returns the database connection of the
|
||||
// [Conn.CreateFunction] or [Conn.CreateWindowFunction]
|
||||
// routines that originally registered the application defined function.
|
||||
//
|
||||
// https://sqlite.org/c3ref/context_db_handle.html
|
||||
func (ctx Context) Conn() *Conn {
|
||||
return ctx.c
|
||||
}
|
||||
|
||||
// SetAuxData saves metadata for argument n of the function.
|
||||
//
|
||||
// https://sqlite.org/c3ref/get_auxdata.html
|
||||
func (ctx Context) SetAuxData(n int, data any) {
|
||||
ptr := util.AddHandle(ctx.c.ctx, data)
|
||||
ctx.c.call("sqlite3_set_auxdata_go", uint64(ctx.handle), uint64(n), uint64(ptr))
|
||||
}
|
||||
|
||||
// GetAuxData returns metadata for argument n of the function.
|
||||
//
|
||||
// https://sqlite.org/c3ref/get_auxdata.html
|
||||
func (ctx Context) GetAuxData(n int) any {
|
||||
ptr := uint32(ctx.c.call("sqlite3_get_auxdata", uint64(ctx.handle), uint64(n)))
|
||||
return util.GetHandle(ctx.c.ctx, ptr)
|
||||
}
|
||||
|
||||
// ResultBool sets the result of the function to a bool.
|
||||
// SQLite does not have a separate boolean storage class.
|
||||
// Instead, boolean values are stored as integers 0 (false) and 1 (true).
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultBool(value bool) {
|
||||
var i int64
|
||||
if value {
|
||||
i = 1
|
||||
}
|
||||
ctx.ResultInt64(i)
|
||||
}
|
||||
|
||||
// ResultInt sets the result of the function to an int.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultInt(value int) {
|
||||
ctx.ResultInt64(int64(value))
|
||||
}
|
||||
|
||||
// ResultInt64 sets the result of the function to an int64.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultInt64(value int64) {
|
||||
ctx.c.call("sqlite3_result_int64",
|
||||
uint64(ctx.handle), uint64(value))
|
||||
}
|
||||
|
||||
// ResultFloat sets the result of the function to a float64.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultFloat(value float64) {
|
||||
ctx.c.call("sqlite3_result_double",
|
||||
uint64(ctx.handle), math.Float64bits(value))
|
||||
}
|
||||
|
||||
// ResultText sets the result of the function to a string.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultText(value string) {
|
||||
ptr := ctx.c.newString(value)
|
||||
ctx.c.call("sqlite3_result_text64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
|
||||
uint64(ctx.c.freer), _UTF8)
|
||||
}
|
||||
|
||||
// ResultRawText sets the text result of the function to a []byte.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultRawText(value []byte) {
|
||||
ptr := ctx.c.newBytes(value)
|
||||
ctx.c.call("sqlite3_result_text64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
|
||||
uint64(ctx.c.freer), _UTF8)
|
||||
}
|
||||
|
||||
// ResultBlob sets the result of the function to a []byte.
|
||||
// Returning a nil slice is the same as calling [Context.ResultNull].
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultBlob(value []byte) {
|
||||
ptr := ctx.c.newBytes(value)
|
||||
ctx.c.call("sqlite3_result_blob64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
|
||||
uint64(ctx.c.freer))
|
||||
}
|
||||
|
||||
// ResultZeroBlob sets the result of the function to a zero-filled, length n BLOB.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultZeroBlob(n int64) {
|
||||
ctx.c.call("sqlite3_result_zeroblob64",
|
||||
uint64(ctx.handle), uint64(n))
|
||||
}
|
||||
|
||||
// ResultNull sets the result of the function to NULL.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultNull() {
|
||||
ctx.c.call("sqlite3_result_null",
|
||||
uint64(ctx.handle))
|
||||
}
|
||||
|
||||
// ResultTime sets the result of the function to a [time.Time].
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultTime(value time.Time, format TimeFormat) {
|
||||
if format == TimeFormatDefault {
|
||||
ctx.resultRFC3339Nano(value)
|
||||
return
|
||||
}
|
||||
switch v := format.Encode(value).(type) {
|
||||
case string:
|
||||
ctx.ResultText(v)
|
||||
case int64:
|
||||
ctx.ResultInt64(v)
|
||||
case float64:
|
||||
ctx.ResultFloat(v)
|
||||
default:
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx Context) resultRFC3339Nano(value time.Time) {
|
||||
const maxlen = uint64(len(time.RFC3339Nano)) + 5
|
||||
|
||||
ptr := ctx.c.new(maxlen)
|
||||
buf := util.View(ctx.c.mod, ptr, maxlen)
|
||||
buf = value.AppendFormat(buf[:0], time.RFC3339Nano)
|
||||
|
||||
ctx.c.call("sqlite3_result_text64",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(buf)),
|
||||
uint64(ctx.c.freer), _UTF8)
|
||||
}
|
||||
|
||||
// ResultPointer sets the result of the function to NULL, just like [Context.ResultNull],
|
||||
// except that it also associates ptr with that NULL value such that it can be retrieved
|
||||
// within an application-defined SQL function using [Value.Pointer].
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultPointer(ptr any) {
|
||||
valPtr := util.AddHandle(ctx.c.ctx, ptr)
|
||||
ctx.c.call("sqlite3_result_pointer_go", uint64(valPtr))
|
||||
}
|
||||
|
||||
// ResultJSON sets the result of the function to the JSON encoding of value.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultJSON(value any) {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
ctx.ResultRawText(data)
|
||||
}
|
||||
|
||||
// ResultValue sets the result of the function to a copy of [Value].
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultValue(value Value) {
|
||||
if value.c != ctx.c {
|
||||
ctx.ResultError(MISUSE)
|
||||
return
|
||||
}
|
||||
ctx.c.call("sqlite3_result_value",
|
||||
uint64(ctx.handle), uint64(value.handle))
|
||||
}
|
||||
|
||||
// ResultError sets the result of the function an error.
|
||||
//
|
||||
// https://sqlite.org/c3ref/result_blob.html
|
||||
func (ctx Context) ResultError(err error) {
|
||||
if errors.Is(err, NOMEM) {
|
||||
ctx.c.call("sqlite3_result_error_nomem", uint64(ctx.handle))
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, TOOBIG) {
|
||||
ctx.c.call("sqlite3_result_error_toobig", uint64(ctx.handle))
|
||||
return
|
||||
}
|
||||
|
||||
msg, code := errorCode(err, _OK)
|
||||
if msg != "" {
|
||||
defer ctx.c.arena.mark()()
|
||||
ptr := ctx.c.arena.string(msg)
|
||||
ctx.c.call("sqlite3_result_error",
|
||||
uint64(ctx.handle), uint64(ptr), uint64(len(msg)))
|
||||
}
|
||||
if code != _OK {
|
||||
ctx.c.call("sqlite3_result_error_code",
|
||||
uint64(ctx.handle), uint64(code))
|
||||
}
|
||||
}
|
||||
|
||||
// VTabNoChange may return true if a column is being fetched as part
|
||||
// of an update during which the column value will not change.
|
||||
//
|
||||
// https://sqlite.org/c3ref/vtab_nochange.html
|
||||
func (ctx Context) VTabNoChange() bool {
|
||||
r := ctx.c.call("sqlite3_vtab_nochange", uint64(ctx.handle))
|
||||
return r != 0
|
||||
}
|
||||
478
driver/driver.go
478
driver/driver.go
@@ -12,90 +12,187 @@
|
||||
//
|
||||
// sql.Open("sqlite3", "file:demo.db?_txlock=immediate")
|
||||
//
|
||||
// Possible values are: "deferred", "immediate", "exclusive".
|
||||
// A [read-only] transaction is always "deferred", regardless of "_txlock".
|
||||
//
|
||||
// The time encoding/decoding format can be specified using "_timefmt":
|
||||
//
|
||||
// sql.Open("sqlite3", "file:demo.db?_timefmt=sqlite")
|
||||
//
|
||||
// Possible values are: "auto" (the default), "sqlite", "rfc3339";
|
||||
// "auto" encodes as RFC 3339 and decodes any [format] supported by SQLite;
|
||||
// "sqlite" encodes as SQLite and decodes any [format] supported by SQLite;
|
||||
// "rfc3339" encodes and decodes RFC 3339 only.
|
||||
//
|
||||
// [PRAGMA] statements can be specified using "_pragma":
|
||||
//
|
||||
// sql.Open("sqlite3", "file:demo.db?_pragma=busy_timeout(10000)&_pragma=locking_mode(normal)")
|
||||
// sql.Open("sqlite3", "file:demo.db?_pragma=busy_timeout(10000)")
|
||||
//
|
||||
// If no PRAGMAs are specified, a busy timeout of 1 minute
|
||||
// and normal locking mode are used.
|
||||
// If no PRAGMAs are specified, a busy timeout of 1 minute is set.
|
||||
//
|
||||
// [URI]: https://www.sqlite.org/uri.html
|
||||
// [PRAGMA]: https://www.sqlite.org/pragma.html
|
||||
// [TRANSACTION]: https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions
|
||||
// Order matters:
|
||||
// busy timeout and locking mode should be the first PRAGMAs set, in that order.
|
||||
//
|
||||
// [URI]: https://sqlite.org/uri.html
|
||||
// [PRAGMA]: https://sqlite.org/pragma.html
|
||||
// [format]: https://sqlite.org/lang_datefunc.html#time_values
|
||||
// [TRANSACTION]: https://sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions
|
||||
// [read-only]: https://pkg.go.dev/database/sql#TxOptions
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// This variable can be replaced with -ldflags:
|
||||
//
|
||||
// go build -ldflags="-X github.com/ncruces/go-sqlite3/driver.driverName=sqlite"
|
||||
var driverName = "sqlite3"
|
||||
|
||||
func init() {
|
||||
sql.Register("sqlite3", sqlite{})
|
||||
if driverName != "" {
|
||||
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].
|
||||
func Open(dataSourceName string, init func(*sqlite3.Conn) error) (*sql.DB, error) {
|
||||
c, err := newConnector(dataSourceName, init)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sql.OpenDB(c), nil
|
||||
}
|
||||
|
||||
type sqlite struct{}
|
||||
|
||||
func (sqlite) Open(name string) (_ driver.Conn, err error) {
|
||||
var c conn
|
||||
c.conn, err = sqlite3.Open(name)
|
||||
func (sqlite) Open(name string) (driver.Conn, error) {
|
||||
c, err := newConnector(name, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Connect(context.Background())
|
||||
}
|
||||
|
||||
var pragmas []string
|
||||
func (sqlite) OpenConnector(name string) (driver.Connector, error) {
|
||||
return newConnector(name, nil)
|
||||
}
|
||||
|
||||
func newConnector(name string, init func(*sqlite3.Conn) error) (*connector, error) {
|
||||
c := connector{name: name, init: init}
|
||||
|
||||
var txlock, timefmt string
|
||||
if strings.HasPrefix(name, "file:") {
|
||||
if _, after, ok := strings.Cut(name, "?"); ok {
|
||||
query, _ := url.ParseQuery(after)
|
||||
|
||||
switch s := query.Get("_txlock"); s {
|
||||
case "":
|
||||
c.txBegin = "BEGIN"
|
||||
case "deferred", "immediate", "exclusive":
|
||||
c.txBegin = "BEGIN " + s
|
||||
default:
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("sqlite3: invalid _txlock: %s", s)
|
||||
query, err := url.ParseQuery(after)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pragmas = query["_pragma"]
|
||||
txlock = query.Get("_txlock")
|
||||
timefmt = query.Get("_timefmt")
|
||||
c.pragmas = query.Has("_pragma")
|
||||
}
|
||||
}
|
||||
if len(pragmas) == 0 {
|
||||
err := c.conn.Exec(`
|
||||
PRAGMA locking_mode=normal;
|
||||
PRAGMA busy_timeout=60000;
|
||||
`)
|
||||
|
||||
switch txlock {
|
||||
case "":
|
||||
c.txBegin = "BEGIN"
|
||||
case "deferred", "immediate", "exclusive":
|
||||
c.txBegin = "BEGIN " + txlock
|
||||
default:
|
||||
return nil, fmt.Errorf("sqlite3: invalid _txlock: %s", txlock)
|
||||
}
|
||||
|
||||
switch timefmt {
|
||||
case "":
|
||||
c.tmRead = sqlite3.TimeFormatAuto
|
||||
c.tmWrite = sqlite3.TimeFormatDefault
|
||||
case "sqlite":
|
||||
c.tmRead = sqlite3.TimeFormatAuto
|
||||
c.tmWrite = sqlite3.TimeFormat3
|
||||
case "rfc3339":
|
||||
c.tmRead = sqlite3.TimeFormatDefault
|
||||
c.tmWrite = sqlite3.TimeFormatDefault
|
||||
default:
|
||||
c.tmRead = sqlite3.TimeFormat(timefmt)
|
||||
c.tmWrite = sqlite3.TimeFormat(timefmt)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
type connector struct {
|
||||
init func(*sqlite3.Conn) error
|
||||
name string
|
||||
txBegin string
|
||||
tmRead sqlite3.TimeFormat
|
||||
tmWrite sqlite3.TimeFormat
|
||||
pragmas bool
|
||||
}
|
||||
|
||||
func (n *connector) Driver() driver.Driver {
|
||||
return sqlite{}
|
||||
}
|
||||
|
||||
func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
|
||||
c := &conn{
|
||||
txBegin: n.txBegin,
|
||||
tmRead: n.tmRead,
|
||||
tmWrite: n.tmWrite,
|
||||
}
|
||||
|
||||
c.Conn, err = sqlite3.Open(n.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
c.reusable = true
|
||||
} else {
|
||||
s, _, err := c.conn.Prepare(`
|
||||
SELECT * FROM
|
||||
PRAGMA_locking_mode,
|
||||
PRAGMA_query_only;
|
||||
`)
|
||||
}()
|
||||
|
||||
old := c.Conn.SetInterrupt(ctx)
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
|
||||
if !n.pragmas {
|
||||
err = c.Conn.BusyTimeout(60 * time.Second)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
if s.Step() {
|
||||
c.reusable = s.ColumnText(0) == "normal"
|
||||
c.readOnly = s.ColumnRawText(1)[0] // 0 or 1
|
||||
}
|
||||
if n.init != nil {
|
||||
err = n.init(c.Conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if n.pragmas || n.init != nil {
|
||||
s, _, err := c.Conn.Prepare(`PRAGMA query_only`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.Step() && s.ColumnBool(0) {
|
||||
c.readOnly = '1'
|
||||
} else {
|
||||
c.readOnly = '0'
|
||||
}
|
||||
err = s.Close()
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -103,35 +200,32 @@ func (sqlite) Open(name string) (_ driver.Conn, err error) {
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
conn *sqlite3.Conn
|
||||
*sqlite3.Conn
|
||||
txBegin string
|
||||
txCommit string
|
||||
txRollback string
|
||||
reusable bool
|
||||
tmRead sqlite3.TimeFormat
|
||||
tmWrite sqlite3.TimeFormat
|
||||
readOnly byte
|
||||
}
|
||||
|
||||
var (
|
||||
// Ensure these interfaces are implemented:
|
||||
_ driver.ExecerContext = conn{}
|
||||
_ driver.ConnBeginTx = conn{}
|
||||
_ driver.Validator = conn{}
|
||||
_ sqlite3.DriverConn = conn{}
|
||||
_ driver.ConnPrepareContext = &conn{}
|
||||
_ driver.ExecerContext = &conn{}
|
||||
_ driver.ConnBeginTx = &conn{}
|
||||
_ sqlite3.DriverConn = &conn{}
|
||||
)
|
||||
|
||||
func (c conn) Close() error {
|
||||
return c.conn.Close()
|
||||
func (c *conn) Raw() *sqlite3.Conn {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
func (c conn) IsValid() bool {
|
||||
return c.reusable
|
||||
}
|
||||
|
||||
func (c conn) Begin() (driver.Tx, error) {
|
||||
func (c *conn) Begin() (driver.Tx, error) {
|
||||
return c.BeginTx(context.Background(), driver.TxOptions{})
|
||||
}
|
||||
|
||||
func (c conn) BeginTx(_ context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
||||
func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
||||
txBegin := c.txBegin
|
||||
c.txCommit = `COMMIT`
|
||||
c.txRollback = `ROLLBACK`
|
||||
@@ -140,10 +234,10 @@ func (c conn) BeginTx(_ context.Context, opts driver.TxOptions) (driver.Tx, erro
|
||||
txBegin = `
|
||||
BEGIN deferred;
|
||||
PRAGMA query_only=on`
|
||||
c.txCommit = `
|
||||
c.txRollback = `
|
||||
ROLLBACK;
|
||||
PRAGMA query_only=` + string(c.readOnly)
|
||||
c.txRollback = c.txCommit
|
||||
c.txCommit = c.txRollback
|
||||
}
|
||||
|
||||
switch opts.Isolation {
|
||||
@@ -155,99 +249,97 @@ func (c conn) BeginTx(_ context.Context, opts driver.TxOptions) (driver.Tx, erro
|
||||
break
|
||||
}
|
||||
|
||||
err := c.conn.Exec(txBegin)
|
||||
old := c.Conn.SetInterrupt(ctx)
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
|
||||
err := c.Conn.Exec(txBegin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c conn) Commit() error {
|
||||
err := c.conn.Exec(c.txCommit)
|
||||
if err != nil && !c.conn.GetAutocommit() {
|
||||
func (c *conn) Commit() error {
|
||||
err := c.Conn.Exec(c.txCommit)
|
||||
if err != nil && !c.Conn.GetAutocommit() {
|
||||
c.Rollback()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c conn) Rollback() error {
|
||||
return c.conn.Exec(c.txRollback)
|
||||
func (c *conn) Rollback() error {
|
||||
err := c.Conn.Exec(c.txRollback)
|
||||
if errors.Is(err, sqlite3.INTERRUPT) {
|
||||
old := c.Conn.SetInterrupt(context.Background())
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
err = c.Conn.Exec(c.txRollback)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c conn) Prepare(query string) (driver.Stmt, error) {
|
||||
s, tail, err := c.conn.Prepare(query)
|
||||
func (c *conn) Prepare(query string) (driver.Stmt, error) {
|
||||
return c.PrepareContext(context.Background(), query)
|
||||
}
|
||||
|
||||
func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
|
||||
old := c.Conn.SetInterrupt(ctx)
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
|
||||
s, tail, err := c.Conn.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tail != "" {
|
||||
// Check if the tail contains any SQL.
|
||||
st, _, err := c.conn.Prepare(tail)
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return nil, err
|
||||
}
|
||||
if st != nil {
|
||||
s.Close()
|
||||
st.Close()
|
||||
return nil, util.TailErr
|
||||
}
|
||||
s.Close()
|
||||
return nil, util.TailErr
|
||||
}
|
||||
return stmt{s, c.conn}, nil
|
||||
return &stmt{Stmt: s, tmRead: c.tmRead, tmWrite: c.tmWrite}, nil
|
||||
}
|
||||
|
||||
func (c conn) PrepareContext(_ context.Context, query string) (driver.Stmt, error) {
|
||||
return c.Prepare(query)
|
||||
}
|
||||
|
||||
func (c conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||
func (c *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||
if len(args) != 0 {
|
||||
// Slow path.
|
||||
return nil, driver.ErrSkip
|
||||
}
|
||||
|
||||
old := c.conn.SetInterrupt(ctx)
|
||||
defer c.conn.SetInterrupt(old)
|
||||
if savept, ok := ctx.(*saveptCtx); ok {
|
||||
// Called from driver.Savepoint.
|
||||
savept.Savepoint = c.Conn.Savepoint()
|
||||
return resultRowsAffected(0), nil
|
||||
}
|
||||
|
||||
err := c.conn.Exec(query)
|
||||
old := c.Conn.SetInterrupt(ctx)
|
||||
defer c.Conn.SetInterrupt(old)
|
||||
|
||||
err := c.Conn.Exec(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newResult(c.conn), nil
|
||||
return newResult(c.Conn), nil
|
||||
}
|
||||
|
||||
func (c conn) Savepoint() sqlite3.Savepoint {
|
||||
return c.conn.Savepoint()
|
||||
}
|
||||
|
||||
func (c conn) OpenBlob(db, table, column string, row int64, write bool) (*sqlite3.Blob, error) {
|
||||
return c.conn.OpenBlob(db, table, column, row, write)
|
||||
}
|
||||
|
||||
func (c conn) SetInterrupt(ctx context.Context) (old context.Context) {
|
||||
return c.conn.SetInterrupt(ctx)
|
||||
func (*conn) CheckNamedValue(arg *driver.NamedValue) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type stmt struct {
|
||||
stmt *sqlite3.Stmt
|
||||
conn *sqlite3.Conn
|
||||
*sqlite3.Stmt
|
||||
tmWrite sqlite3.TimeFormat
|
||||
tmRead sqlite3.TimeFormat
|
||||
}
|
||||
|
||||
var (
|
||||
// Ensure these interfaces are implemented:
|
||||
_ driver.StmtExecContext = stmt{}
|
||||
_ driver.StmtQueryContext = stmt{}
|
||||
_ driver.NamedValueChecker = stmt{}
|
||||
_ driver.StmtExecContext = &stmt{}
|
||||
_ driver.StmtQueryContext = &stmt{}
|
||||
_ driver.NamedValueChecker = &stmt{}
|
||||
)
|
||||
|
||||
func (s stmt) Close() error {
|
||||
return s.stmt.Close()
|
||||
}
|
||||
|
||||
func (s stmt) NumInput() int {
|
||||
n := s.stmt.BindCount()
|
||||
func (s *stmt) NumInput() int {
|
||||
n := s.Stmt.BindCount()
|
||||
for i := 1; i <= n; i++ {
|
||||
if s.stmt.BindName(i) != "" {
|
||||
if s.Stmt.BindName(i) != "" {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
@@ -255,36 +347,45 @@ func (s stmt) NumInput() int {
|
||||
}
|
||||
|
||||
// Deprecated: use ExecContext instead.
|
||||
func (s stmt) Exec(args []driver.Value) (driver.Result, error) {
|
||||
func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {
|
||||
return s.ExecContext(context.Background(), namedValues(args))
|
||||
}
|
||||
|
||||
// Deprecated: use QueryContext instead.
|
||||
func (s stmt) Query(args []driver.Value) (driver.Rows, error) {
|
||||
func (s *stmt) Query(args []driver.Value) (driver.Rows, error) {
|
||||
return s.QueryContext(context.Background(), namedValues(args))
|
||||
}
|
||||
|
||||
func (s stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
|
||||
// Use QueryContext to setup bindings.
|
||||
// No need to close rows: that simply resets the statement, exec does the same.
|
||||
_, err := s.QueryContext(ctx, args)
|
||||
func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
|
||||
err := s.setupBindings(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.stmt.Exec()
|
||||
old := s.Stmt.Conn().SetInterrupt(ctx)
|
||||
defer s.Stmt.Conn().SetInterrupt(old)
|
||||
|
||||
err = s.Stmt.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newResult(s.conn), nil
|
||||
return newResult(s.Stmt.Conn()), nil
|
||||
}
|
||||
|
||||
func (s stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
||||
err := s.stmt.ClearBindings()
|
||||
func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
||||
err := s.setupBindings(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rows{ctx: ctx, stmt: s}, nil
|
||||
}
|
||||
|
||||
func (s *stmt) setupBindings(args []driver.NamedValue) error {
|
||||
err := s.Stmt.ClearBindings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ids [3]int
|
||||
for _, arg := range args {
|
||||
@@ -293,7 +394,7 @@ func (s stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (drive
|
||||
ids = append(ids, arg.Ordinal)
|
||||
} else {
|
||||
for _, prefix := range []string{":", "@", "$"} {
|
||||
if id := s.stmt.BindIndex(prefix + arg.Name); id != 0 {
|
||||
if id := s.Stmt.BindIndex(prefix + arg.Name); id != 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
@@ -302,39 +403,44 @@ func (s stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (drive
|
||||
for _, id := range ids {
|
||||
switch a := arg.Value.(type) {
|
||||
case bool:
|
||||
err = s.stmt.BindBool(id, a)
|
||||
err = s.Stmt.BindBool(id, a)
|
||||
case int:
|
||||
err = s.stmt.BindInt(id, a)
|
||||
err = s.Stmt.BindInt(id, a)
|
||||
case int64:
|
||||
err = s.stmt.BindInt64(id, a)
|
||||
err = s.Stmt.BindInt64(id, a)
|
||||
case float64:
|
||||
err = s.stmt.BindFloat(id, a)
|
||||
err = s.Stmt.BindFloat(id, a)
|
||||
case string:
|
||||
err = s.stmt.BindText(id, a)
|
||||
err = s.Stmt.BindText(id, a)
|
||||
case []byte:
|
||||
err = s.stmt.BindBlob(id, a)
|
||||
err = s.Stmt.BindBlob(id, a)
|
||||
case sqlite3.ZeroBlob:
|
||||
err = s.stmt.BindZeroBlob(id, int64(a))
|
||||
err = s.Stmt.BindZeroBlob(id, int64(a))
|
||||
case time.Time:
|
||||
err = s.stmt.BindTime(id, a, sqlite3.TimeFormatDefault)
|
||||
err = s.Stmt.BindTime(id, a, s.tmWrite)
|
||||
case util.JSON:
|
||||
err = s.Stmt.BindJSON(id, a.Value)
|
||||
case util.PointerUnwrap:
|
||||
err = s.Stmt.BindPointer(id, util.UnwrapPointer(a))
|
||||
case nil:
|
||||
err = s.stmt.BindNull(id)
|
||||
err = s.Stmt.BindNull(id)
|
||||
default:
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rows{ctx, s.stmt, s.conn}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s stmt) CheckNamedValue(arg *driver.NamedValue) error {
|
||||
func (s *stmt) CheckNamedValue(arg *driver.NamedValue) error {
|
||||
switch arg.Value.(type) {
|
||||
case bool, int, int64, float64, string, []byte,
|
||||
sqlite3.ZeroBlob, time.Time, nil:
|
||||
time.Time, sqlite3.ZeroBlob,
|
||||
util.JSON, util.PointerUnwrap,
|
||||
nil:
|
||||
return nil
|
||||
default:
|
||||
return driver.ErrSkip
|
||||
@@ -373,55 +479,93 @@ func (r resultRowsAffected) RowsAffected() (int64, error) {
|
||||
}
|
||||
|
||||
type rows struct {
|
||||
ctx context.Context
|
||||
stmt *sqlite3.Stmt
|
||||
conn *sqlite3.Conn
|
||||
ctx context.Context
|
||||
*stmt
|
||||
names []string
|
||||
types []string
|
||||
}
|
||||
|
||||
func (r rows) Close() error {
|
||||
return r.stmt.Reset()
|
||||
func (r *rows) Close() error {
|
||||
r.Stmt.ClearBindings()
|
||||
return r.Stmt.Reset()
|
||||
}
|
||||
|
||||
func (r rows) Columns() []string {
|
||||
count := r.stmt.ColumnCount()
|
||||
columns := make([]string, count)
|
||||
for i := range columns {
|
||||
columns[i] = r.stmt.ColumnName(i)
|
||||
func (r *rows) Columns() []string {
|
||||
if r.names == nil {
|
||||
count := r.Stmt.ColumnCount()
|
||||
r.names = make([]string, count)
|
||||
for i := range r.names {
|
||||
r.names[i] = r.Stmt.ColumnName(i)
|
||||
}
|
||||
}
|
||||
return columns
|
||||
return r.names
|
||||
}
|
||||
|
||||
func (r rows) Next(dest []driver.Value) error {
|
||||
old := r.conn.SetInterrupt(r.ctx)
|
||||
defer r.conn.SetInterrupt(old)
|
||||
func (r *rows) declType(index int) string {
|
||||
if r.types == nil {
|
||||
count := r.Stmt.ColumnCount()
|
||||
r.types = make([]string, count)
|
||||
for i := range r.types {
|
||||
r.types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
|
||||
}
|
||||
}
|
||||
return r.types[index]
|
||||
}
|
||||
|
||||
if !r.stmt.Step() {
|
||||
if err := r.stmt.Err(); err != nil {
|
||||
func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
|
||||
decltype := r.declType(index)
|
||||
if len := len(decltype); len > 0 && decltype[len-1] == ')' {
|
||||
if i := strings.LastIndexByte(decltype, '('); i >= 0 {
|
||||
decltype = decltype[:i]
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(decltype)
|
||||
}
|
||||
|
||||
func (r *rows) Next(dest []driver.Value) error {
|
||||
old := r.Stmt.Conn().SetInterrupt(r.ctx)
|
||||
defer r.Stmt.Conn().SetInterrupt(old)
|
||||
|
||||
if !r.Stmt.Step() {
|
||||
if err := r.Stmt.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
data := unsafe.Slice((*any)(unsafe.SliceData(dest)), len(dest))
|
||||
err := r.Stmt.Columns(data)
|
||||
for i := range dest {
|
||||
switch r.stmt.ColumnType(i) {
|
||||
case sqlite3.INTEGER:
|
||||
dest[i] = r.stmt.ColumnInt64(i)
|
||||
case sqlite3.FLOAT:
|
||||
dest[i] = r.stmt.ColumnFloat(i)
|
||||
case sqlite3.BLOB:
|
||||
dest[i] = r.stmt.ColumnRawBlob(i)
|
||||
case sqlite3.TEXT:
|
||||
dest[i] = stringOrTime(r.stmt.ColumnRawText(i))
|
||||
case sqlite3.NULL:
|
||||
if buf, ok := dest[i].([]byte); ok {
|
||||
dest[i] = buf[0:0]
|
||||
} else {
|
||||
dest[i] = nil
|
||||
if t, ok := r.decodeTime(i, dest[i]); ok {
|
||||
dest[i] = t
|
||||
continue
|
||||
}
|
||||
if s, ok := dest[i].(string); ok {
|
||||
t, ok := maybeTime(s)
|
||||
if ok {
|
||||
dest[i] = t
|
||||
}
|
||||
default:
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
}
|
||||
|
||||
return r.stmt.Err()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rows) decodeTime(i int, v any) (_ time.Time, _ bool) {
|
||||
if r.tmRead == sqlite3.TimeFormatDefault {
|
||||
return
|
||||
}
|
||||
switch r.declType(i) {
|
||||
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
|
||||
// maybe
|
||||
default:
|
||||
return
|
||||
}
|
||||
switch v.(type) {
|
||||
case int64, float64, string:
|
||||
// maybe
|
||||
default:
|
||||
return
|
||||
}
|
||||
t, err := r.tmRead.Decode(v)
|
||||
return t, err == nil
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"math"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
@@ -113,13 +115,7 @@ func Test_Open_txLock(t *testing.T) {
|
||||
func Test_Open_txLock_invalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sql.Open("sqlite3", "file::memory:?_txlock=xclusive")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Conn(context.TODO())
|
||||
_, err := sql.Open("sqlite3", "file::memory:?_txlock=xclusive")
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
}
|
||||
@@ -185,12 +181,6 @@ func Test_Prepare(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stmt, err := db.Prepare(`SELECT 1; -- HERE`)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
var serr *sqlite3.Error
|
||||
_, err = db.Prepare(`SELECT`)
|
||||
if err == nil {
|
||||
@@ -206,18 +196,14 @@ func Test_Prepare(t *testing.T) {
|
||||
t.Error("got message:", got)
|
||||
}
|
||||
|
||||
_, err = db.Prepare(`SELECT 1; `)
|
||||
if err.Error() != string(util.TailErr) {
|
||||
t.Error("want tailErr")
|
||||
}
|
||||
|
||||
_, err = db.Prepare(`SELECT 1; SELECT`)
|
||||
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: incomplete input` {
|
||||
t.Error("got message:", got)
|
||||
if err.Error() != string(util.TailErr) {
|
||||
t.Error("want tailErr")
|
||||
}
|
||||
|
||||
_, err = db.Prepare(`SELECT 1; SELECT 2`)
|
||||
@@ -296,6 +282,7 @@ func Test_QueryRow_blob_null(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
want := [][]byte{nil, {0xca, 0xfe}, {0xba, 0xbe}, nil}
|
||||
for i := 0; rows.Next(); i++ {
|
||||
@@ -309,3 +296,39 @@ func Test_QueryRow_blob_null(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_time(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, fmt := range []string{"auto", "sqlite", "rfc3339", time.ANSIC} {
|
||||
t.Run(fmt, func(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", "file::memory:?_timefmt="+url.QueryEscape(fmt))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
twosday := time.Date(2022, 2, 22, 22, 22, 22, 0, time.UTC)
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS test (at DATETIME)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`INSERT INTO test VALUES (?)`, twosday)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var got time.Time
|
||||
err = db.QueryRow(`SELECT * FROM test`).Scan(&got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !got.Equal(twosday) {
|
||||
t.Errorf("got: %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
65
driver/json_test.go
Normal file
65
driver/json_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package driver_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func Example_json() {
|
||||
db, err := driver.Open("file:/test.db?vfs=memdb", nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
cart_id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
cart TEXT
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
type CartItem struct {
|
||||
ItemID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Quantity int `json:"quantity,omitempty"`
|
||||
Price int `json:"price,omitempty"`
|
||||
}
|
||||
|
||||
type Cart struct {
|
||||
Items []CartItem `json:"items"`
|
||||
}
|
||||
|
||||
_, err = db.Exec(`INSERT INTO orders (user_id, cart) VALUES (?, ?)`, 123, sqlite3.JSON(Cart{
|
||||
[]CartItem{
|
||||
{ItemID: "111", Name: "T-shirt", Quantity: 1, Price: 250},
|
||||
{ItemID: "222", Name: "Trousers", Quantity: 1, Price: 600},
|
||||
},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var total string
|
||||
err = db.QueryRow(`
|
||||
SELECT total(json_each.value -> 'price')
|
||||
FROM orders, json_each(cart -> 'items')
|
||||
WHERE cart_id = last_insert_rowid()
|
||||
`).Scan(&total)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("total:", total)
|
||||
// Output:
|
||||
// total: 850
|
||||
}
|
||||
27
driver/savepoint.go
Normal file
27
driver/savepoint.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Savepoint establishes a new transaction savepoint.
|
||||
//
|
||||
// https://sqlite.org/lang_savepoint.html
|
||||
func Savepoint(tx *sql.Tx) sqlite3.Savepoint {
|
||||
var ctx saveptCtx
|
||||
tx.ExecContext(&ctx, "")
|
||||
return ctx.Savepoint
|
||||
}
|
||||
|
||||
type saveptCtx struct{ sqlite3.Savepoint }
|
||||
|
||||
func (*saveptCtx) Deadline() (deadline time.Time, ok bool) { return }
|
||||
|
||||
func (*saveptCtx) Done() <-chan struct{} { return nil }
|
||||
|
||||
func (*saveptCtx) Err() error { return nil }
|
||||
|
||||
func (*saveptCtx) Value(key any) any { return nil }
|
||||
88
driver/savepoint_test.go
Normal file
88
driver/savepoint_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package driver_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
func ExampleSavepoint() {
|
||||
db, err := driver.Open("file:/test.db?vfs=memdb", nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
stmt, err := tx.Prepare(`INSERT INTO users (id, name) VALUES (?, ?)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec(0, "go")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(1, "zig")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
savept := driver.Savepoint(tx)
|
||||
|
||||
_, err = stmt.Exec(2, "whatever")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = savept.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(3, "rust")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
rows, err := db.Query(`SELECT id, name FROM users`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id, name string
|
||||
err = rows.Scan(&id, &name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%s %s\n", id, name)
|
||||
}
|
||||
// Output:
|
||||
// 0 go
|
||||
// 1 zig
|
||||
// 3 rust
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -9,23 +8,24 @@ import (
|
||||
// if it roundtrips back to the same string.
|
||||
// This way times can be persisted to, and recovered from, the database,
|
||||
// but if a string is needed, [database/sql] will recover the same string.
|
||||
func stringOrTime(text []byte) driver.Value {
|
||||
func maybeTime(text string) (_ time.Time, _ bool) {
|
||||
// Weed out (some) values that can't possibly be
|
||||
// [time.RFC3339Nano] timestamps.
|
||||
if len(text) < len("2006-01-02T15:04:05Z") {
|
||||
return string(text)
|
||||
return
|
||||
}
|
||||
if len(text) > len(time.RFC3339Nano) {
|
||||
return string(text)
|
||||
return
|
||||
}
|
||||
if text[4] != '-' || text[10] != 'T' || text[16] != ':' {
|
||||
return string(text)
|
||||
return
|
||||
}
|
||||
|
||||
// Slow path.
|
||||
date, err := time.Parse(time.RFC3339Nano, string(text))
|
||||
if err == nil && date.Format(time.RFC3339Nano) == string(text) {
|
||||
return date
|
||||
var buf [len(time.RFC3339Nano)]byte
|
||||
date, err := time.Parse(time.RFC3339Nano, text)
|
||||
if err == nil && text == string(date.AppendFormat(buf[:0], time.RFC3339Nano)) {
|
||||
return date, true
|
||||
}
|
||||
return string(text)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -22,26 +22,18 @@ func Fuzz_stringOrTime_1(f *testing.F) {
|
||||
f.Add("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
|
||||
|
||||
f.Fuzz(func(t *testing.T, str string) {
|
||||
value := stringOrTime([]byte(str))
|
||||
|
||||
switch v := value.(type) {
|
||||
case time.Time:
|
||||
v, ok := maybeTime(str)
|
||||
if ok {
|
||||
// Make sure times round-trip to the same string:
|
||||
// https://pkg.go.dev/database/sql#Rows.Scan
|
||||
if v.Format(time.RFC3339Nano) != str {
|
||||
t.Fatalf("did not round-trip: %q", str)
|
||||
}
|
||||
case string:
|
||||
if v != str {
|
||||
t.Fatalf("did not round-trip: %q", str)
|
||||
}
|
||||
|
||||
} else {
|
||||
date, err := time.Parse(time.RFC3339Nano, str)
|
||||
if err == nil && date.Format(time.RFC3339Nano) == str {
|
||||
t.Fatalf("would round-trip: %q", str)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("invalid type %T: %q", v, str)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -49,38 +41,34 @@ func Fuzz_stringOrTime_1(f *testing.F) {
|
||||
// This checks that any [time.Time] can be recovered as a [time.Time],
|
||||
// with nanosecond accuracy, and preserving any timezone offset.
|
||||
func Fuzz_stringOrTime_2(f *testing.F) {
|
||||
f.Add(0, 0)
|
||||
f.Add(0, 1)
|
||||
f.Add(0, -1)
|
||||
f.Add(0, 999_999_999)
|
||||
f.Add(0, 1_000_000_000)
|
||||
f.Add(7956915742, 222_222_222) // twosday
|
||||
f.Add(639095955742, 222_222_222) // twosday, year 22222AD
|
||||
f.Add(-763421161058, 222_222_222) // twosday, year 22222BC
|
||||
f.Add(int64(0), int64(0))
|
||||
f.Add(int64(0), int64(1))
|
||||
f.Add(int64(0), int64(-1))
|
||||
f.Add(int64(0), int64(999_999_999))
|
||||
f.Add(int64(0), int64(1_000_000_000))
|
||||
f.Add(int64(7956915742), int64(222_222_222)) // twosday
|
||||
f.Add(int64(639095955742), int64(222_222_222)) // twosday, year 22222AD
|
||||
f.Add(int64(-763421161058), int64(222_222_222)) // twosday, year 22222BC
|
||||
|
||||
checkTime := func(t *testing.T, date time.Time) {
|
||||
value := stringOrTime([]byte(date.Format(time.RFC3339Nano)))
|
||||
|
||||
switch v := value.(type) {
|
||||
case time.Time:
|
||||
checkTime := func(t testing.TB, date time.Time) {
|
||||
v, ok := maybeTime(date.Format(time.RFC3339Nano))
|
||||
if ok {
|
||||
// Make sure times round-trip to the same time:
|
||||
if !v.Equal(date) {
|
||||
t.Fatalf("did not round-trip: %v", date)
|
||||
}
|
||||
// Make with the same zone offset:
|
||||
// With the same zone offset:
|
||||
_, off1 := v.Zone()
|
||||
_, off2 := date.Zone()
|
||||
if off1 != off2 {
|
||||
t.Fatalf("did not round-trip: %v", date)
|
||||
}
|
||||
case string:
|
||||
} else {
|
||||
t.Fatalf("was not recovered: %v", date)
|
||||
default:
|
||||
t.Fatalf("invalid type %T: %v", v, date)
|
||||
}
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, sec, nsec int) {
|
||||
f.Fuzz(func(t *testing.T, sec, nsec int64) {
|
||||
// Reduce the search space.
|
||||
if 1e12 < sec || sec < -1e12 {
|
||||
// Dates before 29000BC and after 33000AD; I think we're safe.
|
||||
@@ -91,7 +79,7 @@ func Fuzz_stringOrTime_2(f *testing.F) {
|
||||
return
|
||||
}
|
||||
|
||||
unix := time.Unix(int64(sec), int64(nsec))
|
||||
unix := time.Unix(sec, nsec)
|
||||
checkTime(t, unix)
|
||||
checkTime(t, unix.UTC())
|
||||
checkTime(t, unix.In(time.FixedZone("", -8*3600)))
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package sqlite3_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
func ExampleDriverConn() {
|
||||
var err error
|
||||
db, err = sql.Open("sqlite3", "demo.db")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer os.Remove("demo.db")
|
||||
defer db.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
conn, err := db.Conn(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
res, err := conn.ExecContext(ctx, `INSERT INTO test VALUES (?)`, sqlite3.ZeroBlob(11))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = conn.Raw(func(driverConn any) error {
|
||||
conn := driverConn.(sqlite3.DriverConn)
|
||||
savept := conn.Savepoint()
|
||||
defer savept.Release(&err)
|
||||
|
||||
blob, err := conn.OpenBlob("main", "test", "col", id, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer blob.Close()
|
||||
|
||||
_, err = fmt.Fprint(blob, "Hello BLOB!")
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var msg string
|
||||
err = conn.QueryRowContext(ctx, `SELECT col FROM test`).Scan(&msg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(msg)
|
||||
// Output:
|
||||
// Hello BLOB!
|
||||
}
|
||||
@@ -1,23 +1,26 @@
|
||||
# Embeddable WASM build of SQLite
|
||||
|
||||
This folder includes an embeddable WASM build of SQLite 3.42.0 for use with
|
||||
This folder includes an embeddable WASM build of SQLite 3.45.1 for use with
|
||||
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
|
||||
|
||||
The following optional features are compiled in:
|
||||
- [math functions](https://www.sqlite.org/lang_mathfunc.html)
|
||||
- [FTS3/4](https://www.sqlite.org/fts3.html)/[5](https://www.sqlite.org/fts5.html)
|
||||
- [JSON](https://www.sqlite.org/json1.html)
|
||||
- [R*Tree](https://www.sqlite.org/rtree.html)
|
||||
- [GeoPoly](https://www.sqlite.org/geopoly.html)
|
||||
- [math functions](https://sqlite.org/lang_mathfunc.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)
|
||||
- [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)
|
||||
- [regexp](https://github.com/sqlite/sqlite/blob/master/ext/misc/regexp.c)
|
||||
- [series](https://github.com/sqlite/sqlite/blob/master/ext/misc/series.c)
|
||||
- [uint](https://github.com/sqlite/sqlite/blob/master/ext/misc/uint.c)
|
||||
- [uuid](https://github.com/sqlite/sqlite/blob/master/ext/misc/uuid.c)
|
||||
- [time](../sqlite3/time.c)
|
||||
|
||||
See the [configuration options](../sqlite3/sqlite_cfg.h).
|
||||
See the [configuration options](../sqlite3/sqlite_cfg.h),
|
||||
and [patches](../sqlite3) applied.
|
||||
|
||||
Built using [`wasi-sdk`](https://github.com/WebAssembly/wasi-sdk),
|
||||
and [`binaryen`](https://github.com/WebAssembly/binaryen).
|
||||
@@ -4,24 +4,28 @@ set -euo pipefail
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
ROOT=../
|
||||
BINARYEN="$ROOT/tools/binaryen-version_112/bin"
|
||||
WASI_SDK="$ROOT/tools/wasi-sdk-20.0/bin"
|
||||
BINARYEN="$ROOT/tools/binaryen-version_117/bin"
|
||||
WASI_SDK="$ROOT/tools/wasi-sdk-21.0/bin"
|
||||
|
||||
"$WASI_SDK/clang" --target=wasm32-wasi -flto -g0 -O2 \
|
||||
"$WASI_SDK/clang" --target=wasm32-wasi -std=c17 -flto -g0 -O2 \
|
||||
-Wall -Wextra -Wno-unused-parameter \
|
||||
-o sqlite3.wasm "$ROOT/sqlite3/main.c" \
|
||||
-I"$ROOT/sqlite3" \
|
||||
-mexec-model=reactor \
|
||||
-mmutable-globals \
|
||||
-msimd128 -mmutable-globals \
|
||||
-mbulk-memory -mreference-types \
|
||||
-mnontrapping-fptoint -msign-ext \
|
||||
-fno-stack-protector -fno-stack-clash-protection \
|
||||
-Wl,--initial-memory=327680 \
|
||||
-Wl,--stack-first \
|
||||
-Wl,--import-undefined \
|
||||
-D_HAVE_SQLITE_CONFIG_H \
|
||||
$(awk '{print "-Wl,--export="$0}' exports.txt)
|
||||
|
||||
trap 'rm -f sqlite3.tmp' EXIT
|
||||
"$BINARYEN/wasm-ctor-eval" -g -c _initialize sqlite3.wasm -o sqlite3.tmp
|
||||
"$BINARYEN/wasm-opt" -g -O2 sqlite3.tmp -o sqlite3.wasm \
|
||||
--enable-multivalue --enable-mutable-globals \
|
||||
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
|
||||
sqlite3.tmp -o sqlite3.wasm \
|
||||
--enable-simd --enable-mutable-globals --enable-multivalue \
|
||||
--enable-bulk-memory --enable-reference-types \
|
||||
--enable-nontrapping-float-to-int --enable-sign-ext
|
||||
@@ -1,47 +1,117 @@
|
||||
free
|
||||
malloc
|
||||
malloc_destructor
|
||||
sqlite3_errcode
|
||||
sqlite3_errstr
|
||||
sqlite3_errmsg
|
||||
sqlite3_error_offset
|
||||
sqlite3_open_v2
|
||||
sqlite3_close
|
||||
sqlite3_close_v2
|
||||
sqlite3_prepare_v3
|
||||
sqlite3_finalize
|
||||
sqlite3_reset
|
||||
sqlite3_step
|
||||
sqlite3_exec
|
||||
sqlite3_clear_bindings
|
||||
sqlite3_anycollseq_init
|
||||
sqlite3_backup_finish
|
||||
sqlite3_backup_init
|
||||
sqlite3_backup_pagecount
|
||||
sqlite3_backup_remaining
|
||||
sqlite3_backup_step
|
||||
sqlite3_bind_blob64
|
||||
sqlite3_bind_double
|
||||
sqlite3_bind_int64
|
||||
sqlite3_bind_null
|
||||
sqlite3_bind_parameter_count
|
||||
sqlite3_bind_parameter_index
|
||||
sqlite3_bind_parameter_name
|
||||
sqlite3_bind_null
|
||||
sqlite3_bind_int64
|
||||
sqlite3_bind_double
|
||||
sqlite3_bind_pointer_go
|
||||
sqlite3_bind_text64
|
||||
sqlite3_bind_blob64
|
||||
sqlite3_bind_value
|
||||
sqlite3_bind_zeroblob64
|
||||
sqlite3_column_count
|
||||
sqlite3_column_name
|
||||
sqlite3_column_type
|
||||
sqlite3_column_int64
|
||||
sqlite3_column_double
|
||||
sqlite3_column_text
|
||||
sqlite3_blob_bytes
|
||||
sqlite3_blob_close
|
||||
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
|
||||
sqlite3_close_v2
|
||||
sqlite3_collation_needed_go
|
||||
sqlite3_column_blob
|
||||
sqlite3_column_bytes
|
||||
sqlite3_blob_open
|
||||
sqlite3_blob_close
|
||||
sqlite3_blob_bytes
|
||||
sqlite3_blob_read
|
||||
sqlite3_blob_write
|
||||
sqlite3_blob_reopen
|
||||
sqlite3_column_count
|
||||
sqlite3_column_decltype
|
||||
sqlite3_column_double
|
||||
sqlite3_column_int64
|
||||
sqlite3_column_name
|
||||
sqlite3_column_text
|
||||
sqlite3_column_type
|
||||
sqlite3_column_value
|
||||
sqlite3_columns_go
|
||||
sqlite3_commit_hook_go
|
||||
sqlite3_config_log_go
|
||||
sqlite3_create_aggregate_function_go
|
||||
sqlite3_create_collation_go
|
||||
sqlite3_create_function_go
|
||||
sqlite3_create_module_go
|
||||
sqlite3_create_window_function_go
|
||||
sqlite3_db_config
|
||||
sqlite3_db_name
|
||||
sqlite3_db_readonly
|
||||
sqlite3_db_release_memory
|
||||
sqlite3_declare_vtab
|
||||
sqlite3_errcode
|
||||
sqlite3_errmsg
|
||||
sqlite3_error_offset
|
||||
sqlite3_errstr
|
||||
sqlite3_exec
|
||||
sqlite3_finalize
|
||||
sqlite3_get_autocommit
|
||||
sqlite3_get_auxdata
|
||||
sqlite3_interrupt
|
||||
sqlite3_last_insert_rowid
|
||||
sqlite3_changes64
|
||||
sqlite3_backup_init
|
||||
sqlite3_backup_step
|
||||
sqlite3_backup_finish
|
||||
sqlite3_backup_remaining
|
||||
sqlite3_backup_pagecount
|
||||
sqlite3_limit
|
||||
sqlite3_open_v2
|
||||
sqlite3_overload_function
|
||||
sqlite3_prepare_v3
|
||||
sqlite3_progress_handler_go
|
||||
sqlite3_reset
|
||||
sqlite3_result_blob64
|
||||
sqlite3_result_double
|
||||
sqlite3_result_error
|
||||
sqlite3_result_error_code
|
||||
sqlite3_result_error_nomem
|
||||
sqlite3_result_error_toobig
|
||||
sqlite3_result_int64
|
||||
sqlite3_result_null
|
||||
sqlite3_result_pointer_go
|
||||
sqlite3_result_text64
|
||||
sqlite3_result_value
|
||||
sqlite3_result_zeroblob64
|
||||
sqlite3_rollback_hook_go
|
||||
sqlite3_set_authorizer_go
|
||||
sqlite3_set_auxdata_go
|
||||
sqlite3_set_last_insert_rowid
|
||||
sqlite3_step
|
||||
sqlite3_stmt_busy
|
||||
sqlite3_stmt_readonly
|
||||
sqlite3_stmt_status
|
||||
sqlite3_total_changes64
|
||||
sqlite3_txn_state
|
||||
sqlite3_update_hook_go
|
||||
sqlite3_uri_key
|
||||
sqlite3_uri_parameter
|
||||
sqlite3_value_blob
|
||||
sqlite3_value_bytes
|
||||
sqlite3_value_double
|
||||
sqlite3_value_dup
|
||||
sqlite3_value_free
|
||||
sqlite3_value_int64
|
||||
sqlite3_value_nochange
|
||||
sqlite3_value_numeric_type
|
||||
sqlite3_value_pointer_go
|
||||
sqlite3_value_text
|
||||
sqlite3_value_type
|
||||
sqlite3_vtab_collation
|
||||
sqlite3_vtab_config_go
|
||||
sqlite3_vtab_distinct
|
||||
sqlite3_vtab_in
|
||||
sqlite3_vtab_in_first
|
||||
sqlite3_vtab_in_next
|
||||
sqlite3_vtab_nochange
|
||||
sqlite3_vtab_on_conflict
|
||||
sqlite3_vtab_rhs_value
|
||||
Binary file not shown.
57
error.go
57
error.go
@@ -1,6 +1,7 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -9,7 +10,7 @@ import (
|
||||
|
||||
// Error wraps an SQLite Error Code.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/errcode.html
|
||||
// https://sqlite.org/c3ref/errcode.html
|
||||
type Error struct {
|
||||
str string
|
||||
msg string
|
||||
@@ -19,14 +20,14 @@ type Error struct {
|
||||
|
||||
// Code returns the primary error code for this error.
|
||||
//
|
||||
// https://www.sqlite.org/rescode.html
|
||||
// https://sqlite.org/rescode.html
|
||||
func (e *Error) Code() ErrorCode {
|
||||
return ErrorCode(e.code)
|
||||
}
|
||||
|
||||
// ExtendedCode returns the extended error code for this error.
|
||||
//
|
||||
// https://www.sqlite.org/rescode.html
|
||||
// https://sqlite.org/rescode.html
|
||||
func (e *Error) ExtendedCode() ExtendedErrorCode {
|
||||
return ExtendedErrorCode(e.code)
|
||||
}
|
||||
@@ -43,8 +44,7 @@ func (e *Error) Error() string {
|
||||
}
|
||||
|
||||
if e.msg != "" {
|
||||
b.WriteByte(':')
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(": ")
|
||||
b.WriteString(e.msg)
|
||||
}
|
||||
|
||||
@@ -68,6 +68,19 @@ func (e *Error) Is(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// As converts this error to an [ErrorCode] or [ExtendedErrorCode].
|
||||
func (e *Error) As(err any) bool {
|
||||
switch c := err.(type) {
|
||||
case *ErrorCode:
|
||||
*c = e.Code()
|
||||
return true
|
||||
case *ExtendedErrorCode:
|
||||
*c = e.ExtendedCode()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Temporary returns true for [BUSY] errors.
|
||||
func (e *Error) Temporary() bool {
|
||||
return e.Code() == BUSY
|
||||
@@ -104,6 +117,15 @@ func (e ExtendedErrorCode) Is(err error) bool {
|
||||
return ok && c == ErrorCode(e)
|
||||
}
|
||||
|
||||
// As converts this error to an [ErrorCode].
|
||||
func (e ExtendedErrorCode) As(err any) bool {
|
||||
c, ok := err.(*ErrorCode)
|
||||
if ok {
|
||||
*c = ErrorCode(e)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// Temporary returns true for [BUSY] errors.
|
||||
func (e ExtendedErrorCode) Temporary() bool {
|
||||
return ErrorCode(e) == BUSY
|
||||
@@ -113,3 +135,28 @@ func (e ExtendedErrorCode) Temporary() bool {
|
||||
func (e ExtendedErrorCode) Timeout() bool {
|
||||
return e == BUSY_TIMEOUT
|
||||
}
|
||||
|
||||
func errorCode(err error, def ErrorCode) (msg string, code uint32) {
|
||||
switch code := err.(type) {
|
||||
case nil:
|
||||
return "", _OK
|
||||
case ErrorCode:
|
||||
return "", uint32(code)
|
||||
case xErrorCode:
|
||||
return "", uint32(code)
|
||||
case *Error:
|
||||
return code.msg, uint32(code.code)
|
||||
}
|
||||
|
||||
var ecode ErrorCode
|
||||
var xcode xErrorCode
|
||||
switch {
|
||||
case errors.As(err, &xcode):
|
||||
code = uint32(xcode)
|
||||
case errors.As(err, &ecode):
|
||||
code = uint32(ecode)
|
||||
default:
|
||||
code = uint32(def)
|
||||
}
|
||||
return err.Error(), code
|
||||
}
|
||||
|
||||
@@ -18,22 +18,36 @@ func Test_assertErr(t *testing.T) {
|
||||
func TestError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := Error{code: 0x8080}
|
||||
if rc := err.Code(); rc != 0x80 {
|
||||
t.Errorf("got %#x, want 0x80", rc)
|
||||
var ecode ErrorCode
|
||||
var xcode xErrorCode
|
||||
err := &Error{code: 0x8080}
|
||||
if !errors.As(err, &err) {
|
||||
t.Fatal("want true")
|
||||
}
|
||||
if !errors.Is(&err, ErrorCode(0x80)) {
|
||||
if ecode := err.Code(); ecode != 0x80 {
|
||||
t.Errorf("got %#x, want 0x80", uint8(ecode))
|
||||
}
|
||||
if ok := errors.As(err, &ecode); !ok || ecode != ErrorCode(0x80) {
|
||||
t.Errorf("got %#x, want 0x80", uint8(ecode))
|
||||
}
|
||||
if !errors.Is(err, ErrorCode(0x80)) {
|
||||
t.Errorf("want true")
|
||||
}
|
||||
if rc := err.ExtendedCode(); rc != 0x8080 {
|
||||
t.Errorf("got %#x, want 0x8080", rc)
|
||||
if xcode := err.ExtendedCode(); xcode != 0x8080 {
|
||||
t.Errorf("got %#x, want 0x8080", uint16(xcode))
|
||||
}
|
||||
if !errors.Is(&err, ExtendedErrorCode(0x8080)) {
|
||||
if ok := errors.As(err, &xcode); !ok || xcode != xErrorCode(0x8080) {
|
||||
t.Errorf("got %#x, want 0x8080", uint16(xcode))
|
||||
}
|
||||
if !errors.Is(err, xErrorCode(0x8080)) {
|
||||
t.Errorf("want true")
|
||||
}
|
||||
if s := err.Error(); s != "sqlite3: 32896" {
|
||||
t.Errorf("got %q", s)
|
||||
}
|
||||
if ok := errors.As(err.ExtendedCode(), &ecode); !ok || ecode != ErrorCode(0x80) {
|
||||
t.Errorf("got %#x, want 0x80", uint8(ecode))
|
||||
}
|
||||
if !errors.Is(err.ExtendedCode(), ErrorCode(0x80)) {
|
||||
t.Errorf("want true")
|
||||
}
|
||||
@@ -121,8 +135,8 @@ func Test_ErrorCode_Error(t *testing.T) {
|
||||
// Test all error codes.
|
||||
for i := 0; i == int(ErrorCode(i)); i++ {
|
||||
want := "sqlite3: "
|
||||
r := db.call(db.api.errstr, uint64(i))
|
||||
want += util.ReadString(db.mod, uint32(r[0]), _MAX_STRING)
|
||||
r := db.call("sqlite3_errstr", uint64(i))
|
||||
want += util.ReadString(db.mod, uint32(r), _MAX_NAME)
|
||||
|
||||
got := ErrorCode(i).Error()
|
||||
if got != want {
|
||||
@@ -143,8 +157,8 @@ func Test_ExtendedErrorCode_Error(t *testing.T) {
|
||||
// Test all extended error codes.
|
||||
for i := 0; i == int(ExtendedErrorCode(i)); i++ {
|
||||
want := "sqlite3: "
|
||||
r := db.call(db.api.errstr, uint64(i))
|
||||
want += util.ReadString(db.mod, uint32(r[0]), _MAX_STRING)
|
||||
r := db.call("sqlite3_errstr", uint64(i))
|
||||
want += util.ReadString(db.mod, uint32(r), _MAX_NAME)
|
||||
|
||||
got := ExtendedErrorCode(i).Error()
|
||||
if got != want {
|
||||
|
||||
136
ext/array/array.go
Normal file
136
ext/array/array.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Package array provides the array table-valued SQL function.
|
||||
//
|
||||
// https://sqlite.org/carray.html
|
||||
package array
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Register registers the array single-argument, table-valued SQL function.
|
||||
// The argument must be bound to a Go slice or array of
|
||||
// ints, floats, bools, strings or byte slices,
|
||||
// using [sqlite3.BindPointer] or [sqlite3.Pointer].
|
||||
func Register(db *sqlite3.Conn) {
|
||||
sqlite3.CreateModule[array](db, "array", nil,
|
||||
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (array, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(value, array HIDDEN)`)
|
||||
return array{}, err
|
||||
})
|
||||
}
|
||||
|
||||
type array struct{}
|
||||
|
||||
func (array) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
for i, cst := range idx.Constraint {
|
||||
if cst.Column == 1 && cst.Op == sqlite3.INDEX_CONSTRAINT_EQ && cst.Usable {
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
Omit: true,
|
||||
ArgvIndex: 1,
|
||||
}
|
||||
idx.EstimatedCost = 1
|
||||
idx.EstimatedRows = 100
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
|
||||
func (array) Open() (sqlite3.VTabCursor, error) {
|
||||
return &cursor{}, nil
|
||||
}
|
||||
|
||||
type cursor struct {
|
||||
array reflect.Value
|
||||
rowID int
|
||||
}
|
||||
|
||||
func (c *cursor) EOF() bool {
|
||||
return c.rowID >= c.array.Len()
|
||||
}
|
||||
|
||||
func (c *cursor) Next() error {
|
||||
c.rowID++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) RowID() (int64, error) {
|
||||
return int64(c.rowID), nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
|
||||
if n != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := c.array.Index(c.rowID)
|
||||
k := v.Kind()
|
||||
|
||||
if k == reflect.Interface {
|
||||
if v.IsNil() {
|
||||
ctx.ResultNull()
|
||||
return nil
|
||||
}
|
||||
v = v.Elem()
|
||||
k = v.Kind()
|
||||
}
|
||||
|
||||
switch {
|
||||
case v.CanInt():
|
||||
ctx.ResultInt64(v.Int())
|
||||
|
||||
case v.CanUint():
|
||||
i64 := int64(v.Uint())
|
||||
if i64 < 0 {
|
||||
return fmt.Errorf("array: integer element overflow:%.0w %d", sqlite3.MISMATCH, v.Uint())
|
||||
}
|
||||
ctx.ResultInt64(i64)
|
||||
|
||||
case v.CanFloat():
|
||||
ctx.ResultFloat(v.Float())
|
||||
|
||||
case k == reflect.Bool:
|
||||
ctx.ResultBool(v.Bool())
|
||||
|
||||
case k == reflect.String:
|
||||
ctx.ResultText(v.String())
|
||||
|
||||
case (k == reflect.Slice || k == reflect.Array && v.CanAddr()) &&
|
||||
v.Type().Elem().Kind() == reflect.Uint8:
|
||||
ctx.ResultBlob(v.Bytes())
|
||||
|
||||
default:
|
||||
return fmt.Errorf("array: unsupported element:%.0w %v", sqlite3.MISMATCH, util.ReflectType(v))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
array := reflect.ValueOf(arg[0].Pointer())
|
||||
array, err := indexable(array)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.array = array
|
||||
c.rowID = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func indexable(v reflect.Value) (reflect.Value, error) {
|
||||
switch v.Kind() {
|
||||
case reflect.Slice:
|
||||
return v, nil
|
||||
case reflect.Array:
|
||||
return v, nil
|
||||
case reflect.Pointer:
|
||||
if v := v.Elem(); v.Kind() == reflect.Array {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return v, fmt.Errorf("array: unsupported argument:%.0w %v", sqlite3.MISMATCH, util.ReflectType(v))
|
||||
}
|
||||
156
ext/array/array_test.go
Normal file
156
ext/array/array_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package array_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/array"
|
||||
)
|
||||
|
||||
func Example_driver() {
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
array.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT name
|
||||
FROM pragma_function_list
|
||||
WHERE name like 'geopoly%' AND narg IN array(?)`,
|
||||
sqlite3.Pointer([]int{2, 3, 4}))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
err := rows.Scan(&name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%s\n", name)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Unordered output:
|
||||
// geopoly_regular
|
||||
// geopoly_overlap
|
||||
// geopoly_contains_point
|
||||
// geopoly_within
|
||||
}
|
||||
|
||||
func Example() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
array.Register(db)
|
||||
|
||||
stmt, _, err := db.Prepare(`
|
||||
SELECT name
|
||||
FROM pragma_function_list
|
||||
WHERE name like 'geopoly%' AND narg IN array(?)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
err = stmt.BindPointer(1, [...]int{2, 3, 4})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for stmt.Step() {
|
||||
fmt.Printf("%s\n", stmt.ColumnText(0))
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Unordered output:
|
||||
// geopoly_regular
|
||||
// geopoly_overlap
|
||||
// geopoly_contains_point
|
||||
// geopoly_within
|
||||
}
|
||||
|
||||
func Test_cursor_Column(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
array.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT rowid, value FROM array(?)`,
|
||||
sqlite3.Pointer(&[...]any{nil, true, 1, uint(2), math.Pi, "text", []byte{1, 2, 3}}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
want := []string{"nil", "int64", "int64", "int64", "float64", "string", "[]uint8"}
|
||||
|
||||
for rows.Next() {
|
||||
var id, val any
|
||||
err := rows.Scan(&id, &val)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := want[0]; val == nil {
|
||||
if want != "nil" {
|
||||
t.Errorf("got nil, want %s", want)
|
||||
}
|
||||
} else if got := reflect.TypeOf(val).String(); got != want {
|
||||
t.Errorf("got %s, want %s", got, want)
|
||||
}
|
||||
want = want[1:]
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_array_errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
array.Register(db)
|
||||
|
||||
err = db.Exec(`SELECT * FROM array()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT * FROM array(?)`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
}
|
||||
139
ext/blobio/blob.go
Normal file
139
ext/blobio/blob.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Package blobio provides an SQL interface to incremental BLOB I/O.
|
||||
package blobio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
// Register registers the SQL functions:
|
||||
//
|
||||
// readblob(schema, table, column, rowid, offset, n)
|
||||
//
|
||||
// Reads n bytes of a blob, starting at offset.
|
||||
//
|
||||
// writeblob(schema, table, column, rowid, offset, data)
|
||||
//
|
||||
// Writes data into a blob, at the given offset.
|
||||
//
|
||||
// openblob(schema, table, column, rowid, write, callback, args...)
|
||||
//
|
||||
// Opens blobs for reading or writing.
|
||||
// The callback is invoked for each open blob,
|
||||
// and must be bound to an [OpenCallback],
|
||||
// using [sqlite3.BindPointer] or [sqlite3.Pointer].
|
||||
// The optional args will be passed to the callback,
|
||||
// along with the [sqlite3.Blob] handle.
|
||||
//
|
||||
// https://sqlite.org/c3ref/blob.html
|
||||
func Register(db *sqlite3.Conn) {
|
||||
db.CreateFunction("readblob", 6, 0, readblob)
|
||||
db.CreateFunction("writeblob", 6, 0, writeblob)
|
||||
db.CreateFunction("openblob", -1, 0, openblob)
|
||||
}
|
||||
|
||||
// OpenCallback is the type for the openblob callback.
|
||||
type OpenCallback func(*sqlite3.Blob, ...sqlite3.Value) error
|
||||
|
||||
func readblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
blob, err := getAuxBlob(ctx, arg, false)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = blob.Seek(arg[4].Int64(), io.SeekStart)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
|
||||
n := arg[5].Int64()
|
||||
if n <= 0 {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
|
||||
_, err = io.ReadFull(blob, buf)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ResultBlob(buf)
|
||||
setAuxBlob(ctx, blob, false)
|
||||
}
|
||||
|
||||
func writeblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
blob, err := getAuxBlob(ctx, arg, true)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = blob.Seek(arg[4].Int64(), io.SeekStart)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = blob.Write(arg[5].RawBlob())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
|
||||
setAuxBlob(ctx, blob, false)
|
||||
}
|
||||
|
||||
func openblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if len(arg) < 6 {
|
||||
ctx.ResultError(util.ErrorString("openblob: wrong number of arguments"))
|
||||
return
|
||||
}
|
||||
|
||||
blob, err := getAuxBlob(ctx, arg, arg[4].Bool())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
|
||||
fn := arg[5].Pointer().(OpenCallback)
|
||||
err = fn(blob, arg[6:]...)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
|
||||
setAuxBlob(ctx, blob, true)
|
||||
}
|
||||
|
||||
func getAuxBlob(ctx sqlite3.Context, arg []sqlite3.Value, write bool) (*sqlite3.Blob, error) {
|
||||
row := arg[3].Int64()
|
||||
|
||||
if blob, ok := ctx.GetAuxData(0).(*sqlite3.Blob); ok {
|
||||
if err := blob.Reopen(row); errors.Is(err, sqlite3.MISUSE) {
|
||||
// Blob was closed (db, table, column or write changed).
|
||||
} else {
|
||||
return blob, err
|
||||
}
|
||||
}
|
||||
|
||||
db := arg[0].Text()
|
||||
table := arg[1].Text()
|
||||
column := arg[2].Text()
|
||||
return ctx.Conn().OpenBlob(db, table, column, row, write)
|
||||
}
|
||||
|
||||
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 open {
|
||||
ctx.SetAuxData(4, blob) // write
|
||||
}
|
||||
}
|
||||
184
ext/blobio/blob_test.go
Normal file
184
ext/blobio/blob_test.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package blobio_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "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/vfs/memdb"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
// Open the database, registering the extension.
|
||||
db, err := driver.Open("file:/test.db?vfs=memdb", func(conn *sqlite3.Conn) error {
|
||||
blobio.Register(conn)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
const message = "Hello BLOB!"
|
||||
|
||||
// Create the BLOB.
|
||||
_, err = db.Exec(`INSERT INTO test VALUES (?)`, sqlite3.ZeroBlob(len(message)))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Write the BLOB.
|
||||
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', last_insert_rowid(), 0, ?)`, message)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Read the BLOB.
|
||||
_, err = db.Exec(`SELECT openblob('main', 'test', 'col', rowid, false, ?) FROM test`,
|
||||
sqlite3.Pointer[blobio.OpenCallback](func(blob *sqlite3.Blob, _ ...sqlite3.Value) error {
|
||||
_, err = io.Copy(os.Stdout, blob)
|
||||
return err
|
||||
}))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// Hello BLOB!
|
||||
}
|
||||
|
||||
func Test_readblob(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
blobio.Register(db)
|
||||
array.Register(db)
|
||||
|
||||
err = db.Exec(`SELECT readblob()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS test1 (col);
|
||||
CREATE TABLE IF NOT EXISTS test2 (col);
|
||||
INSERT INTO test1 VALUES (x'cafe');
|
||||
INSERT INTO test2 VALUES (x'babe');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT readblob('main', value, 'col', 1, 1, 1) FROM array(?)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
err = stmt.BindPointer(1, []string{"test1", "test2"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnText(0)
|
||||
if got != "\xfe" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnText(0)
|
||||
if got != "\xbe" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
err = stmt.Err()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_openblob(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
blobio.Register(db)
|
||||
array.Register(db)
|
||||
|
||||
err = db.Exec(`SELECT openblob()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS test1 (col);
|
||||
CREATE TABLE IF NOT EXISTS test2 (col);
|
||||
INSERT INTO test1 VALUES (x'cafe');
|
||||
INSERT INTO test2 VALUES (x'babe');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT openblob('main', value, 'col', 1, false, ?) FROM array(?)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
var got []string
|
||||
err = stmt.BindPointer(1, blobio.OpenCallback(func(b *sqlite3.Blob, _ ...sqlite3.Value) error {
|
||||
d, err := io.ReadAll(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
got = append(got, string(d))
|
||||
return nil
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = stmt.BindPointer(2, []string{"test1", "test2"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = stmt.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := []string{"\xca\xfe", "\xba\xbe"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
36
ext/csv/arg.go
Normal file
36
ext/csv/arg.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/util/vtabutil"
|
||||
)
|
||||
|
||||
func uintArg(key, val string) (int, error) {
|
||||
i, err := strconv.ParseUint(val, 10, 15)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("csv: invalid %q parameter: %s", key, val)
|
||||
}
|
||||
return int(i), nil
|
||||
}
|
||||
|
||||
func boolArg(key, val string) (bool, error) {
|
||||
if val == "" {
|
||||
return true, nil
|
||||
}
|
||||
b, ok := util.ParseBool(val)
|
||||
if ok {
|
||||
return b, nil
|
||||
}
|
||||
return false, fmt.Errorf("csv: invalid %q parameter: %s", key, val)
|
||||
}
|
||||
|
||||
func runeArg(key, val string) (rune, error) {
|
||||
r, _, tail, err := strconv.UnquoteChar(vtabutil.Unquote(val), 0)
|
||||
if tail != "" || err != nil {
|
||||
return 0, fmt.Errorf("csv: invalid %q parameter: %s", key, val)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
108
ext/csv/arg_test.go
Normal file
108
ext/csv/arg_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/util/vtabutil"
|
||||
)
|
||||
|
||||
func Test_uintArg(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
arg string
|
||||
key string
|
||||
val int
|
||||
err bool
|
||||
}{
|
||||
{"columns 1", "columns 1", 0, true},
|
||||
{"columns = 1", "columns", 1, false},
|
||||
{"columns\t= 2", "columns", 2, false},
|
||||
{" columns = 3", "columns", 3, false},
|
||||
{" columns = -1", "columns", 0, true},
|
||||
{" columns = 32768", "columns", 0, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.arg, func(t *testing.T) {
|
||||
key, val := vtabutil.NamedArg(tt.arg)
|
||||
if key != tt.key {
|
||||
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
|
||||
}
|
||||
got, err := uintArg(key, val)
|
||||
if (err != nil) != tt.err {
|
||||
t.Fatalf("uintArg() error = %v, want err %v", err, tt.err)
|
||||
}
|
||||
if got != tt.val {
|
||||
t.Errorf("uintArg() = %v, want %v", got, tt.val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_boolArg(t *testing.T) {
|
||||
tests := []struct {
|
||||
arg string
|
||||
key string
|
||||
val bool
|
||||
err bool
|
||||
}{
|
||||
{"header", "header", true, false},
|
||||
{"header\t= 1", "header", true, false},
|
||||
{" header = 0", "header", false, false},
|
||||
{" header = TrUe", "header", true, false},
|
||||
{" header = FaLsE", "header", false, false},
|
||||
{" header = Yes", "header", true, false},
|
||||
{" header = nO", "header", false, false},
|
||||
{" header = On", "header", true, false},
|
||||
{" header = Off", "header", false, false},
|
||||
{" header = T", "header", false, true},
|
||||
{" header = f", "header", false, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.arg, func(t *testing.T) {
|
||||
key, val := vtabutil.NamedArg(tt.arg)
|
||||
if key != tt.key {
|
||||
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
|
||||
}
|
||||
got, err := boolArg(key, val)
|
||||
if (err != nil) != tt.err {
|
||||
t.Fatalf("boolArg() error = %v, want err %v", err, tt.err)
|
||||
}
|
||||
if got != tt.val {
|
||||
t.Errorf("boolArg() = %v, want %v", got, tt.val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_runeArg(t *testing.T) {
|
||||
tests := []struct {
|
||||
arg string
|
||||
key string
|
||||
val rune
|
||||
err bool
|
||||
}{
|
||||
{"comma", "comma", 0, true},
|
||||
{"comma\t= ,", "comma", ',', false},
|
||||
{" comma = ;", "comma", ';', false},
|
||||
{" comma = ;;", "comma", 0, true},
|
||||
{` comma = '\t`, "comma", 0, true},
|
||||
{` comma = '\t'`, "comma", '\t', false},
|
||||
{` comma = "\t"`, "comma", '\t', false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.arg, func(t *testing.T) {
|
||||
key, val := vtabutil.NamedArg(tt.arg)
|
||||
if key != tt.key {
|
||||
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
|
||||
}
|
||||
got, err := runeArg(key, val)
|
||||
if (err != nil) != tt.err {
|
||||
t.Fatalf("runeArg() error = %v, want err %v", err, tt.err)
|
||||
}
|
||||
if got != tt.val {
|
||||
t.Errorf("runeArg() = %v, want %v", got, tt.val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
232
ext/csv/csv.go
Normal file
232
ext/csv/csv.go
Normal file
@@ -0,0 +1,232 @@
|
||||
// Package csv provides a CSV virtual table.
|
||||
//
|
||||
// The CSV virtual table reads RFC 4180 formatted comma-separated values,
|
||||
// and returns that content as if it were rows and columns of an SQL table.
|
||||
//
|
||||
// https://sqlite.org/csv.html
|
||||
package csv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/util/osutil"
|
||||
"github.com/ncruces/go-sqlite3/util/vtabutil"
|
||||
)
|
||||
|
||||
// Register registers the CSV virtual table.
|
||||
// If a filename is specified, [os.Open] is used to open the file.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
RegisterFS(db, osutil.FS{})
|
||||
}
|
||||
|
||||
// RegisterFS registers the CSV virtual table.
|
||||
// If a filename is specified, fsys is used to open the file.
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
|
||||
var (
|
||||
filename string
|
||||
data string
|
||||
schema string
|
||||
header bool
|
||||
columns int = -1
|
||||
comma rune = ','
|
||||
|
||||
done = map[string]struct{}{}
|
||||
)
|
||||
|
||||
for _, arg := range arg {
|
||||
key, val := vtabutil.NamedArg(arg)
|
||||
if _, ok := done[key]; ok {
|
||||
return nil, fmt.Errorf("csv: more than one %q parameter", key)
|
||||
}
|
||||
switch key {
|
||||
case "filename":
|
||||
filename = vtabutil.Unquote(val)
|
||||
case "data":
|
||||
data = vtabutil.Unquote(val)
|
||||
case "schema":
|
||||
schema = vtabutil.Unquote(val)
|
||||
case "header":
|
||||
header, err = boolArg(key, val)
|
||||
case "columns":
|
||||
columns, err = uintArg(key, val)
|
||||
case "comma":
|
||||
comma, err = runeArg(key, val)
|
||||
default:
|
||||
return nil, fmt.Errorf("csv: unknown %q parameter", key)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
done[key] = struct{}{}
|
||||
}
|
||||
|
||||
if (filename == "") == (data == "") {
|
||||
return nil, fmt.Errorf(`csv: must specify either "filename" or "data" but not both`)
|
||||
}
|
||||
|
||||
table := &table{
|
||||
fsys: fsys,
|
||||
name: filename,
|
||||
data: data,
|
||||
comma: comma,
|
||||
header: header,
|
||||
}
|
||||
|
||||
if schema == "" {
|
||||
var row []string
|
||||
if header || columns < 0 {
|
||||
csv, c, err := table.newReader()
|
||||
defer c.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row, err = csv.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
schema = getSchema(header, columns, row)
|
||||
}
|
||||
|
||||
err = db.DeclareVTab(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return table, nil
|
||||
}
|
||||
|
||||
sqlite3.CreateModule(db, "csv", declare, declare)
|
||||
}
|
||||
|
||||
type table struct {
|
||||
fsys fs.FS
|
||||
name string
|
||||
data string
|
||||
comma rune
|
||||
header bool
|
||||
}
|
||||
|
||||
func (t *table) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
idx.EstimatedCost = 1e6
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *table) Open() (sqlite3.VTabCursor, error) {
|
||||
return &cursor{table: t}, nil
|
||||
}
|
||||
|
||||
func (t *table) Rename(new string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *table) Integrity(schema, table string, flags int) error {
|
||||
if flags&1 != 0 {
|
||||
return nil
|
||||
}
|
||||
csv, c, err := t.newReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
_, err = csv.ReadAll()
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *table) newReader() (*csv.Reader, io.Closer, error) {
|
||||
var r io.Reader
|
||||
var c io.Closer
|
||||
if t.name != "" {
|
||||
f, err := t.fsys.Open(t.name)
|
||||
if err != nil {
|
||||
return nil, f, err
|
||||
}
|
||||
|
||||
buf := bufio.NewReader(f)
|
||||
bom, err := buf.Peek(3)
|
||||
if err != nil {
|
||||
return nil, f, err
|
||||
}
|
||||
if string(bom) == "\xEF\xBB\xBF" {
|
||||
buf.Discard(3)
|
||||
}
|
||||
|
||||
r = buf
|
||||
c = f
|
||||
} else {
|
||||
r = strings.NewReader(t.data)
|
||||
c = io.NopCloser(r)
|
||||
}
|
||||
|
||||
csv := csv.NewReader(r)
|
||||
csv.ReuseRecord = true
|
||||
csv.Comma = t.comma
|
||||
return csv, c, nil
|
||||
}
|
||||
|
||||
type cursor struct {
|
||||
table *table
|
||||
closer io.Closer
|
||||
csv *csv.Reader
|
||||
row []string
|
||||
rowID int64
|
||||
}
|
||||
|
||||
func (c *cursor) Close() (err error) {
|
||||
if c.closer != nil {
|
||||
err = c.closer.Close()
|
||||
c.closer = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.csv, c.closer, err = c.table.newReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.table.header {
|
||||
c.Next() // skip header
|
||||
}
|
||||
c.rowID = 0
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (c *cursor) Next() (err error) {
|
||||
c.rowID++
|
||||
c.row, err = c.csv.Read()
|
||||
if err != io.EOF {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) EOF() bool {
|
||||
return c.row == nil
|
||||
}
|
||||
|
||||
func (c *cursor) RowID() (int64, error) {
|
||||
return c.rowID, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
|
||||
if col < len(c.row) {
|
||||
ctx.ResultText(c.row[col])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
160
ext/csv/csv_test.go
Normal file
160
ext/csv/csv_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package csv_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/csv"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
csv.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS eurofxref USING csv(
|
||||
filename = 'testdata/eurofxref.csv',
|
||||
header = YES,
|
||||
columns = 42,
|
||||
)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT USD FROM eurofxref WHERE Date = '2022-02-22'`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
fmt.Printf("On Twosday, 1€ = $%g", stmt.ColumnFloat(0))
|
||||
}
|
||||
if err := stmt.Reset(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`DROP TABLE eurofxref`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// On Twosday, 1€ = $1.1342
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
csv.Register(db)
|
||||
|
||||
const data = `
|
||||
"Rob" "Pike" rob
|
||||
"Ken" Thompson ken
|
||||
Robert "Griesemer" "gri"`
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE temp.users USING csv(
|
||||
data = ` + sqlite3.Quote(data) + `,
|
||||
schema = 'CREATE TABLE x(first_name, last_name, username)',
|
||||
comma = '\t'
|
||||
)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT * FROM temp.users WHERE rowid = 1 ORDER BY username`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if !stmt.Step() {
|
||||
t.Fatal("no rows")
|
||||
}
|
||||
if got := stmt.ColumnText(0); got != "Rob" {
|
||||
t.Errorf("got %q want Rob", got)
|
||||
}
|
||||
if stmt.Step() {
|
||||
t.Fatal("more rows")
|
||||
}
|
||||
|
||||
err = db.Exec(`ALTER TABLE temp.users RENAME TO csv`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`PRAGMA integrity_check`)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`PRAGMA quick_check`)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`DROP TABLE temp.csv`)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
csv.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv(data='abc', data='abc')`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv(data='abc', xpto='abc')`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv(data='abc', comma='"')`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv(data='abc', header=tru)`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
}
|
||||
39
ext/csv/schema.go
Normal file
39
ext/csv/schema.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
func getSchema(header bool, columns int, row []string) string {
|
||||
var sep string
|
||||
var str strings.Builder
|
||||
str.WriteString("CREATE TABLE x(")
|
||||
|
||||
if 0 <= columns && columns < len(row) {
|
||||
row = row[:columns]
|
||||
}
|
||||
for i, f := range row {
|
||||
str.WriteString(sep)
|
||||
if header && f != "" {
|
||||
str.WriteString(sqlite3.QuoteIdentifier(f))
|
||||
} else {
|
||||
str.WriteString("c")
|
||||
str.WriteString(strconv.Itoa(i + 1))
|
||||
}
|
||||
str.WriteString(" TEXT")
|
||||
sep = ","
|
||||
}
|
||||
for i := len(row); i < columns; i++ {
|
||||
str.WriteString(sep)
|
||||
str.WriteString("c")
|
||||
str.WriteString(strconv.Itoa(i + 1))
|
||||
str.WriteString(" TEXT")
|
||||
sep = ","
|
||||
}
|
||||
str.WriteByte(')')
|
||||
|
||||
return str.String()
|
||||
}
|
||||
28
ext/csv/schema_test.go
Normal file
28
ext/csv/schema_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package csv
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_getSchema(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
header bool
|
||||
columns int
|
||||
row []string
|
||||
want string
|
||||
}{
|
||||
{true, 2, nil, `CREATE TABLE x(c1 TEXT,c2 TEXT)`},
|
||||
{false, 2, nil, `CREATE TABLE x(c1 TEXT,c2 TEXT)`},
|
||||
{false, -1, []string{"abc", ""}, `CREATE TABLE x(c1 TEXT,c2 TEXT)`},
|
||||
{true, 3, []string{"abc", ""}, `CREATE TABLE x("abc" TEXT,c2 TEXT,c3 TEXT)`},
|
||||
{true, -1, []string{"abc", "def"}, `CREATE TABLE x("abc" TEXT,"def" TEXT)`},
|
||||
{true, 1, []string{"abc", "def"}, `CREATE TABLE x("abc" TEXT)`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want, func(t *testing.T) {
|
||||
if got := getSchema(tt.header, tt.columns, tt.row); got != tt.want {
|
||||
t.Errorf("getSchema() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
258
ext/csv/testdata/eurofxref.csv
vendored
Normal file
258
ext/csv/testdata/eurofxref.csv
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
Date,USD,JPY,BGN,CYP,CZK,DKK,EEK,GBP,HUF,LTL,LVL,MTL,PLN,ROL,RON,SEK,SIT,SKK,CHF,ISK,NOK,HRK,RUB,TRL,TRY,AUD,BRL,CAD,CNY,HKD,IDR,ILS,INR,KRW,MXN,MYR,NZD,PHP,SGD,THB,ZAR,
|
||||
2022-12-30,1.0666,140.66,1.9558,N/A,24.116,7.4365,N/A,0.88693,400.87,N/A,N/A,N/A,4.6808,N/A,4.9495,11.1218,N/A,N/A,0.9847,151.5,10.5138,7.5365,N/A,N/A,19.9649,1.5693,5.6386,1.444,7.3582,8.3163,16519.82,3.7554,88.171,1344.09,20.856,4.6984,1.6798,59.32,1.43,36.835,18.0986,
|
||||
2022-12-29,1.0649,142.24,1.9558,N/A,24.191,7.4365,N/A,0.88549,399.6,N/A,N/A,N/A,4.6855,N/A,4.9493,11.158,N/A,N/A,0.984,152.5,10.55,7.5365,N/A,N/A,19.934,1.5859,5.5351,1.4475,7.4151,8.2994,16680.38,3.7575,88.2295,1350.18,20.651,4.7106,1.6887,59.367,1.436,36.877,18.1967,
|
||||
2022-12-28,1.064,142.21,1.9558,N/A,24.252,7.4365,N/A,0.88058,403.3,N/A,N/A,N/A,4.7008,N/A,4.946,11.1038,N/A,N/A,0.9863,151.9,10.4495,7.5365,N/A,N/A,19.9144,1.566,5.6109,1.4361,7.4224,8.2931,16765.93,3.7526,88.0943,1348.59,20.6856,4.7055,1.6772,59.613,1.4323,36.953,18.289,
|
||||
2022-12-27,1.0624,141.68,1.9558,N/A,24.26,7.4366,N/A,0.88333,401.65,N/A,N/A,N/A,4.6683,N/A,4.927,11.1285,N/A,N/A,0.9885,152.3,10.4895,7.5375,N/A,N/A,19.8799,1.577,5.6035,1.4384,7.3994,8.2874,16620.58,3.7278,88.0808,1349.85,20.5515,4.699,1.6916,59.356,1.43,36.775,18.3181,
|
||||
2022-12-23,1.0622,140.86,1.9558,N/A,24.247,7.4364,N/A,0.8803,400.68,N/A,N/A,N/A,4.6423,N/A,4.9056,11.1045,N/A,N/A,0.9867,152.3,10.4448,7.537,N/A,N/A,19.843,1.5857,5.4834,1.4433,7.4198,8.2878,16569.18,3.704,87.958,1359.5,20.7115,4.7002,1.6887,58.623,1.4337,36.842,18.1048,
|
||||
2022-12-22,1.0633,140.42,1.9558,N/A,24.215,7.4367,N/A,0.88243,402.13,N/A,N/A,N/A,4.6443,N/A,4.8993,11.05,N/A,N/A,0.9852,153.3,10.4123,7.538,N/A,N/A,19.8553,1.5804,5.5386,1.4484,7.4229,8.2883,16525.91,3.6942,88.0365,1361.75,20.8485,4.7051,1.6918,58.705,1.4356,36.849,18.2238,
|
||||
2022-12-21,1.0636,140.29,1.9558,N/A,24.218,7.438,N/A,0.87651,402.93,N/A,N/A,N/A,4.6665,N/A,4.8937,11.0623,N/A,N/A,0.9836,152.1,10.4309,7.5419,N/A,N/A,19.8541,1.5859,5.4913,1.4475,7.4219,8.2902,16573.77,3.6989,88.109,1367.63,20.9919,4.7197,1.685,58.556,1.4366,36.907,18.3529,
|
||||
2022-12-20,1.0599,140.58,1.9558,N/A,24.181,7.4388,N/A,0.8753,403.88,N/A,N/A,N/A,4.6757,N/A,4.9125,11.0615,N/A,N/A,0.9854,151.5,10.5098,7.5471,N/A,N/A,19.7744,1.5972,5.6234,1.4451,7.39,8.2488,16537.09,3.6759,87.6649,1363.73,20.9355,4.6991,1.6816,58.549,1.4347,36.853,18.4239,
|
||||
2022-12-19,1.0598,144.65,1.9558,N/A,24.233,7.4382,N/A,0.87118,403.18,N/A,N/A,N/A,4.6853,N/A,4.9107,11.0063,N/A,N/A,0.9884,151.9,10.5025,7.5395,N/A,N/A,19.7676,1.5794,5.6327,1.4472,7.3901,8.2428,16506.72,3.6551,87.5321,1377.17,20.9743,4.6912,1.6632,58.649,1.4378,36.923,18.3074,
|
||||
2022-12-16,1.0619,145.53,1.9558,N/A,24.262,7.4379,N/A,0.87233,407.1,N/A,N/A,N/A,4.6925,N/A,4.9213,11.0153,N/A,N/A,0.9879,150.1,10.4833,7.5385,N/A,N/A,19.8039,1.5866,5.6233,1.4506,7.4037,8.2632,16575.47,3.6689,87.824,1389.7,21.0634,4.6984,1.6687,58.967,1.4413,37.145,18.6708,
|
||||
2022-12-15,1.0621,145.07,1.9558,N/A,24.27,7.4387,N/A,0.86194,406.4,N/A,N/A,N/A,4.689,N/A,4.922,10.898,N/A,N/A,0.9862,150.9,10.4013,7.5395,N/A,N/A,19.806,1.5695,5.6247,1.4443,7.4007,8.2551,16591.4,3.6388,87.9355,1393.97,20.9431,4.6918,1.6628,59.297,1.4406,37.12,18.3599,
|
||||
2022-12-14,1.0649,143.68,1.9558,N/A,24.276,7.4392,N/A,0.86118,406.63,N/A,N/A,N/A,4.681,N/A,4.9248,10.8638,N/A,N/A,0.9865,150.9,10.362,7.538,N/A,N/A,19.8579,1.551,5.6842,1.4441,7.4009,8.2751,16599.51,3.6327,87.8435,1379.99,20.8635,4.6765,1.6508,59.326,1.4349,36.851,18.2563,
|
||||
2022-12-13,1.0545,144.85,1.9558,N/A,24.287,7.4391,N/A,0.85753,409.65,N/A,N/A,N/A,4.6938,N/A,4.9298,10.8965,N/A,N/A,0.9869,151.1,10.4679,7.5495,N/A,N/A,19.6649,1.5553,5.5784,1.4341,7.3637,8.2033,16521.81,3.6266,87.2965,1378.75,20.9435,4.6704,1.6464,58.852,1.4288,36.707,18.6855,
|
||||
2022-12-12,1.0562,144.86,1.9558,N/A,24.307,7.4379,N/A,0.86006,416.78,N/A,N/A,N/A,4.6923,N/A,4.9318,10.9075,N/A,N/A,0.9855,150.7,10.5548,7.554,N/A,N/A,19.6913,1.5625,5.556,1.4428,7.367,8.2103,16524.63,3.6232,87.253,1377.93,20.9047,4.6652,1.6523,58.788,1.4284,36.708,18.4697,
|
||||
2022-12-09,1.0559,143.3,1.9558,N/A,24.293,7.4379,N/A,0.8595,417.53,N/A,N/A,N/A,4.6869,N/A,4.9224,10.9188,N/A,N/A,0.9856,149.5,10.5345,7.555,N/A,N/A,19.6872,1.5553,5.5457,1.438,7.3475,8.2169,16453.46,3.6128,86.9535,1373.94,20.849,4.6512,1.6482,58.47,1.426,36.656,18.2358,
|
||||
2022-12-08,1.0519,143.75,1.9558,N/A,24.324,7.4382,N/A,0.86258,417.66,N/A,N/A,N/A,4.6853,N/A,4.9131,10.906,N/A,N/A,0.9889,149.5,10.488,7.5553,N/A,N/A,19.6114,1.559,5.488,1.4307,7.3324,8.1889,16423.92,3.6206,86.6755,1387.06,20.6989,4.6257,1.6547,58.233,1.4256,36.559,18.0225,
|
||||
2022-12-07,1.0529,144.44,1.9558,N/A,24.322,7.4382,N/A,0.86408,410.63,N/A,N/A,N/A,4.7003,N/A,4.918,10.919,N/A,N/A,0.9893,148.7,10.5255,7.5525,N/A,N/A,19.6256,1.5728,5.5023,1.4387,7.3476,8.1997,16465.17,3.626,86.692,1390.87,20.7534,4.6301,1.6573,58.432,1.4286,36.904,18.1353,
|
||||
2022-12-06,1.0516,143.33,1.9558,N/A,24.316,7.438,N/A,0.8617,415.08,N/A,N/A,N/A,4.6975,N/A,4.913,10.889,N/A,N/A,0.9872,148.9,10.4408,7.5563,N/A,N/A,19.601,1.5625,5.5113,1.4326,7.3494,8.1813,16441.49,3.5831,86.6485,1386.39,20.6884,4.6223,1.6583,58.782,1.4263,36.827,18.2068,
|
||||
2022-12-05,1.0587,143.07,1.9558,N/A,24.351,7.4369,N/A,0.86085,412.13,N/A,N/A,N/A,4.695,N/A,4.9215,10.8931,N/A,N/A,0.9893,148.9,10.3366,7.551,N/A,N/A,19.7326,1.5542,5.5491,1.4198,7.3573,8.2236,16332.36,3.5856,86.5249,1370.87,20.7295,4.6255,1.6498,59.245,1.4271,36.732,18.2038,
|
||||
2022-12-02,1.0538,141.32,1.9558,N/A,24.377,7.4373,N/A,0.85855,410,N/A,N/A,N/A,4.684,N/A,4.9298,10.902,N/A,N/A,0.9834,148.7,10.2615,7.5503,N/A,N/A,19.6392,1.5457,5.4657,1.416,7.3971,8.2035,16179.78,3.5769,85.6435,1366.67,20.187,4.6241,1.6453,58.734,1.4227,36.614,18.2749,
|
||||
2022-12-01,1.0454,142.48,1.9558,N/A,24.361,7.4373,N/A,0.85715,413.5,N/A,N/A,N/A,4.6998,N/A,4.9303,10.8984,N/A,N/A,0.9868,148.7,10.2495,7.55,N/A,N/A,19.4778,1.5377,5.4508,1.4059,7.3965,8.1371,16160.84,3.5642,84.933,1363.81,20.1472,4.605,1.6446,58.793,1.4195,36.526,18.5393,
|
||||
2022-11-30,1.0376,144.28,1.9558,N/A,24.338,7.4366,N/A,0.86488,408.4,N/A,N/A,N/A,4.6635,N/A,4.9245,10.9345,N/A,N/A,0.9854,147.1,10.2648,7.549,N/A,N/A,19.3333,1.5425,5.5063,1.4021,7.3437,8.0944,16271.81,3.5691,84.4215,1365.14,20.0111,4.6147,1.6634,58.697,1.418,36.588,17.5768,
|
||||
2022-11-29,1.0366,143.36,1.9558,N/A,24.334,7.4367,N/A,0.86218,406.5,N/A,N/A,N/A,4.673,N/A,4.9193,10.901,N/A,N/A,0.9862,147.1,10.3313,7.55,N/A,N/A,19.3181,1.5414,5.5126,1.4005,7.4289,8.0965,16301.58,3.5581,84.6548,1375.7,19.8075,4.6735,1.6639,58.64,1.4237,36.706,17.6027,
|
||||
2022-11-28,1.0463,144.9,1.9558,N/A,24.348,7.4367,N/A,0.86606,408.87,N/A,N/A,N/A,4.6938,N/A,4.9246,10.8973,N/A,N/A,0.9872,146.7,10.364,7.5488,N/A,N/A,19.4844,1.5632,5.6354,1.4062,7.5326,8.1782,16440.45,3.6002,85.437,1396.56,20.22,4.6874,1.6827,59.21,1.4375,37.285,17.9376,
|
||||
2022-11-25,1.0375,144.62,1.9558,N/A,24.367,7.4365,N/A,0.85885,411.33,N/A,N/A,N/A,4.6875,N/A,4.9255,10.8183,N/A,N/A,0.9836,146.5,10.2985,7.5473,N/A,N/A,19.3333,1.5404,5.5476,1.3864,7.4425,8.1084,16282.44,3.5551,84.7145,1383.2,20.1069,4.648,1.6651,58.795,1.4277,37.153,17.7677,
|
||||
2022-11-24,1.0413,143.9,1.9558,N/A,24.392,7.4369,N/A,0.85933,413.33,N/A,N/A,N/A,4.6958,N/A,4.9205,10.8573,N/A,N/A,0.9818,146.5,10.3435,7.547,N/A,N/A,19.3969,1.5414,5.5504,1.3894,7.4442,8.1324,16295.35,3.5607,85.0295,1382.28,20.1501,4.6806,1.6598,58.992,1.4319,37.258,17.7246,
|
||||
2022-11-23,1.0325,145.75,1.9558,N/A,24.356,7.437,N/A,0.86369,405.75,N/A,N/A,N/A,4.7033,N/A,4.937,10.8933,N/A,N/A,0.9795,146.7,10.3659,7.5435,N/A,N/A,19.2316,1.5522,5.565,1.3856,7.3982,8.0708,16189.81,3.5681,84.466,1397.42,20.001,4.7237,1.6718,58.914,1.4295,37.423,17.7103,
|
||||
2022-11-22,1.0274,145.2,1.9558,N/A,24.351,7.4377,N/A,0.86358,408.23,N/A,N/A,N/A,4.7125,N/A,4.9269,10.9653,N/A,N/A,0.9791,145.7,10.4445,7.5438,N/A,N/A,19.1221,1.5473,5.4578,1.3765,7.3344,8.0313,16106.79,3.5615,83.8768,1392.68,20.0951,4.7055,1.6707,58.942,1.4167,37.11,17.7568,
|
||||
2022-11-21,1.0246,145.33,1.9558,N/A,24.356,7.4377,N/A,0.86793,409.43,N/A,N/A,N/A,4.7075,N/A,4.9413,10.9873,N/A,N/A,0.9817,147.9,10.4898,7.5425,N/A,N/A,19.0822,1.5471,5.4401,1.377,7.3419,7.9989,16116.76,3.5515,83.7375,1395,19.9357,4.6927,1.6766,58.822,1.4162,37.121,17.7858,
|
||||
2022-11-18,1.0366,145.12,1.9558,N/A,24.351,7.4385,N/A,0.87063,407.41,N/A,N/A,N/A,4.7033,N/A,4.9413,10.9805,N/A,N/A,0.9881,148.9,10.486,7.5415,N/A,N/A,19.3009,1.5433,5.547,1.3841,7.379,8.1092,16224.36,3.591,84.6875,1389.02,20.162,4.7202,1.6757,59.41,1.4229,37.069,17.908,
|
||||
2022-11-17,1.0319,144.8,1.9558,N/A,24.399,7.4383,N/A,0.87475,415.6,N/A,N/A,N/A,4.7153,N/A,4.9254,10.9871,N/A,N/A,0.9818,148.9,10.498,7.541,N/A,N/A,19.2124,1.5526,5.6535,1.382,7.3859,8.077,16224.01,3.5796,84.394,1394.06,20.062,4.7122,1.6986,59.293,1.4221,37.123,18.0961,
|
||||
2022-11-16,1.0412,145.29,1.9558,N/A,24.355,7.4386,N/A,0.87483,408.18,N/A,N/A,N/A,4.7065,N/A,4.9206,10.8754,N/A,N/A,0.9795,148.9,10.3675,7.5443,N/A,N/A,19.3783,1.54,5.5438,1.3801,7.372,8.1444,16248.37,3.5684,84.5905,1378.1,20.1227,4.7323,1.6897,59.678,1.425,37.103,18.0195,
|
||||
2022-11-15,1.0404,144.84,1.9558,N/A,24.326,7.4388,N/A,0.87455,405.45,N/A,N/A,N/A,4.7073,N/A,4.9116,10.8081,N/A,N/A,0.979,149.9,10.357,7.5459,N/A,N/A,19.3608,1.5415,5.548,1.3816,7.3299,8.143,16164.78,3.5694,84.1304,1365.61,20.0795,4.7208,1.6897,59.532,1.4238,36.939,17.8822,
|
||||
2022-11-14,1.0319,144.86,1.9558,N/A,24.289,7.4382,N/A,0.87513,407.28,N/A,N/A,N/A,4.6898,N/A,4.9043,10.7713,N/A,N/A,0.9751,150.3,10.3143,7.5465,N/A,N/A,19.1923,1.5427,5.4605,1.3706,7.2906,8.0852,16052.12,3.541,83.7779,1369.32,20.0985,4.7429,1.6957,59.04,1.4177,36.978,17.8393,
|
||||
2022-11-11,1.0308,143.89,1.9558,N/A,24.278,7.4384,N/A,0.87538,402.08,N/A,N/A,N/A,4.6765,N/A,4.894,10.7241,N/A,N/A,0.9844,148.7,10.2635,7.5445,N/A,N/A,19.0987,1.5459,5.5147,1.3698,7.3267,8.0758,15979.45,3.5255,83.2253,1359.2,20.0239,4.77,1.702,59.106,1.4199,37.088,17.7944,
|
||||
2022-11-10,0.9954,145.47,1.9558,N/A,24.361,7.4381,N/A,0.87298,400.95,N/A,N/A,N/A,4.706,N/A,4.8913,10.8743,N/A,N/A,0.9834,147.5,10.3615,7.5427,N/A,N/A,18.51,1.5525,5.286,1.3467,7.2184,7.8128,15615.6,3.5453,81.3058,1373.96,19.4562,4.6789,1.6984,57.793,1.3963,36.7,17.6882,
|
||||
2022-11-09,1.0039,146.82,1.9558,N/A,24.337,7.4382,N/A,0.87774,403.53,N/A,N/A,N/A,4.701,N/A,4.9045,10.845,N/A,N/A,0.988,146.7,10.322,7.5425,N/A,N/A,18.6728,1.5538,5.1947,1.3501,7.2813,7.8801,15717.07,3.5621,81.6575,1369.73,19.6554,4.7098,1.7033,58.236,1.4061,36.999,17.877,
|
||||
2022-11-08,0.9996,146.25,1.9558,N/A,24.326,7.4378,N/A,0.87378,400.75,N/A,N/A,N/A,4.6918,N/A,4.8978,10.8373,N/A,N/A,0.9911,146.3,10.2795,7.539,N/A,N/A,18.5991,1.5435,5.203,1.3489,7.2495,7.8468,15652.76,3.5436,81.518,1377.94,19.4495,4.7346,1.686,58.187,1.4022,37.22,17.8397,
|
||||
2022-11-07,0.9993,146.18,1.9558,N/A,24.301,7.4393,N/A,0.87135,401.03,N/A,N/A,N/A,4.6865,N/A,4.8855,10.832,N/A,N/A,0.9874,145.9,10.2555,7.5375,N/A,N/A,18.5875,1.5428,5.07,1.3464,7.2189,7.8444,15648.95,3.5402,81.8407,1391.25,19.4395,4.7362,1.6834,58.361,1.4022,37.284,17.7583,
|
||||
2022-11-04,0.9872,145.19,1.9558,N/A,24.422,7.4419,N/A,0.87478,401.15,N/A,N/A,N/A,4.6825,N/A,4.8893,10.8538,N/A,N/A,0.9863,145.5,10.2019,7.5353,N/A,N/A,18.3845,1.5311,4.9682,1.3351,7.0894,7.7493,15491.81,3.5065,81.02,1397.7,19.2611,4.6872,1.6769,57.672,1.3891,36.906,17.7983,
|
||||
2022-11-03,0.9753,144.58,1.9558,N/A,24.539,7.4433,N/A,0.87228,407.87,N/A,N/A,N/A,4.709,N/A,4.9013,10.932,N/A,N/A,0.9889,144.9,10.3543,7.5375,N/A,N/A,18.1602,1.5517,5.0262,1.3452,7.1367,7.656,15400.2,3.4847,80.8845,1391.75,19.2363,4.6271,1.6957,57.463,1.3878,37.091,18.0173,
|
||||
2022-11-02,0.9908,145.75,1.9558,N/A,24.506,7.4431,N/A,0.861,407,N/A,N/A,N/A,4.7035,N/A,4.912,10.9065,N/A,N/A,0.9861,143.7,10.2388,7.5335,N/A,N/A,18.4488,1.5426,5.0964,1.347,7.2156,7.7774,15492.57,3.4987,81.992,1402.01,19.4921,4.6944,1.6844,57.841,1.3983,37.314,17.9608,
|
||||
2022-11-01,0.9947,146.35,1.9558,N/A,24.484,7.4438,N/A,0.86058,406.9,N/A,N/A,N/A,4.7053,N/A,4.9138,10.874,N/A,N/A,0.9878,143.3,10.1835,7.5342,N/A,N/A,18.5216,1.5409,5.1337,1.3469,7.2165,7.8079,15534.6,3.4922,82.084,1404.63,19.5984,4.7119,1.6876,57.786,1.4017,37.45,17.9802,
|
||||
2022-10-31,0.9914,147.4,1.9558,N/A,24.488,7.4444,N/A,0.86115,409.65,N/A,N/A,N/A,4.7085,N/A,4.9143,10.901,N/A,N/A,0.9925,143.3,10.3028,7.531,N/A,N/A,18.4562,1.5529,5.2694,1.3553,7.238,7.7822,15489.55,3.4933,82.1035,1416.12,19.7122,4.6873,1.7099,57.8,1.4038,37.748,18.1736,
|
||||
2022-10-28,0.9951,146.79,1.9558,N/A,24.465,7.4423,N/A,0.8612,411.7,N/A,N/A,N/A,4.7275,N/A,4.9189,10.9403,N/A,N/A,0.992,143.3,10.2695,7.532,N/A,N/A,18.5219,1.5511,5.327,1.3542,7.2159,7.8107,15481.88,3.5215,82.0565,1417.7,19.7718,4.6994,1.7151,57.739,1.4055,37.724,18.053,
|
||||
2022-10-27,1.0037,147.37,1.9558,N/A,24.53,7.4387,N/A,0.86745,412.15,N/A,N/A,N/A,4.7585,N/A,4.8893,10.9583,N/A,N/A,0.9949,143.1,10.342,7.533,N/A,N/A,18.681,1.561,5.3889,1.3672,7.2552,7.8782,15629.06,3.5376,82.656,1428.57,20.015,4.7324,1.7316,58.441,1.4154,37.975,18.1521,
|
||||
2022-10-26,1.0023,147.32,1.9558,N/A,24.535,7.4381,N/A,0.86603,408.09,N/A,N/A,N/A,4.7548,N/A,4.8806,10.953,N/A,N/A,0.9917,143.5,10.3408,7.532,N/A,N/A,18.6461,1.5466,5.2944,1.3568,7.1948,7.8678,15589.27,3.5145,82.206,1422.11,19.8501,4.7262,1.7249,58.493,1.4104,37.862,18.0212,
|
||||
2022-10-25,0.9861,146.84,1.9558,N/A,24.472,7.4387,N/A,0.87143,413.7,N/A,N/A,N/A,4.777,N/A,4.9036,10.9728,N/A,N/A,0.9888,142.9,10.391,7.5315,N/A,N/A,18.3508,1.5599,5.2254,1.3537,7.2072,7.7407,15407.12,3.506,81.653,1417.5,19.6353,4.6697,1.7321,57.988,1.405,37.758,18.2211,
|
||||
2022-10-24,0.9851,146.76,1.9558,N/A,24.482,7.4385,N/A,0.8707,411.88,N/A,N/A,N/A,4.7908,N/A,4.9128,11.0795,N/A,N/A,0.9856,142.5,10.392,7.5337,N/A,N/A,18.3298,1.5631,5.1461,1.3502,7.1544,7.7329,15362.63,3.4997,81.5451,1418.4,19.6514,4.6674,1.7343,58.021,1.4008,37.6,18.0625,
|
||||
2022-10-21,0.973,147.59,1.9558,N/A,24.511,7.4382,N/A,0.87728,412.88,N/A,N/A,N/A,4.7885,N/A,4.9125,11.0868,N/A,N/A,0.9855,141.1,10.4315,7.5325,N/A,N/A,18.0988,1.5646,5.1117,1.3465,7.0504,7.6376,15199.12,3.4803,80.739,1404.32,19.5521,4.6101,1.7347,57.287,1.3917,37.349,18.0021,
|
||||
2022-10-20,0.9811,146.99,1.9558,N/A,24.525,7.4389,N/A,0.87258,411.2,N/A,N/A,N/A,4.7728,N/A,4.9203,10.982,N/A,N/A,0.9836,141.1,10.402,7.5353,N/A,N/A,18.2257,1.5554,5.1387,1.3461,7.0858,7.7008,15250.05,3.4754,81.1755,1400.3,19.7005,4.6396,1.7206,57.742,1.3959,37.36,17.9106,
|
||||
2022-10-19,0.9778,146.34,1.9558,N/A,24.563,7.439,N/A,0.86993,413.78,N/A,N/A,N/A,4.7878,N/A,4.9248,10.9448,N/A,N/A,0.981,141.1,10.3823,7.5325,N/A,N/A,18.1793,1.5568,5.1755,1.3479,7.0672,7.6757,15185.1,3.4628,81.1955,1398.35,19.6845,4.6152,1.7264,57.741,1.3931,37.469,17.8339,
|
||||
2022-10-18,0.9835,146.65,1.9558,N/A,24.593,7.4393,N/A,0.86928,413.08,N/A,N/A,N/A,4.804,N/A,4.9359,10.906,N/A,N/A,0.9792,141.5,10.3528,7.5298,N/A,N/A,18.2813,1.5557,5.1795,1.3495,7.0805,7.72,15214.98,3.464,80.9195,1400.92,19.664,4.6382,1.7251,57.897,1.3963,37.422,17.7904,
|
||||
2022-10-17,0.9739,145,1.9558,N/A,24.562,7.4379,N/A,0.8625,418.3,N/A,N/A,N/A,4.8143,N/A,4.937,10.9893,N/A,N/A,0.9762,140.9,10.342,7.5265,N/A,N/A,18.1043,1.5599,5.1497,1.3452,7.013,7.6448,15061.8,3.4486,80.128,1399.41,19.5,4.5934,1.7404,57.433,1.3896,37.169,17.6769,
|
||||
2022-10-14,0.9717,143.63,1.9558,N/A,24.587,7.4378,N/A,0.86823,418.24,N/A,N/A,N/A,4.8328,N/A,4.9335,11.0035,N/A,N/A,0.9757,140.5,10.3323,7.5266,N/A,N/A,18.0614,1.5493,5.1177,1.3426,6.9952,7.6278,15031.5,3.444,79.9695,1398.5,19.5032,4.5689,1.7302,57.375,1.3852,37.109,17.6932,
|
||||
2022-10-13,0.9739,142.94,1.9558,N/A,24.569,7.4385,N/A,0.86513,430.65,N/A,N/A,N/A,4.8303,N/A,4.9355,11.0098,N/A,N/A,0.9725,140.5,10.3525,7.531,N/A,N/A,18.1041,1.5495,5.1214,1.3443,6.9945,7.644,14952.86,3.4731,79.9981,1392.71,19.4442,4.5691,1.7314,57.352,1.3949,36.843,17.8173,
|
||||
2022-10-12,0.9706,142.34,1.9558,N/A,24.561,7.4399,N/A,0.8784,429.65,N/A,N/A,N/A,4.8495,N/A,4.94,11.02,N/A,N/A,0.9664,140.1,10.4145,7.529,N/A,N/A,18.0427,1.5525,5.1378,1.3395,6.9603,7.6192,14907.04,3.4623,79.8955,1384.66,19.4522,4.5448,1.7372,57.148,1.3941,36.902,17.6876,
|
||||
2022-10-11,0.9723,141.54,1.9558,N/A,24.535,7.439,N/A,0.87703,428.73,N/A,N/A,N/A,4.869,N/A,4.9394,11.0015,N/A,N/A,0.9675,140.7,10.4235,7.5293,N/A,N/A,18.0686,1.545,5.0456,1.3402,6.9669,7.6325,14930.83,3.4776,79.9555,1392.84,19.4115,4.5436,1.7323,57.243,1.3967,37.03,17.6153,
|
||||
2022-10-10,0.9697,141.16,1.9558,N/A,24.521,7.4384,N/A,0.8773,428.2,N/A,N/A,N/A,4.8655,N/A,4.94,10.9502,N/A,N/A,0.968,139.9,10.3378,7.528,N/A,N/A,18.0131,1.536,5.0328,1.3312,6.9344,7.612,14872.51,3.4463,79.9678,1384.26,19.3588,4.5091,1.7369,57.197,1.3939,36.81,17.5866,
|
||||
2022-10-07,0.9797,141.92,1.9558,N/A,24.517,7.4381,N/A,0.87383,423.85,N/A,N/A,N/A,4.8595,N/A,4.9415,10.8555,N/A,N/A,0.97,140.7,10.4498,7.527,N/A,N/A,18.209,1.5266,5.1075,1.3437,6.9715,7.6906,14933.14,3.4477,80.546,1381.42,19.643,4.5556,1.7328,57.747,1.3996,36.602,17.6222,
|
||||
2022-10-06,0.986,142.68,1.9558,N/A,24.479,7.439,N/A,0.87583,422.59,N/A,N/A,N/A,4.8505,N/A,4.9364,10.8728,N/A,N/A,0.9709,139.9,10.4278,7.5288,N/A,N/A,18.3191,1.5263,5.1185,1.3475,7.0164,7.74,15021.24,3.4846,81.0615,1388.39,19.817,4.5726,1.728,57.949,1.4057,36.827,17.5769,
|
||||
2022-10-05,0.9915,143.18,1.9558,N/A,24.524,7.4388,N/A,0.8734,423.6,N/A,N/A,N/A,4.791,N/A,4.9385,10.8376,N/A,N/A,0.9756,141.3,10.4858,7.5255,N/A,N/A,18.4201,1.538,5.1575,1.3493,7.0555,7.7831,15061.81,3.503,80.909,1406.71,19.8625,4.5911,1.7419,58.212,1.4124,37.102,17.6228,
|
||||
2022-10-04,0.9891,143.3,1.9558,N/A,24.544,7.4374,N/A,0.87273,417.68,N/A,N/A,N/A,4.8193,N/A,4.9418,10.8166,N/A,N/A,0.9767,141.9,10.4915,7.523,N/A,N/A,18.3374,1.5318,5.0589,1.3503,7.0384,7.7644,15080.18,3.4885,80.6995,1412.2,19.777,4.5939,1.7368,58.104,1.4148,37.16,17.5437,
|
||||
2022-10-03,0.9764,141.49,1.9558,N/A,24.527,7.4366,N/A,0.8707,424.86,N/A,N/A,N/A,4.832,N/A,4.9479,10.8743,N/A,N/A,0.9658,141.7,10.5655,7.5275,N/A,N/A,18.124,1.5128,5.178,1.3412,6.9481,7.6647,14969.79,3.498,79.898,1408.25,19.604,4.5383,1.7263,57.599,1.4015,37.181,17.5871,
|
||||
2022-09-30,0.9748,141.01,1.9558,N/A,24.549,7.4365,N/A,0.883,422.18,N/A,N/A,N/A,4.8483,N/A,4.949,10.8993,N/A,N/A,0.9561,140.9,10.5838,7.524,N/A,N/A,18.0841,1.5076,5.2584,1.3401,6.9368,7.6521,14863.26,3.4759,79.425,1400.69,19.6393,4.5201,1.7177,57.276,1.4001,36.823,17.5353,
|
||||
2022-09-29,0.9706,140.46,1.9558,N/A,24.687,7.4365,N/A,0.89485,421.93,N/A,N/A,N/A,4.857,N/A,4.9481,10.958,N/A,N/A,0.9538,140.1,10.4518,7.528,N/A,N/A,18,1.4982,5.2521,1.3294,6.9223,7.6192,14735.97,3.4422,79.314,1388.34,19.5779,4.4992,1.704,56.86,1.3961,36.946,17.4466,
|
||||
2022-09-28,0.9565,138.39,1.9558,N/A,24.65,7.4368,N/A,0.90268,411.72,N/A,N/A,N/A,4.8043,N/A,4.9485,10.9194,N/A,N/A,0.9437,139.7,10.4576,7.5313,N/A,N/A,17.7311,1.4924,5.1728,1.3157,6.9199,7.5084,14622.96,3.3931,78.2655,1378.84,19.5294,4.4281,1.6998,56.528,1.3846,36.687,17.2916,
|
||||
2022-09-27,0.9644,139.28,1.9558,N/A,24.661,7.4366,N/A,0.89275,406.65,N/A,N/A,N/A,4.764,N/A,4.9444,10.8533,N/A,N/A,0.9503,139.3,10.3473,7.528,N/A,N/A,17.824,1.4859,5.1235,1.3196,6.9156,7.5704,14604.52,3.3714,78.574,1370.06,19.5832,4.4466,1.6921,56.933,1.3838,36.565,17.2361,
|
||||
2022-09-26,0.9646,139.07,1.9558,N/A,24.64,7.4365,N/A,0.89404,408.83,N/A,N/A,N/A,4.7608,N/A,4.9418,10.9275,N/A,N/A,0.9555,138.9,10.3585,7.5278,N/A,N/A,17.8001,1.4858,5.1504,1.3195,6.9075,7.572,14620.74,3.4069,78.704,1379.4,19.6066,4.4401,1.6886,56.908,1.3842,36.496,17.4247,
|
||||
2022-09-23,0.9754,139.43,1.9558,N/A,24.658,7.4365,N/A,0.88201,406.3,N/A,N/A,N/A,4.7543,N/A,4.9433,10.9328,N/A,N/A,0.9565,139.9,10.2335,7.5228,N/A,N/A,17.9515,1.4828,5.0456,1.3177,6.9442,7.6567,14697.3,3.4152,79.0705,1381.97,19.5708,4.4659,1.6846,57.217,1.3897,36.636,17.3853,
|
||||
2022-09-22,0.9884,139.18,1.9558,N/A,24.657,7.4365,N/A,0.87256,405.25,N/A,N/A,N/A,4.7592,N/A,4.9411,10.8724,N/A,N/A,0.9684,139.9,10.235,7.5235,N/A,N/A,18.1559,1.484,5.0677,1.3278,6.9804,7.7583,14824.28,3.4216,79.897,1384.79,19.6129,4.514,1.6832,57.721,1.3998,36.803,17.3514,
|
||||
2022-09-21,0.9906,142.66,1.9558,N/A,24.637,7.4364,N/A,0.87335,405.1,N/A,N/A,N/A,4.7505,N/A,4.9443,10.9214,N/A,N/A,0.9549,140.3,10.2858,7.5205,N/A,N/A,18.149,1.4851,5.0924,1.3262,6.9821,7.7761,14866.28,3.4298,79.1555,1381.38,19.7847,4.5097,1.6844,57.285,1.4006,36.786,17.4879,
|
||||
2022-09-20,0.9986,143.34,1.9558,N/A,24.556,7.4368,N/A,0.87395,398.58,N/A,N/A,N/A,4.7208,N/A,4.934,10.8338,N/A,N/A,0.9644,140.9,10.273,7.5198,N/A,N/A,18.2833,1.4893,5.2139,1.3268,7.003,7.8382,14997.82,3.4406,79.6095,1390.71,19.9667,4.5516,1.6908,57.497,1.4074,36.968,17.7261,
|
||||
2022-09-19,0.999,143.42,1.9558,N/A,24.494,7.4373,N/A,0.87785,400.85,N/A,N/A,N/A,4.7058,N/A,4.93,10.7993,N/A,N/A,0.9658,139.9,10.2826,7.5215,N/A,N/A,18.2738,1.495,5.2886,1.3294,7.0066,7.8416,14975.43,3.446,79.653,1391.82,20.113,4.5455,1.6807,57.347,1.4082,36.983,17.7267,
|
||||
2022-09-16,0.9954,142.53,1.9558,N/A,24.497,7.4366,N/A,0.874,403.98,N/A,N/A,N/A,4.7143,N/A,4.9238,10.7541,N/A,N/A,0.9579,138.3,10.1985,7.5235,N/A,N/A,18.1923,1.4894,5.2279,1.3226,6.9787,7.8133,14904.67,3.4267,79.3605,1383.58,20.0028,4.5141,1.6717,57.111,1.4025,36.8,17.6004,
|
||||
2022-09-15,0.9992,143.43,1.9558,N/A,24.518,7.4366,N/A,0.86934,407.15,N/A,N/A,N/A,4.7273,N/A,4.9238,10.69,N/A,N/A,0.9572,138.7,10.1203,7.5258,N/A,N/A,18.2477,1.4853,5.1837,1.3172,6.9852,7.8423,14925.35,3.4384,79.7119,1397.18,20.0021,4.5314,1.6689,57.258,1.4062,36.816,17.5283,
|
||||
2022-09-14,0.999,143.08,1.9558,N/A,24.527,7.4366,N/A,0.86498,402.9,N/A,N/A,N/A,4.7163,N/A,4.9297,10.675,N/A,N/A,0.9612,139.7,10.1125,7.5195,N/A,N/A,18.2397,1.4873,5.1827,1.3177,6.955,7.8405,14903.93,3.4339,79.422,1391.97,20.028,4.5225,1.6675,57.054,1.4039,36.608,17.4342,
|
||||
2022-09-13,1.0175,144.5,1.9558,N/A,24.551,7.4366,N/A,0.86793,396.83,N/A,N/A,N/A,4.705,N/A,4.921,10.6108,N/A,N/A,0.9669,140.1,9.9988,7.5255,N/A,N/A,18.564,1.4736,5.1764,1.32,7.0467,7.9855,15099.17,3.4125,80.5453,1397.3,20.1615,4.5869,1.6555,57.665,1.4186,36.859,17.3112,
|
||||
2022-09-12,1.0155,144.49,1.9558,N/A,24.546,7.4365,N/A,0.86778,395.73,N/A,N/A,N/A,4.6965,N/A,4.9135,10.6368,N/A,N/A,0.9667,140.9,9.9718,7.5195,N/A,N/A,18.5232,1.4749,5.1933,1.3194,7.0348,7.9709,15083.83,3.4346,80.692,1397.58,20.1025,4.5733,1.6499,57.701,1.4168,36.873,17.322,
|
||||
2022-09-09,1.0049,143.3,1.9558,N/A,24.536,7.4365,N/A,0.8686,396.3,N/A,N/A,N/A,4.721,N/A,4.9019,10.6643,N/A,N/A,0.9657,140.9,9.9836,7.5245,N/A,N/A,18.3282,1.4704,5.2087,1.307,6.9543,7.8871,14905.33,3.4416,79.9685,1384.64,19.991,4.52,1.6463,57.098,1.4063,36.508,17.3753,
|
||||
2022-09-08,1.0009,143.65,1.9558,N/A,24.543,7.4365,N/A,0.86656,395.48,N/A,N/A,N/A,4.7155,N/A,4.8756,10.7075,N/A,N/A,0.9739,140.3,10.0615,7.515,N/A,N/A,18.2546,1.4824,5.2042,1.3134,6.9564,7.8568,14891.86,3.429,79.7375,1381.7,20.013,4.5051,1.6491,57.031,1.4054,36.418,17.3797,
|
||||
2022-09-07,0.9885,143.2,1.9558,N/A,24.631,7.4365,N/A,0.8651,401.83,N/A,N/A,N/A,4.729,N/A,4.8585,10.6888,N/A,N/A,0.975,141.3,9.9483,7.5143,N/A,N/A,18.0262,1.4748,5.1881,1.3037,6.8968,7.7596,14779.47,3.4053,79.028,1374.44,19.9225,4.4497,1.6459,56.532,1.3931,36.322,17.2582,
|
||||
2022-09-06,0.9928,140.91,1.9558,N/A,24.55,7.4365,N/A,0.85743,402.65,N/A,N/A,N/A,4.7068,N/A,4.8424,10.6825,N/A,N/A,0.9745,141.9,9.8945,7.5133,N/A,N/A,18.0938,1.4651,5.13,1.3029,6.9091,7.7932,14783.94,3.39,79.2305,1366.65,19.8545,4.4676,1.6313,56.655,1.3947,36.242,17.0805,
|
||||
2022-09-05,0.992,139.47,1.9558,N/A,24.622,7.4364,N/A,0.86358,403.9,N/A,N/A,N/A,4.736,N/A,4.8198,10.729,N/A,N/A,0.9747,142.7,9.9188,7.5173,N/A,N/A,18.0792,1.4616,5.1407,1.3043,6.8768,7.7867,14782.83,3.3826,79.2332,1359.98,19.8192,4.4563,1.6289,56.477,1.3932,36.263,17.088,
|
||||
2022-09-02,0.9993,140.36,1.9558,N/A,24.481,7.437,N/A,0.86478,398.38,N/A,N/A,N/A,4.7063,N/A,4.8335,10.7498,N/A,N/A,0.9839,141.5,10.0035,7.5225,N/A,N/A,18.2072,1.4671,5.2153,1.3131,6.9031,7.8439,14895.92,3.396,79.8096,1360.84,20.1024,4.4809,1.6394,56.854,1.4013,36.624,17.2791,
|
||||
2022-09-01,1.0004,139.34,1.9558,N/A,24.488,7.4372,N/A,0.86473,399.58,N/A,N/A,N/A,4.7128,N/A,4.8447,10.7415,N/A,N/A,0.9802,141.7,10.013,7.521,N/A,N/A,18.2149,1.4651,5.2239,1.3169,6.9017,7.8511,14878,3.3644,79.6195,1353.69,20.1954,4.4828,1.6389,56.609,1.4002,36.71,17.1524,
|
||||
2022-08-31,1,138.72,1.9558,N/A,24.55,7.4371,N/A,0.86035,402.8,N/A,N/A,N/A,4.7283,N/A,4.8595,10.6788,N/A,N/A,0.9796,141.7,9.9388,7.5148,N/A,N/A,18.1849,1.4591,5.1482,1.3111,6.8947,7.8488,14849.93,3.3399,79.5465,1342.79,20.2044,4.4755,1.6322,56.153,1.3969,36.45,17.0667,
|
||||
2022-08-30,1.0034,138.71,1.9558,N/A,24.577,7.4376,N/A,0.85645,406.38,N/A,N/A,N/A,4.7323,N/A,4.8657,10.65,N/A,N/A,0.9741,142.1,9.7553,7.5103,N/A,N/A,18.239,1.4472,5.0286,1.3047,6.9233,7.8751,14875,3.3168,79.8025,1350.92,20.0077,4.4907,1.6245,56.393,1.3997,36.494,16.8567,
|
||||
2022-08-29,0.9986,138.49,1.9558,N/A,24.592,7.4379,N/A,0.8542,409.9,N/A,N/A,N/A,4.745,N/A,4.8699,10.628,N/A,N/A,0.967,141.1,9.7675,7.5119,N/A,N/A,18.1605,1.4529,5.0663,1.3026,6.9044,7.8368,14871.09,3.3146,79.8295,1347.47,19.9876,4.4837,1.6305,56.187,1.395,36.399,16.8891,
|
||||
2022-08-26,1.0007,137.02,1.9558,N/A,24.635,7.4379,N/A,0.8459,410.48,N/A,N/A,N/A,4.7485,N/A,4.8729,10.5703,N/A,N/A,0.9642,140.3,9.667,7.5135,N/A,N/A,18.1923,1.4333,5.1069,1.2944,6.8671,7.8521,14825.57,3.2537,79.9025,1333.46,19.924,4.4706,1.6112,56.109,1.3906,36.035,16.7995,
|
||||
2022-08-25,0.997,136.07,1.9558,N/A,24.648,7.4374,N/A,0.84293,408.93,N/A,N/A,N/A,4.7578,N/A,4.8758,10.5525,N/A,N/A,0.9616,140.3,9.64,7.514,N/A,N/A,18.112,1.4306,5.0879,1.2881,6.8317,7.8234,14753.15,3.2791,79.6555,1331.98,19.8132,4.4586,1.6006,55.842,1.3857,35.732,16.7903,
|
||||
2022-08-24,0.9934,135.74,1.9558,N/A,24.629,7.4381,N/A,0.84283,410.93,N/A,N/A,N/A,4.7668,N/A,4.88,10.586,N/A,N/A,0.9576,139.5,9.636,7.5125,N/A,N/A,18.0362,1.4389,5.0606,1.2908,6.822,7.795,14757.6,3.2599,79.3006,1332.84,19.7781,4.4559,1.6065,55.7,1.3857,35.906,16.8976,
|
||||
2022-08-23,0.9927,136.34,1.9558,N/A,24.658,7.4374,N/A,0.84343,410.55,N/A,N/A,N/A,4.7788,N/A,4.8839,10.6063,N/A,N/A,0.9602,140.1,9.7438,7.5128,N/A,N/A,17.983,1.4437,5.0984,1.2928,6.7952,7.79,14743.28,3.2598,79.2805,1332.1,19.9152,4.4557,1.6071,55.661,1.386,35.856,16.9568,
|
||||
2022-08-22,1.0001,137.08,1.9558,N/A,24.651,7.437,N/A,0.84658,408,N/A,N/A,N/A,4.764,N/A,4.8853,10.65,N/A,N/A,0.958,140.9,9.7719,7.5113,N/A,N/A,18.1268,1.4478,5.1752,1.2989,6.8457,7.8468,14895.47,3.2778,79.8615,1344.21,20.1583,4.4854,1.6125,56.218,1.3958,36.099,17.0766,
|
||||
2022-08-19,1.0054,137.67,1.9558,N/A,24.625,7.4373,N/A,0.84938,407.35,N/A,N/A,N/A,4.751,N/A,4.8811,10.6095,N/A,N/A,0.9616,140.5,9.8418,7.5155,N/A,N/A,18.2028,1.4584,5.2334,1.3062,6.8531,7.8877,14951.91,3.2879,80.2988,1343.39,20.3273,4.5007,1.6203,56.325,1.3979,35.988,17.1007,
|
||||
2022-08-18,1.0178,137.17,1.9558,N/A,24.611,7.4389,N/A,0.84391,405.13,N/A,N/A,N/A,4.724,N/A,4.8808,10.5903,N/A,N/A,0.9683,140.5,9.8283,7.5215,N/A,N/A,18.4095,1.4617,5.2326,1.3118,6.906,7.9844,15092.62,3.2993,81.042,1344.81,20.313,4.5536,1.6145,56.776,1.4053,36.254,17.0004,
|
||||
2022-08-17,1.0164,137.36,1.9558,N/A,24.566,7.4377,N/A,0.84208,404.28,N/A,N/A,N/A,4.7078,N/A,4.8825,10.5617,N/A,N/A,0.9686,140.3,9.8428,7.5071,N/A,N/A,18.2568,1.4655,5.2838,1.3117,6.8917,7.9705,15015.81,3.311,80.7555,1337.02,20.3825,4.5413,1.6165,56.772,1.4051,36.052,16.9125,
|
||||
2022-08-16,1.0131,136.11,1.9558,N/A,24.54,7.4368,N/A,0.84218,406.2,N/A,N/A,N/A,4.7043,N/A,4.882,10.5365,N/A,N/A,0.9625,140.3,9.8428,7.51,N/A,N/A,18.1994,1.4463,5.1835,1.3076,6.8767,7.9449,14968.68,3.3087,80.3745,1329.66,20.1595,4.5245,1.6012,56.602,1.398,35.93,16.6556,
|
||||
2022-08-15,1.0195,135.61,1.9558,N/A,24.46,7.4373,N/A,0.84375,398.6,N/A,N/A,N/A,4.6858,N/A,4.8849,10.498,N/A,N/A,0.9631,140.3,9.871,7.5028,N/A,N/A,18.3143,1.4508,5.2268,1.3167,6.905,7.9899,15042.36,3.3294,81.061,1336.35,20.3914,4.5465,1.6002,57.122,1.4036,36.218,16.7375,
|
||||
2022-08-12,1.0285,137.47,1.9558,N/A,24.38,7.4395,N/A,0.84715,392.3,N/A,N/A,N/A,4.6773,N/A,4.8915,10.4515,N/A,N/A,0.9689,140.3,9.813,7.5138,N/A,N/A,18.4733,1.4496,5.3007,1.3148,6.9352,8.06,15104.2,3.345,81.9935,1342.59,20.4925,4.5709,1.5985,57.246,1.4106,36.393,16.7318,
|
||||
2022-08-11,1.0338,136.57,1.9558,N/A,24.346,7.4395,N/A,0.84575,394.18,N/A,N/A,N/A,4.6828,N/A,4.9055,10.36,N/A,N/A,0.9712,139.7,9.804,7.515,N/A,N/A,18.5674,1.4532,5.2447,1.3202,6.9668,8.1118,15215.93,3.3505,82.2845,1344.62,20.6398,4.5952,1.6045,57.225,1.4151,36.343,16.7083,
|
||||
2022-08-10,1.0252,138.16,1.9558,N/A,24.397,7.4397,N/A,0.84608,397.65,N/A,N/A,N/A,4.7063,N/A,4.9138,10.3773,N/A,N/A,0.9713,139.7,9.9118,7.5158,N/A,N/A,18.4099,1.4682,5.251,1.3207,6.9222,8.046,15218.87,3.3773,81.468,1344.16,20.713,4.5693,1.6211,57.061,1.4107,36.42,16.8788,
|
||||
2022-08-09,1.0234,138.26,1.9558,N/A,24.532,7.4407,N/A,0.8452,397.35,N/A,N/A,N/A,4.7085,N/A,4.9038,10.3875,N/A,N/A,0.9763,140.1,9.9365,7.514,N/A,N/A,18.3342,1.4687,5.2478,1.3163,6.9106,8.0334,15197.09,3.3865,81.406,1336.74,20.7145,4.5592,1.6304,56.939,1.411,36.264,17.05,
|
||||
2022-08-08,1.0199,137.62,1.9558,N/A,24.515,7.4405,N/A,0.84165,394.27,N/A,N/A,N/A,4.7043,N/A,4.9163,10.365,N/A,N/A,0.9763,140.1,9.9405,7.5123,N/A,N/A,18.3175,1.4607,5.238,1.3134,6.8931,8.0061,15147.65,3.3895,81.166,1329.93,20.681,4.5477,1.6202,56.554,1.4058,36.38,16.9694,
|
||||
2022-08-05,1.0233,136.22,1.9558,N/A,24.581,7.4415,N/A,0.84268,393.78,N/A,N/A,N/A,4.7085,N/A,4.9251,10.3573,N/A,N/A,0.9776,138.9,9.982,7.5148,N/A,N/A,18.3853,1.4713,5.3348,1.3185,6.9068,8.0328,15236.2,3.4033,81.0469,1324.53,20.8367,4.5598,1.6248,56.524,1.4077,36.373,17.0342,
|
||||
2022-08-04,1.0181,135.81,1.9558,N/A,24.659,7.4425,N/A,0.84231,395.98,N/A,N/A,N/A,4.7233,N/A,4.9261,10.374,N/A,N/A,0.9765,138.7,9.9065,7.515,N/A,N/A,18.2922,1.4607,5.3614,1.307,6.8769,7.9919,15200.94,3.4101,80.7715,1333.7,20.756,4.5387,1.6172,56.633,1.4037,36.606,17.0352,
|
||||
2022-08-03,1.0194,136.18,1.9558,N/A,24.65,7.4427,N/A,0.83629,395.03,N/A,N/A,N/A,4.691,N/A,4.9245,10.3913,N/A,N/A,0.9773,139.3,9.8743,7.5178,N/A,N/A,18.3112,1.4681,5.3547,1.3085,6.883,8.0022,15184.35,3.4362,80.6895,1336.03,21.0788,4.5435,1.6266,56.855,1.4069,36.963,17.1283,
|
||||
2022-08-02,1.0224,133.9,1.9558,N/A,24.644,7.4432,N/A,0.83665,396.82,N/A,N/A,N/A,4.7063,N/A,4.9298,10.3995,N/A,N/A,0.9744,139.3,9.9305,7.5195,N/A,N/A,18.3564,1.4745,5.3175,1.315,6.9117,8.0257,15204.85,3.4486,80.3243,1339.16,20.952,4.5531,1.6254,56.834,1.4103,36.914,16.982,
|
||||
2022-08-01,1.0233,135.38,1.9558,N/A,24.628,7.4457,N/A,0.837,401.35,N/A,N/A,N/A,4.734,N/A,4.9283,10.3668,N/A,N/A,0.9717,138.7,9.8638,7.521,N/A,N/A,18.3475,1.4535,5.2723,1.3076,6.9105,8.0329,15203.21,3.4546,80.9335,1333.3,20.7635,4.5568,1.616,56.734,1.4087,36.977,16.8613,
|
||||
2022-07-29,1.0198,136.42,1.9558,N/A,24.61,7.4438,N/A,0.8399,404.8,N/A,N/A,N/A,4.7375,N/A,4.9343,10.3875,N/A,N/A,0.9744,138.3,9.8773,7.518,N/A,N/A,18.2472,1.4646,5.2739,1.31,6.8705,8.0054,15155.56,3.4714,80.882,1329.4,20.6745,4.5386,1.6283,56.375,1.4088,36.978,16.8627,
|
||||
2022-07-28,1.0122,137.26,1.9558,N/A,24.609,7.4442,N/A,0.83586,407.3,N/A,N/A,N/A,4.7908,N/A,4.9342,10.449,N/A,N/A,0.9745,138.7,9.8983,7.52,N/A,N/A,18.1417,1.4535,5.33,1.2986,6.8325,7.9456,15107.59,3.4701,80.6535,1320.45,20.676,4.5071,1.6172,56.592,1.4009,37.097,17.0011,
|
||||
2022-07-27,1.0152,138.89,1.9558,N/A,24.575,7.4446,N/A,0.84138,404.67,N/A,N/A,N/A,4.7978,N/A,4.9334,10.4545,N/A,N/A,0.9768,139.1,9.9558,7.514,N/A,N/A,18.1859,1.462,5.4039,1.3049,6.8534,7.9692,15213.07,3.4855,81.135,1333.99,20.753,4.5263,1.6306,56.455,1.4088,37.4,17.1347,
|
||||
2022-07-26,1.0124,138.35,1.9558,N/A,24.607,7.4449,N/A,0.84558,400.99,N/A,N/A,N/A,4.742,N/A,4.9324,10.4445,N/A,N/A,0.9765,139.1,10.0105,7.5145,N/A,N/A,18.0705,1.4605,5.4437,1.3035,6.8451,7.9466,15185.27,3.4891,80.805,1326.65,20.7845,4.5113,1.6235,56.16,1.4066,37.18,17.087,
|
||||
2022-07-25,1.0236,139.84,1.9558,N/A,24.535,7.4449,N/A,0.84813,396.5,N/A,N/A,N/A,4.708,N/A,4.9339,10.3973,N/A,N/A,0.9869,139.5,10.0704,7.5195,N/A,N/A,18.2653,1.4707,5.5976,1.3168,6.9094,8.0345,15303.87,3.5201,81.6675,1341.25,20.9376,4.5586,1.6325,57.209,1.4176,37.525,17.1502,
|
||||
2022-07-22,1.019,139.51,1.9558,N/A,24.514,7.4443,N/A,0.85141,398.3,N/A,N/A,N/A,4.7508,N/A,4.9321,10.4328,N/A,N/A,0.9832,139.5,10.1498,7.5234,N/A,N/A,18.094,1.4677,5.5821,1.3105,6.8852,7.9985,15275.69,3.5083,81.384,1335.66,20.9595,4.5366,1.6265,57.24,1.4151,37.392,17.2009,
|
||||
2022-07-21,1.0199,141.46,1.9558,N/A,24.496,7.4446,N/A,0.85545,400.13,N/A,N/A,N/A,4.761,N/A,4.9391,10.426,N/A,N/A,0.9924,139.7,10.175,7.52,N/A,N/A,18.0327,1.4848,5.5777,1.3178,6.904,8.0056,15342.25,3.5203,81.451,1337.47,20.941,4.5457,1.6479,57.512,1.4218,37.66,17.5195,
|
||||
2022-07-20,1.0199,140.92,1.9558,N/A,24.493,7.4452,N/A,0.85178,399.5,N/A,N/A,N/A,4.782,N/A,4.9396,10.4606,N/A,N/A,0.9896,139.5,10.1323,7.5143,N/A,N/A,17.9444,1.4767,5.5427,1.3132,6.8892,8.0062,15275.82,3.5147,81.599,1337.61,20.8967,4.5406,1.6308,57.398,1.4204,37.405,17.3924,
|
||||
2022-07-19,1.0245,141.01,1.9558,N/A,24.555,7.4449,N/A,0.85303,397.45,N/A,N/A,N/A,4.7598,N/A,4.9395,10.4964,N/A,N/A,0.9918,138.9,10.176,7.5093,N/A,N/A,18.018,1.4869,5.5454,1.3264,6.9064,8.0423,15344.49,3.5295,81.898,1340.33,20.8552,4.559,1.6456,57.583,1.4269,37.492,17.457,
|
||||
2022-07-18,1.0131,140.16,1.9558,N/A,24.508,7.4435,N/A,0.84708,402.05,N/A,N/A,N/A,4.776,N/A,4.9389,10.5265,N/A,N/A,0.9911,138.9,10.2553,7.513,N/A,N/A,17.7225,1.4839,5.4505,1.3151,6.8266,7.9528,15157.63,3.5004,81.034,1333.33,20.7095,4.5113,1.6432,57.056,1.4153,37.13,17.383,
|
||||
2022-07-15,1.0059,139.49,1.9558,N/A,24.561,7.443,N/A,0.84988,403.73,N/A,N/A,N/A,4.7953,N/A,4.9407,10.5943,N/A,N/A,0.9849,138.9,10.2763,7.516,N/A,N/A,17.5451,1.4886,5.4434,1.3147,6.7943,7.8963,15081.58,3.5044,80.316,1333.79,20.9209,4.4752,1.6377,56.678,1.4113,36.866,17.2875,
|
||||
2022-07-14,1.0005,139.04,1.9558,N/A,24.417,7.4425,N/A,0.8456,408.78,N/A,N/A,N/A,4.8146,N/A,4.942,10.6019,N/A,N/A,0.9841,138.89,10.2536,7.5122,N/A,N/A,17.494,1.4893,5.4586,1.3162,6.7618,7.8539,15119.56,3.4915,80.0752,1322.29,20.9585,4.4462,1.6437,56.565,1.4065,36.632,17.202,
|
||||
2022-07-13,1.0067,138.02,1.9558,N/A,24.397,7.4416,N/A,0.84371,409.35,N/A,N/A,N/A,4.824,N/A,4.9414,10.602,N/A,N/A,0.9829,138.3,10.2428,7.5155,N/A,N/A,17.5629,1.4802,5.4533,1.3073,6.7722,7.9025,15117.08,3.4864,80.1285,1311.4,20.9029,4.4667,1.635,56.669,1.4134,36.377,17.0527,
|
||||
2022-07-12,1.0042,137.31,1.9558,N/A,24.582,7.4408,N/A,0.84823,409.98,N/A,N/A,N/A,4.819,N/A,4.9413,10.629,N/A,N/A,0.9883,139.1,10.2754,7.517,N/A,N/A,17.4392,1.49,5.4009,1.3094,6.7518,7.8828,15054.05,3.4987,79.8965,1315.1,20.8883,4.4556,1.6395,56.648,1.4127,36.392,17.1509,
|
||||
2022-07-11,1.0098,138.77,1.9558,N/A,24.592,7.4414,N/A,0.8454,408.22,N/A,N/A,N/A,4.7968,N/A,4.943,10.6943,N/A,N/A,0.9908,139.3,10.299,7.518,N/A,N/A,17.5447,1.491,5.3673,1.314,6.7793,7.9265,15131.92,3.5108,80.2435,1320.36,20.7845,4.4704,1.6466,56.574,1.4178,36.585,17.1634,
|
||||
2022-07-08,1.0163,138.05,1.9558,N/A,24.614,7.4424,N/A,0.84585,402.45,N/A,N/A,N/A,4.763,N/A,4.9431,10.6665,N/A,N/A,0.9913,139.5,10.263,7.519,N/A,N/A,17.6026,1.4871,5.4345,1.3201,6.8095,7.9769,15210.73,3.5325,80.528,1321.61,20.8477,4.4992,1.6464,56.882,1.4228,36.602,17.1922,
|
||||
2022-07-07,1.018,138.11,1.9558,N/A,24.779,7.4405,N/A,0.85105,410.04,N/A,N/A,N/A,4.7721,N/A,4.9448,10.723,N/A,N/A,0.9906,139.3,10.291,7.5193,N/A,N/A,17.5551,1.4883,5.4983,1.3227,6.823,7.9893,15265.27,3.5548,80.6,1324.66,20.9675,4.5077,1.6461,56.939,1.4255,36.74,17.0372,
|
||||
2022-07-06,1.0177,137.71,1.9558,N/A,24.778,7.4403,N/A,0.85676,411.8,N/A,N/A,N/A,4.771,N/A,4.944,10.745,N/A,N/A,0.9896,138.5,10.2803,7.5198,N/A,N/A,17.5505,1.4961,5.5116,1.3274,6.8289,7.9864,15287.49,3.5884,80.5321,1331.69,21.0194,4.5028,1.6505,56.779,1.4305,36.78,17.0246,
|
||||
2022-07-05,1.029,139.77,1.9558,N/A,24.751,7.4396,N/A,0.85845,407.38,N/A,N/A,N/A,4.7448,N/A,4.9438,10.8031,N/A,N/A,0.9932,139.1,10.285,7.5246,N/A,N/A,17.5049,1.518,5.5141,1.3364,6.9029,8.0748,15487.93,3.6343,81.673,1348.97,21.0171,4.5477,1.6772,57.009,1.4455,36.879,16.9143,
|
||||
2022-07-04,1.0455,141.51,1.9558,N/A,24.745,7.4391,N/A,0.8596,401.52,N/A,N/A,N/A,4.71,N/A,4.944,10.7658,N/A,N/A,1.0037,139.3,10.2958,7.5301,N/A,N/A,17.5994,1.5205,5.5663,1.3435,6.9977,8.2033,15684.13,3.6655,82.5067,1353.4,21.1972,4.6138,1.6748,57.487,1.4587,37.298,17.0275,
|
||||
2022-07-01,1.0425,141.05,1.9558,N/A,24.753,7.4391,N/A,0.86648,401.11,N/A,N/A,N/A,4.7168,N/A,4.9457,10.7783,N/A,N/A,1.0027,138.7,10.3651,7.531,N/A,N/A,17.4608,1.5382,5.5117,1.3492,6.987,8.1801,15621.64,3.6717,82.3747,1352.58,21.115,4.5943,1.6929,57.452,1.4565,37.186,17.1323,
|
||||
2022-06-30,1.0387,141.54,1.9558,N/A,24.739,7.4392,N/A,0.8582,397.04,N/A,N/A,N/A,4.6904,N/A,4.9464,10.73,N/A,N/A,0.996,138.9,10.3485,7.5307,N/A,N/A,17.322,1.5099,5.4229,1.3425,6.9624,8.1493,15552,3.6392,82.113,1351.6,20.9641,4.5781,1.6705,57.15,1.4483,36.754,17.0143,
|
||||
2022-06-29,1.0517,143.53,1.9558,N/A,24.739,7.4392,N/A,0.86461,394.28,N/A,N/A,N/A,4.6869,N/A,4.9419,10.6848,N/A,N/A,1.0005,139.9,10.3065,7.5285,N/A,N/A,17.4998,1.5256,5.5163,1.3513,7.0382,8.2532,15612.61,3.6344,83.037,1364.02,21.1375,4.6272,1.6871,57.773,1.4607,36.925,16.9295,
|
||||
2022-06-28,1.0561,143.67,1.9558,N/A,24.726,7.4394,N/A,0.8635,398.55,N/A,N/A,N/A,4.6905,N/A,4.9443,10.6543,N/A,N/A,1.0101,139.5,10.337,7.532,N/A,N/A,17.5891,1.521,5.5308,1.3565,7.0775,8.288,15669.91,3.6267,83.408,1361.75,21.088,4.6432,1.6822,57.85,1.4645,37.154,16.9072,
|
||||
2022-06-27,1.0572,143.25,1.9558,N/A,24.724,7.4408,N/A,0.862,402.62,N/A,N/A,N/A,4.699,N/A,4.944,10.6713,N/A,N/A,1.0143,139.7,10.408,7.5333,N/A,N/A,17.4794,1.5278,5.5446,1.3639,7.0737,8.2946,15635.36,3.6009,82.9325,1357.68,21.041,4.6559,1.6801,57.9,1.4641,37.361,16.7967,
|
||||
2022-06-24,1.0524,142.19,1.9558,N/A,24.731,7.4398,N/A,0.85773,401.34,N/A,N/A,N/A,4.7023,N/A,4.9463,10.694,N/A,N/A,1.0072,139.7,10.4345,7.5295,N/A,N/A,18.2856,1.5248,5.4851,1.3657,7.0478,8.2609,15633.96,3.621,82.3985,1364.09,20.9901,4.6327,1.6731,57.83,1.462,37.36,16.7137,
|
||||
2022-06-23,1.0493,142.11,1.9558,N/A,24.75,7.4388,N/A,0.85818,399.6,N/A,N/A,N/A,4.7085,N/A,4.9468,10.705,N/A,N/A,1.013,139.9,10.475,7.5286,N/A,N/A,18.2199,1.5212,5.4515,1.36,7.0367,8.236,15594.75,3.6192,82.1489,1367.21,21.0924,4.623,1.6713,57.44,1.4589,37.261,16.817,
|
||||
2022-06-22,1.0521,143.11,1.9558,N/A,24.712,7.4387,N/A,0.85885,396,N/A,N/A,N/A,4.6905,N/A,4.9467,10.6688,N/A,N/A,1.0153,138.7,10.5045,7.5228,N/A,N/A,18.255,1.5254,5.4349,1.366,7.0604,8.2589,15618.98,3.6432,82.4075,1369.29,21.1491,4.6345,1.6835,57.174,1.4615,37.281,16.7985,
|
||||
2022-06-21,1.055,143.75,1.9558,N/A,24.69,7.4393,N/A,0.8601,396.48,N/A,N/A,N/A,4.6435,N/A,4.9462,10.646,N/A,N/A,1.0214,138.7,10.3283,7.5205,N/A,N/A,18.3049,1.5177,5.442,1.366,7.068,8.2817,15639.62,3.6505,82.423,1365.09,21.2485,4.6399,1.6675,57.304,1.4612,37.294,16.7881,
|
||||
2022-06-20,1.0517,141.94,1.9558,N/A,24.728,7.4387,N/A,0.85748,397.85,N/A,N/A,N/A,4.652,N/A,4.9453,10.6375,N/A,N/A,1.0162,137.3,10.4085,7.5175,N/A,N/A,18.2239,1.5061,5.4117,1.3662,7.0346,8.2558,15589.89,3.639,81.994,1357.54,21.3016,4.6291,1.6549,56.872,1.4589,37.157,16.8603,
|
||||
2022-06-17,1.0486,141.21,1.9558,N/A,24.742,7.4384,N/A,0.855,400.53,N/A,N/A,N/A,4.7003,N/A,4.9469,10.6748,N/A,N/A,1.0105,137.7,10.4525,7.5155,N/A,N/A,18.1495,1.5039,5.3824,1.3631,7.0308,8.2314,15537.05,3.6112,81.871,1356.27,21.4474,4.6159,1.6601,56.371,1.4547,36.974,16.7133,
|
||||
2022-06-16,1.04,138.24,1.9558,N/A,24.742,7.4386,N/A,0.8555,398.1,N/A,N/A,N/A,4.7138,N/A,4.9443,10.6942,N/A,N/A,1.0142,137.5,10.4588,7.5245,N/A,N/A,18.0126,1.4939,5.2559,1.3446,6.9844,8.1638,15427.31,3.5934,81.1945,1346.88,21.4115,4.5786,1.6608,55.7,1.4451,36.566,16.6052,
|
||||
2022-06-15,1.0431,140.49,1.9558,N/A,24.703,7.4392,N/A,0.86328,397.96,N/A,N/A,N/A,4.669,N/A,4.9427,10.6278,N/A,N/A,1.0435,137.5,10.3868,7.5245,N/A,N/A,18.0465,1.5051,5.3164,1.3498,7.0013,8.1883,15361.97,3.6007,81.5142,1346.86,21.4763,4.6037,1.6706,55.627,1.4519,36.529,16.7111,
|
||||
2022-06-14,1.0452,140.62,1.9558,N/A,24.749,7.4403,N/A,0.86578,398.68,N/A,N/A,N/A,4.6563,N/A,4.9443,10.622,N/A,N/A,1.0394,138.3,10.3945,7.5238,N/A,N/A,18.06,1.5174,5.3329,1.3522,7.0417,8.2048,15400.02,3.6208,81.559,1346.72,21.4832,4.6224,1.6755,55.669,1.4541,36.566,16.7959,
|
||||
2022-06-13,1.0455,140.51,1.9558,N/A,24.724,7.4397,N/A,0.8585,399.3,N/A,N/A,N/A,4.6373,N/A,4.9459,10.616,N/A,N/A,1.0375,138.7,10.3222,7.5215,N/A,N/A,18.0495,1.4998,5.2785,1.3435,7.0434,8.2071,15376.17,3.5994,81.606,1349.93,21.2102,4.6195,1.6635,55.72,1.4538,36.425,16.807,
|
||||
2022-06-10,1.0578,141.69,1.9558,N/A,24.705,7.4389,N/A,0.85048,398.48,N/A,N/A,N/A,4.6053,N/A,4.9442,10.5255,N/A,N/A,1.0404,137.7,10.1495,7.5225,N/A,N/A,18.0116,1.4845,5.1718,1.3484,7.0868,8.3031,15393.27,3.5626,82.3355,1344.25,20.8285,4.6564,1.6482,56.101,1.462,36.774,16.5209,
|
||||
2022-06-09,1.0743,143.93,1.9558,N/A,24.689,7.4391,N/A,0.85653,396.45,N/A,N/A,N/A,4.5925,N/A,4.9453,10.5045,N/A,N/A,1.0495,138.7,10.1818,7.5223,N/A,N/A,18.5104,1.4985,5.2506,1.3506,7.1722,8.4317,15646.25,3.5859,83.526,1348.99,21.0248,4.7199,1.6673,56.872,1.4779,37.079,16.4132,
|
||||
2022-06-08,1.0739,143.92,1.9558,N/A,24.622,7.4386,N/A,0.85575,391.25,N/A,N/A,N/A,4.5698,N/A,4.945,10.4938,N/A,N/A,1.0486,138.9,10.1395,7.5215,N/A,N/A,18.453,1.4917,5.2447,1.3467,7.1785,8.4275,15577.86,3.5848,83.414,1349.34,21.0458,4.7187,1.6644,56.799,1.4769,37.076,16.4626,
|
||||
2022-06-07,1.0662,141.66,1.9558,N/A,24.739,7.4395,N/A,0.85365,389.33,N/A,N/A,N/A,4.5813,N/A,4.9426,10.5039,N/A,N/A,1.0423,138.9,10.1843,7.5244,N/A,N/A,17.8702,1.4884,5.1256,1.3437,7.1146,8.3656,15412.37,3.5661,82.873,1340.75,20.8435,4.6865,1.6582,56.421,1.4685,36.768,16.4059,
|
||||
2022-06-06,1.0726,140.16,1.9558,N/A,24.715,7.439,N/A,0.85415,388.05,N/A,N/A,N/A,4.5808,N/A,4.9424,10.452,N/A,N/A,1.032,138.3,10.0853,7.5222,N/A,N/A,17.796,1.4842,5.0986,1.3463,7.1223,8.4154,15464.73,3.5697,83.245,1341.13,20.9078,4.7076,1.6428,56.67,1.4732,36.774,16.4142,
|
||||
2022-06-03,1.073,139.59,1.9558,N/A,24.708,7.4388,N/A,0.8542,394.78,N/A,N/A,N/A,4.5955,N/A,4.9428,10.4589,N/A,N/A,1.0296,137.9,10.103,7.522,N/A,N/A,17.738,1.4805,5.1643,1.3484,7.1465,8.4167,15498.1,3.5751,83.273,1337.4,20.985,4.7094,1.6409,56.738,1.4741,36.777,16.6153,
|
||||
2022-06-02,1.0692,138.72,1.9558,N/A,24.702,7.4391,N/A,0.85195,394.9,N/A,N/A,N/A,4.5787,N/A,4.9398,10.4705,N/A,N/A,1.0264,136.9,10.0845,7.5325,N/A,N/A,17.6175,1.4829,5.1335,1.352,7.135,8.3896,15481.86,3.5705,82.922,1334.06,20.9831,4.6949,1.6413,56.467,1.4701,36.754,16.6143,
|
||||
2022-06-01,1.0712,138.68,1.9558,N/A,24.748,7.4393,N/A,0.85158,395.03,N/A,N/A,N/A,4.5913,N/A,4.9428,10.4758,N/A,N/A,1.0305,137.1,10.0438,7.5345,N/A,N/A,17.6223,1.4861,5.0646,1.3536,7.1586,8.4057,15574.33,3.5682,83.051,1331.44,21.0678,4.6951,1.6442,56.182,1.47,36.801,16.609,
|
||||
2022-05-31,1.0713,137.36,1.9558,N/A,24.714,7.4394,N/A,0.85138,396.2,N/A,N/A,N/A,4.5805,N/A,4.9408,10.5053,N/A,N/A,1.0281,136.3,10.0983,7.541,N/A,N/A,17.5817,1.4933,5.0965,1.3573,7.1402,8.4063,15580.15,3.5746,83.231,1329.32,20.987,4.6907,1.6459,56.323,1.4687,36.751,16.745,
|
||||
2022-05-30,1.0764,137.25,1.9558,N/A,24.712,7.4391,N/A,0.8515,392.18,N/A,N/A,N/A,4.5855,N/A,4.9441,10.518,N/A,N/A,1.0327,136.9,10.1256,7.5305,N/A,N/A,17.6416,1.4982,5.0629,1.3647,7.1735,8.449,15682.41,3.5722,83.475,1331.66,20.8994,4.6998,1.6439,56.322,1.4719,36.7,16.648,
|
||||
2022-05-27,1.0722,136.05,1.9558,N/A,24.7,7.4392,N/A,0.84875,392.83,N/A,N/A,N/A,4.5858,N/A,4.9427,10.5293,N/A,N/A,1.0258,137.9,10.179,7.5379,N/A,N/A,17.582,1.4995,5.0959,1.3661,7.1831,8.4165,15583.97,3.597,83.1915,1343.63,21.136,4.6952,1.6426,56.02,1.4679,36.589,16.746,
|
||||
2022-05-26,1.0697,135.95,1.9558,N/A,24.676,7.4409,N/A,0.85073,391.72,N/A,N/A,N/A,4.6083,N/A,4.9423,10.5983,N/A,N/A,1.0283,138.1,10.2715,7.5355,N/A,N/A,17.5588,1.511,5.1741,1.3715,7.2024,8.397,15628.91,3.5935,83.0065,1352.69,21.1935,4.7045,1.6541,55.975,1.4709,36.589,16.9312,
|
||||
2022-05-25,1.0656,135.34,1.9558,N/A,24.648,7.4405,N/A,0.85295,388.25,N/A,N/A,N/A,4.621,N/A,4.9416,10.5419,N/A,N/A,1.0269,138.3,10.2704,7.5355,N/A,N/A,17.3954,1.5126,5.1736,1.372,7.1334,8.3647,15587.59,3.585,82.6666,1354.61,21.2213,4.6833,1.6539,55.787,1.4676,36.55,16.7628,
|
||||
2022-05-24,1.072,136.49,1.9558,N/A,24.663,7.4411,N/A,0.8575,383.33,N/A,N/A,N/A,4.6015,N/A,4.9446,10.5013,N/A,N/A,1.0334,139.3,10.289,7.5285,N/A,N/A,17.2572,1.5152,5.1793,1.3714,7.1449,8.4143,15711.88,3.5848,83.185,1353.65,21.2456,4.7076,1.6656,56.152,1.4722,36.609,16.7814,
|
||||
2022-05-23,1.0659,136.05,1.9558,N/A,24.594,7.4413,N/A,0.84783,381.65,N/A,N/A,N/A,4.621,N/A,4.947,10.4918,N/A,N/A,1.031,139.1,10.252,7.5275,N/A,N/A,16.8672,1.4982,5.1623,1.3626,7.085,8.3664,15609,3.5745,82.6795,1344.19,21.1273,4.6782,1.6463,55.686,1.4639,36.41,16.7437,
|
||||
2022-05-20,1.0577,135.34,1.9558,N/A,24.67,7.4424,N/A,0.8482,382.93,N/A,N/A,N/A,4.6365,N/A,4.9477,10.4915,N/A,N/A,1.028,138.5,10.262,7.5335,N/A,N/A,16.8201,1.498,5.1989,1.3526,7.0638,8.2999,15501.99,3.533,82.1617,1340.58,21.0314,4.6422,1.6518,55.181,1.4588,36.284,16.7131,
|
||||
2022-05-19,1.0525,134.46,1.9558,N/A,24.7,7.4423,N/A,0.84728,385.83,N/A,N/A,N/A,4.6423,N/A,4.9474,10.5098,N/A,N/A,1.0265,139.5,10.3102,7.5395,N/A,N/A,16.8037,1.5036,5.2094,1.349,7.1028,8.2594,15416.76,3.5623,81.7115,1343.21,21.0043,4.6363,1.6551,55.14,1.4576,36.343,16.8315,
|
||||
2022-05-18,1.0523,135.76,1.9558,N/A,24.647,7.4419,N/A,0.8467,382.88,N/A,N/A,N/A,4.6443,N/A,4.9473,10.4675,N/A,N/A,1.0486,138.9,10.2125,7.535,N/A,N/A,16.7811,1.498,5.1974,1.3488,7.0972,8.2591,15446,3.526,81.6455,1332.76,20.9204,4.6254,1.6548,55.077,1.4598,36.399,16.7313,
|
||||
2022-05-17,1.0541,136.32,1.9558,N/A,24.712,7.4414,N/A,0.844,386.3,N/A,N/A,N/A,4.6488,N/A,4.9478,10.4393,N/A,N/A,1.0457,138.7,10.178,7.5245,N/A,N/A,16.6027,1.4993,5.2621,1.3517,7.0899,8.2744,15433.23,3.5387,81.6515,1333.66,21.0273,4.6211,1.6561,55.137,1.4589,36.361,16.844,
|
||||
2022-05-16,1.0422,135.01,1.9558,N/A,24.71,7.4418,N/A,0.85045,385.85,N/A,N/A,N/A,4.6675,N/A,4.9469,10.4978,N/A,N/A,1.0479,138.3,10.2188,7.5225,N/A,N/A,16.3121,1.5057,5.2819,1.3473,7.0786,8.1812,15294.29,3.5474,81.081,1337.9,20.9324,4.5836,1.6601,54.705,1.4531,36.274,16.9195,
|
||||
2022-05-13,1.0385,133.91,1.9558,N/A,24.74,7.4412,N/A,0.85115,385,N/A,N/A,N/A,4.6883,N/A,4.9455,10.4905,N/A,N/A,1.0385,140.1,10.2043,7.52,N/A,N/A,16.0687,1.5067,5.3204,1.3505,7.0513,8.1522,15193.55,3.5586,80.4315,1330.83,20.988,4.5673,1.6633,54.449,1.45,36.109,16.7789,
|
||||
2022-05-12,1.0408,133.85,1.9558,N/A,24.925,7.4413,N/A,0.85293,382.2,N/A,N/A,N/A,4.668,N/A,4.947,10.5648,N/A,N/A,1.0377,139.7,10.2898,7.5235,N/A,N/A,16.0132,1.5163,5.4161,1.3569,7.0691,8.1702,15255.73,3.5981,80.667,1341.98,21.2531,4.5725,1.6692,54.589,1.4529,36.15,16.8806,
|
||||
2022-05-11,1.0553,137.07,1.9558,N/A,25.365,7.4393,N/A,0.85393,379.13,N/A,N/A,N/A,4.6575,N/A,4.947,10.526,N/A,N/A,1.0446,139.3,10.1793,7.5365,N/A,N/A,16.1851,1.5055,5.3859,1.3685,7.0893,8.2839,15308.87,3.6148,81.4935,1343.99,21.387,4.6185,1.6645,54.992,1.4622,36.492,16.9275,
|
||||
2022-05-10,1.0554,137.38,1.9558,N/A,25.014,7.4386,N/A,0.85595,380.15,N/A,N/A,N/A,4.6763,N/A,4.9458,10.6075,N/A,N/A,1.0479,139.5,10.2315,7.5385,N/A,N/A,16.0883,1.5162,5.4232,1.3707,7.0967,8.2847,15349.06,3.6587,81.5425,1346.56,21.4716,4.6248,1.6707,55.289,1.4667,36.448,17.005,
|
||||
2022-05-09,1.0559,138.1,1.9558,N/A,25.055,7.4385,N/A,0.85235,383.23,N/A,N/A,N/A,4.6985,N/A,4.9467,10.5818,N/A,N/A,1.0462,139.5,10.0583,7.5335,N/A,N/A,15.8941,1.5048,5.4321,1.3656,7.0886,8.2887,15367.67,3.6239,81.7415,1345.48,21.4341,4.6285,1.6584,55.67,1.4676,36.497,17.1332,
|
||||
2022-05-06,1.057,137.9,1.9558,N/A,24.665,7.44,N/A,0.85625,381.47,N/A,N/A,N/A,4.7028,N/A,4.949,10.4686,N/A,N/A,1.0419,138.3,9.9808,7.5336,N/A,N/A,15.8078,1.4888,5.3183,1.356,7.0506,8.2969,15312.44,3.5965,81.298,1343.9,21.3555,4.6191,1.644,55.467,1.4642,36.303,16.9614,
|
||||
2022-05-05,1.0568,137.18,1.9558,N/A,24.606,7.4405,N/A,0.8519,378.65,N/A,N/A,N/A,4.6673,N/A,4.9485,10.3748,N/A,N/A,1.0355,138,9.8463,7.5398,N/A,N/A,15.7051,1.4669,5.2192,1.3483,6.9944,8.2948,15250.92,3.5916,80.6185,1330.89,21.2069,4.5955,1.6273,55.364,1.4561,36.026,16.5862,
|
||||
2022-05-04,1.0531,136.84,1.9558,N/A,24.644,7.4409,N/A,0.84194,377.55,N/A,N/A,N/A,4.6875,N/A,4.947,10.3968,N/A,N/A,1.0324,137.2,9.9042,7.5499,N/A,N/A,15.5769,1.478,5.24,1.3498,6.9594,8.2655,15201.44,3.5487,80.4035,1331.45,21.2947,4.5847,1.6333,55.26,1.4559,36.137,16.6489,
|
||||
2022-05-03,1.0556,137.06,1.9558,N/A,24.662,7.4403,N/A,0.8413,382.15,N/A,N/A,N/A,4.6925,N/A,4.9475,10.3978,N/A,N/A,1.0272,137.6,9.909,7.5555,N/A,N/A,15.6941,1.4825,5.3143,1.357,6.9759,8.2838,15288.47,3.5597,80.842,1335.64,21.5025,4.5956,1.6366,55.455,1.4605,36.387,16.8303,
|
||||
2022-05-02,1.0524,136.63,1.9558,N/A,24.671,7.4391,N/A,0.8381,378.51,N/A,N/A,N/A,4.685,N/A,4.9478,10.4035,N/A,N/A,1.0253,137.2,9.9248,7.561,N/A,N/A,15.6697,1.4913,5.248,1.356,6.9548,8.2581,15293.41,3.5277,80.497,1333.71,21.4758,4.5816,1.6362,55.229,1.4585,36.208,16.7383,
|
||||
2022-04-29,1.054,137.01,1.9558,N/A,24.605,7.4415,N/A,0.83908,378.71,N/A,N/A,N/A,4.678,N/A,4.9479,10.2958,N/A,N/A,1.0229,137.8,9.7525,7.5667,N/A,N/A,15.6385,1.4699,5.1608,1.3426,6.9441,8.2703,15301.52,3.4993,80.638,1326.71,21.4181,4.5886,1.6119,55.2,1.4545,36.026,16.6473,
|
||||
2022-04-28,1.0485,137.13,1.9558,N/A,24.526,7.4421,N/A,0.8435,377.06,N/A,N/A,N/A,4.6891,N/A,4.9479,10.3594,N/A,N/A,1.0216,137.8,9.899,7.5703,N/A,N/A,15.5362,1.4814,5.2465,1.3498,6.9381,8.2267,15222.05,3.5096,80.367,1337.82,21.4531,4.5741,1.6221,54.845,1.4556,36.152,16.7472,
|
||||
2022-04-27,1.0583,135.57,1.9558,N/A,24.55,7.441,N/A,0.84215,379.74,N/A,N/A,N/A,4.7043,N/A,4.948,10.4035,N/A,N/A,1.0229,138.2,9.7838,7.565,N/A,N/A,15.6857,1.4828,5.3045,1.3572,6.9377,8.3045,15259.86,3.5178,81.0705,1341.98,21.6259,4.6142,1.6118,55.195,1.4602,36.331,16.8406,
|
||||
2022-04-26,1.0674,136.15,1.9558,N/A,24.423,7.4393,N/A,0.84135,374.46,N/A,N/A,N/A,4.6466,N/A,4.9458,10.3935,N/A,N/A,1.0229,138.4,9.7943,7.5625,N/A,N/A,15.7944,1.4828,5.249,1.3613,6.9837,8.3735,15365.76,3.5147,81.7265,1340.02,21.6538,4.6485,1.6102,55.681,1.4666,36.596,16.7787,
|
||||
2022-04-25,1.0746,137.73,1.9558,N/A,24.418,7.4391,N/A,0.8433,374.08,N/A,N/A,N/A,4.6398,N/A,4.9455,10.3476,N/A,N/A,1.0267,139.2,9.7018,7.562,N/A,N/A,15.864,1.4972,5.1953,1.3709,7.0398,8.4325,15533.84,3.5306,82.321,1344.49,21.8989,4.6815,1.624,56.259,1.4757,36.542,16.8549,
|
||||
2022-04-22,1.0817,138.83,1.9558,N/A,24.32,7.4402,N/A,0.83925,370.35,N/A,N/A,N/A,4.6336,N/A,4.9455,10.278,N/A,N/A,1.0336,139.8,9.6255,7.5625,N/A,N/A,15.9446,1.4816,5.0926,1.3714,7.0332,8.4859,15603.47,3.5288,82.6943,1344.04,22.0034,4.6784,1.6193,56.721,1.4784,36.724,16.8652,
|
||||
2022-04-21,1.0887,139.61,1.9558,N/A,24.38,7.4403,N/A,0.83523,370.6,N/A,N/A,N/A,4.63,N/A,4.945,10.2553,N/A,N/A,1.0335,139,9.5788,7.5635,N/A,N/A,15.9983,1.4653,5.0324,1.36,7.0228,8.5406,15624.02,3.5153,82.965,1348.33,21.8836,4.6716,1.6053,57.081,1.482,36.891,16.5996,
|
||||
2022-04-20,1.083,138.53,1.9558,N/A,24.409,7.4405,N/A,0.82965,371.36,N/A,N/A,N/A,4.6338,N/A,4.9436,10.23,N/A,N/A,1.0254,139.2,9.5443,7.561,N/A,N/A,15.8892,1.4581,5.0481,1.3579,6.9448,8.494,15537.05,3.4908,82.6348,1337.89,21.6392,4.6415,1.595,56.747,1.4779,36.567,16.3019,
|
||||
2022-04-19,1.0803,138.4,1.9558,N/A,24.424,7.4391,N/A,0.82955,374.12,N/A,N/A,N/A,4.6553,N/A,4.9411,10.3408,N/A,N/A,1.0208,139.8,9.5228,7.562,N/A,N/A,15.8416,1.4663,5.0261,1.3631,6.9008,8.4698,15498.35,3.5038,82.6038,1339.46,21.4725,4.5961,1.6016,56.683,1.4763,36.466,16.0401,
|
||||
2022-04-14,1.0878,136.32,1.9558,N/A,24.42,7.4389,N/A,0.82908,376.57,N/A,N/A,N/A,4.6478,N/A,4.9459,10.3008,N/A,N/A,1.0189,140.4,9.5313,7.5587,N/A,N/A,15.9046,1.4612,5.1226,1.3663,6.932,8.5298,15621.3,3.4896,82.814,1334.71,21.5941,4.603,1.5957,56.759,1.4732,36.615,15.9331,
|
||||
2022-04-13,1.0826,136.26,1.9558,N/A,24.45,7.4377,N/A,0.8328,378.45,N/A,N/A,N/A,4.6453,N/A,4.9415,10.3323,N/A,N/A,1.0116,140.2,9.5693,7.5538,N/A,N/A,15.7992,1.4603,5.0449,1.37,6.8939,8.4867,15549.1,3.4782,82.478,1328.47,21.417,4.5799,1.5991,56.446,1.4769,36.305,15.682,
|
||||
2022-04-12,1.0861,136.29,1.9558,N/A,24.45,7.4379,N/A,0.83455,377.78,N/A,N/A,N/A,4.6552,N/A,4.9417,10.332,N/A,N/A,1.0131,139.6,9.5395,7.5513,N/A,N/A,15.9548,1.4599,5.0944,1.3724,6.9199,8.5112,15609.66,3.4972,82.7285,1335.49,21.5616,4.5972,1.5874,56.574,1.4803,36.531,15.849,
|
||||
2022-04-11,1.09,137.01,1.9558,N/A,24.429,7.4375,N/A,0.83693,378.27,N/A,N/A,N/A,4.6456,N/A,4.9397,10.3128,N/A,N/A,1.018,140,9.5478,7.5519,N/A,N/A,16.0485,1.4654,5.155,1.3738,6.9405,8.544,15658.28,3.501,82.7085,1345.23,21.8653,4.6112,1.5938,56.753,1.4874,36.613,15.9127,
|
||||
2022-04-08,1.0861,134.87,1.9558,N/A,24.479,7.4372,N/A,0.83355,375.66,N/A,N/A,N/A,4.6437,N/A,4.9425,10.2768,N/A,N/A,1.0155,139.6,9.508,7.549,N/A,N/A,16.0237,1.4552,5.1583,1.3675,6.9115,8.5134,15601.96,3.501,82.389,1333.12,21.8729,4.585,1.5849,55.99,1.4801,36.488,15.9968,
|
||||
2022-04-07,1.0916,135.32,1.9558,N/A,24.512,7.4378,N/A,0.8345,379.26,N/A,N/A,N/A,4.637,N/A,4.9419,10.313,N/A,N/A,1.0185,141,9.5595,7.5562,N/A,N/A,16.0929,1.4578,5.146,1.3704,6.9448,8.5554,15692.35,3.5259,82.951,1330.92,21.9806,4.6046,1.5816,56.114,1.4848,36.541,16.052,
|
||||
2022-04-06,1.0923,135.3,1.9558,N/A,24.441,7.4378,N/A,0.83473,377.77,N/A,N/A,N/A,4.6328,N/A,4.9433,10.2855,N/A,N/A,1.0187,141.4,9.5523,7.547,N/A,N/A,16.0998,1.4431,5.0996,1.3647,6.9498,8.5617,15683.36,3.5199,82.8343,1330.44,21.8759,4.604,1.5718,56.167,1.4844,36.701,15.9934,
|
||||
2022-04-05,1.0969,134.76,1.9558,N/A,24.338,7.4378,N/A,0.8349,370.93,N/A,N/A,N/A,4.6265,N/A,4.9438,10.2593,N/A,N/A,1.0141,141.6,9.5398,7.5399,N/A,N/A,16.15,1.4374,5.0384,1.3647,6.9783,8.5917,15732.77,3.5152,82.635,1330.81,21.7474,4.6185,1.5657,56.194,1.4867,36.697,15.9529,
|
||||
2022-04-04,1.1005,135.08,1.9558,N/A,24.32,7.4385,N/A,0.8389,369.15,N/A,N/A,N/A,4.6375,N/A,4.9432,10.3849,N/A,N/A,1.0203,141.8,9.5489,7.5455,N/A,N/A,16.183,1.4651,5.1162,1.3749,7.0026,8.6226,15783.89,3.5312,83.118,1338.41,21.82,4.643,1.586,56.521,1.4938,36.894,16.0957,
|
||||
2022-04-01,1.1052,135.35,1.9558,N/A,24.376,7.4388,N/A,0.84145,368.12,N/A,N/A,N/A,4.6401,N/A,4.9452,10.332,N/A,N/A,1.0217,142,9.6628,7.5675,N/A,N/A,16.2411,1.4696,5.2188,1.3805,7.0311,8.6596,15887.5,3.5315,83.9847,1345.61,21.9087,4.6534,1.5911,57.084,1.4985,36.941,16.1685,
|
||||
2022-03-31,1.1101,135.17,1.9558,N/A,24.375,7.4379,N/A,0.84595,369.77,N/A,N/A,N/A,4.6531,N/A,4.9463,10.337,N/A,N/A,1.0267,142,9.711,7.574,N/A,N/A,16.2823,1.4829,5.3009,1.3896,7.0403,8.6918,15947,3.5243,84.134,1347.37,22.0903,4.6677,1.6014,57.514,1.5028,36.911,16.1727,
|
||||
2022-03-30,1.1126,135.47,1.9558,N/A,24.45,7.4391,N/A,0.84563,368.13,N/A,N/A,N/A,4.6679,N/A,4.9477,10.3498,N/A,N/A,1.0309,142.2,9.6398,7.572,N/A,N/A,16.3296,1.4809,5.2808,1.3891,7.0666,8.7081,15957.24,3.5399,84.38,1346.97,22.1557,4.6779,1.5947,57.906,1.5064,37.144,16.1288,
|
||||
2022-03-29,1.1085,136.66,1.9558,N/A,24.464,7.4388,N/A,0.8444,369.8,N/A,N/A,N/A,4.6594,N/A,4.9478,10.329,N/A,N/A,1.0362,142.2,9.5995,7.5815,N/A,N/A,16.3275,1.4795,5.2434,1.387,7.055,8.6767,15896.31,3.5505,83.9685,1345.62,22.1561,4.6707,1.6054,57.602,1.5051,37.29,16.1804,
|
||||
2022-03-28,1.0966,135.93,1.9558,N/A,24.65,7.4393,N/A,0.83643,374.13,N/A,N/A,N/A,4.718,N/A,4.9483,10.4225,N/A,N/A,1.0257,142.8,9.5123,7.5735,N/A,N/A,16.275,1.459,5.2133,1.3702,6.9862,8.5861,15737.77,3.5313,83.4825,1342.49,21.9841,4.6238,1.5838,57.08,1.4921,37.027,15.9925,
|
||||
2022-03-25,1.1002,134.07,1.9558,N/A,24.645,7.4404,N/A,0.8338,373.81,N/A,N/A,N/A,4.7307,N/A,4.9487,10.3505,N/A,N/A,1.0207,142.2,9.5205,7.5754,N/A,N/A,16.3304,1.4624,5.2634,1.3781,7.0007,8.6117,15777.69,3.5351,83.8235,1343.32,21.9908,4.6324,1.5787,57.322,1.4919,36.906,16.0386,
|
||||
2022-03-24,1.0978,133.71,1.9558,N/A,24.72,7.4397,N/A,0.83288,374.44,N/A,N/A,N/A,4.7421,N/A,4.9489,10.3555,N/A,N/A,1.0225,141.2,9.4923,7.5745,N/A,N/A,16.2917,1.4668,5.3057,1.3806,6.9933,8.5897,15778.31,3.5461,83.879,1342.53,22.177,4.6396,1.5812,57.426,1.4912,36.837,16.1478,
|
||||
2022-03-23,1.0985,132.65,1.9558,N/A,24.605,7.4381,N/A,0.8328,372.25,N/A,N/A,N/A,4.7052,N/A,4.9463,10.4005,N/A,N/A,1.0269,141.4,9.6425,7.571,N/A,N/A,16.3108,1.4728,5.3903,1.384,7.0003,8.5948,15792.11,3.5407,83.9675,1337.76,22.1978,4.6401,1.5822,57.565,1.4919,36.965,16.2501,
|
||||
2022-03-22,1.1024,132.96,1.9558,N/A,24.679,7.4402,N/A,0.83228,371.23,N/A,N/A,N/A,4.6851,N/A,4.9463,10.3822,N/A,N/A,1.0275,142.7,9.6233,7.575,N/A,N/A,16.3432,1.4802,5.4105,1.3867,7.0137,8.6285,15808.01,3.5521,83.9145,1343.81,22.3667,4.6483,1.586,57.749,1.4957,36.881,16.343,
|
||||
2022-03-21,1.1038,131.57,1.9558,N/A,24.683,7.4411,N/A,0.83775,374.48,N/A,N/A,N/A,4.696,N/A,4.947,10.4088,N/A,N/A,1.0278,142.7,9.6575,7.5733,N/A,N/A,16.3773,1.4897,5.502,1.3898,7.0152,8.6387,15826.61,3.5637,84.1835,1342.62,22.48,4.6415,1.5997,57.817,1.496,37.038,16.4543,
|
||||
2022-03-18,1.1008,131.4,1.9558,N/A,24.837,7.4423,N/A,0.83925,375.33,N/A,N/A,N/A,4.7135,N/A,4.9483,10.4303,N/A,N/A,1.0314,142.9,9.694,7.5685,N/A,N/A,16.3054,1.4945,5.5784,1.3911,7.0031,8.6101,15782.11,3.5761,83.7825,1337.31,22.5905,4.6157,1.6026,57.622,1.4952,36.745,16.5347,
|
||||
2022-03-17,1.1051,131.27,1.9558,N/A,24.777,7.4439,N/A,0.84315,372.05,N/A,N/A,N/A,4.6889,N/A,4.9465,10.4503,N/A,N/A,1.0385,142.1,9.78,7.573,N/A,N/A,16.3123,1.5055,5.6339,1.3998,7.0176,8.6391,15835.97,3.5777,83.8435,1340.02,22.788,4.6364,1.613,57.69,1.498,36.767,16.5286,
|
||||
2022-03-16,1.0994,130.05,1.9558,N/A,24.687,7.4412,N/A,0.83988,371.18,N/A,N/A,N/A,4.6765,N/A,4.9473,10.4205,N/A,N/A,1.0336,143.5,9.7988,7.5725,N/A,N/A,16.1783,1.5165,5.6523,1.3967,6.9817,8.5996,15690.36,3.5872,83.7805,1353.77,22.854,4.6147,1.6168,57.433,1.4966,36.681,16.5574,
|
||||
2022-03-15,1.0991,129.67,1.9558,N/A,24.867,7.441,N/A,0.84053,371.41,N/A,N/A,N/A,4.7355,N/A,4.9482,10.526,N/A,N/A,1.0322,144.9,9.849,7.575,N/A,N/A,16.0968,1.5234,5.6385,1.4099,7.0117,8.6026,15710.44,3.6088,83.9555,1366.05,22.9352,4.6239,1.6216,57.536,1.4993,36.842,16.6249,
|
||||
2022-03-14,1.096,129.3,1.9558,N/A,24.89,7.4405,N/A,0.83915,373.88,N/A,N/A,N/A,4.7218,N/A,4.949,10.5368,N/A,N/A,1.0249,145.1,9.8588,7.5745,N/A,N/A,16.2,1.5137,5.5286,1.3978,6.9738,8.5815,15678.53,3.586,83.931,1357.77,22.8311,4.6087,1.613,57.398,1.4947,36.59,16.5029,
|
||||
2022-03-11,1.099,128.46,1.9558,N/A,25.213,7.4402,N/A,0.8397,380.92,N/A,N/A,N/A,4.782,N/A,4.949,10.646,N/A,N/A,1.023,144.9,9.8033,7.5713,N/A,N/A,16.2554,1.5017,5.5077,1.4024,6.9633,8.6007,15696.64,3.5683,83.9875,1354.04,22.9524,4.6098,1.6053,57.461,1.4949,36.542,16.4896,
|
||||
2022-03-10,1.1084,128.54,1.9558,N/A,25.316,7.4401,N/A,0.84175,381.63,N/A,N/A,N/A,4.8239,N/A,4.9491,10.7073,N/A,N/A,1.027,145.5,9.919,7.5665,N/A,N/A,16.574,1.5109,5.5958,1.4189,7.0063,8.6688,15824.95,3.6219,84.607,1360.48,23.3153,4.6414,1.6185,57.825,1.5058,36.71,16.7264,
|
||||
2022-03-09,1.0993,127.31,1.9558,N/A,25.364,7.444,N/A,0.8357,379.66,N/A,N/A,N/A,4.8196,N/A,4.9485,10.734,N/A,N/A,1.0198,145.3,9.798,7.5625,N/A,N/A,16.1323,1.4991,5.5201,1.4108,6.9454,8.5974,15710.06,3.5978,84.2025,1357.08,23.2145,4.6028,1.6055,57.259,1.4966,36.326,16.656,
|
||||
2022-03-08,1.0892,126.03,1.9558,N/A,25.642,7.4441,N/A,0.83185,388.28,N/A,N/A,N/A,4.9103,N/A,4.9494,10.8803,N/A,N/A,1.0111,145.9,9.7925,7.5715,N/A,N/A,15.8183,1.4971,5.5346,1.3978,6.8805,8.5183,15639.76,3.6022,83.924,1344.71,23.2866,4.5556,1.5958,56.9,1.4856,36.156,16.7051,
|
||||
2022-03-07,1.0895,125.55,1.9558,N/A,25.584,7.4406,N/A,0.82625,393.25,N/A,N/A,N/A,4.9525,N/A,4.9494,10.8573,N/A,N/A,1.0069,145.8,9.8325,7.56,N/A,N/A,15.6577,1.4751,5.5065,1.3864,6.8846,8.5154,15685.76,3.5653,83.8125,1338.45,23.0249,4.5426,1.5861,56.832,1.4831,35.866,16.6951,
|
||||
2022-03-04,1.0929,126.17,1.9558,N/A,25.737,7.4394,N/A,0.82388,386.54,N/A,N/A,N/A,4.853,N/A,4.9495,10.7935,N/A,N/A,1.0056,144.2,9.8358,7.5584,N/A,N/A,15.5681,1.4872,5.5313,1.3937,6.9065,8.5411,15725.3,3.5603,83.4354,1332.23,22.7543,4.5661,1.6005,56.814,1.4872,35.776,16.8044,
|
||||
2022-03-03,1.1076,128.18,1.9558,N/A,25.634,7.4399,N/A,0.82773,378.64,N/A,N/A,N/A,4.7691,N/A,4.9496,10.7688,N/A,N/A,1.0192,143.4,9.8418,7.57,N/A,N/A,15.6897,1.5139,5.6041,1.3992,6.9996,8.6547,15934.42,3.5872,84.174,1334.2,22.8945,4.637,1.6329,57.293,1.5042,36.063,16.8798,
|
||||
2022-03-02,1.1106,128.08,1.9558,N/A,25.866,7.4387,N/A,0.83316,382.31,N/A,N/A,N/A,4.8021,N/A,4.9493,10.788,N/A,N/A,1.0216,143,9.8826,7.574,N/A,N/A,15.628,1.5272,5.7313,1.4088,7.0153,8.6785,15980.03,3.5954,84.1765,1338.99,22.999,4.6595,1.6404,57.206,1.5059,36.339,17.1904,
|
||||
2022-03-01,1.1162,128.15,1.9558,N/A,25.465,7.4377,N/A,0.8329,379.6,N/A,N/A,N/A,4.7947,N/A,4.949,10.6893,N/A,N/A,1.0247,142,9.8598,7.567,117.201,N/A,15.5509,1.5365,5.7598,1.4158,7.0462,8.7234,16033.36,3.6152,84.5015,1342.6,22.8558,4.6802,1.6484,57.295,1.515,36.505,17.2145,
|
||||
2022-02-28,1.1199,129.31,1.9558,N/A,24.997,7.4404,N/A,0.8355,369.72,N/A,N/A,N/A,4.6835,N/A,4.9484,10.6055,N/A,N/A,1.0336,141.8,9.9465,7.5655,115.4842,N/A,15.4532,1.5508,5.7828,1.4264,7.067,8.7514,16100.72,3.63,84.554,1347.62,22.9011,4.7019,1.6628,57.432,1.5201,36.593,17.2863,
|
||||
2022-02-25,1.1216,129.64,1.9558,N/A,24.66,7.4418,N/A,0.8374,365.28,N/A,N/A,N/A,4.6369,N/A,4.9479,10.5848,N/A,N/A,1.0398,140.8,9.9756,7.5535,92.5673,N/A,15.4799,1.5541,5.738,1.4325,7.0828,8.7578,16088.71,3.6389,84.347,1346.44,22.9145,4.7107,1.6651,57.549,1.5176,36.441,17.0315,
|
||||
2022-02-24,1.1163,128.28,1.9558,N/A,25.09,7.4405,N/A,0.83463,368.63,N/A,N/A,N/A,4.6554,N/A,4.9501,10.7338,N/A,N/A,1.032,142,10.0878,7.552,95.7175,N/A,16.0525,1.5593,5.6874,1.4316,7.0601,8.7178,16074,3.6618,84.296,1347.7,22.9355,4.6896,1.6692,57.45,1.5125,36.514,17.1634,
|
||||
2022-02-23,1.1344,130.58,1.9558,N/A,24.473,7.4388,N/A,0.83463,357.25,N/A,N/A,N/A,4.5481,N/A,4.9468,10.5658,N/A,N/A,1.0431,141.2,10.0335,7.5362,90.8791,N/A,15.6871,1.5592,5.6808,1.4394,7.1669,8.8529,16270.12,3.6515,84.6135,1350.23,22.9079,4.748,1.6679,57.98,1.5253,36.601,17.0508,
|
||||
2022-02-22,1.1342,130.54,1.9558,N/A,24.496,7.4392,N/A,0.83685,355.89,N/A,N/A,N/A,4.5447,N/A,4.9464,10.5996,N/A,N/A,1.0422,141.2,10.1018,7.537,89.8055,N/A,15.6959,1.5739,5.7677,1.4441,7.1771,8.8495,16293.92,3.6584,84.758,1353.34,23.033,4.7472,1.6848,58.208,1.5268,36.748,17.1768,
|
||||
2022-02-21,1.1338,130.2,1.9558,N/A,24.345,7.4397,N/A,0.83298,357.54,N/A,N/A,N/A,4.5351,N/A,4.9448,10.6535,N/A,N/A,1.0387,141.4,10.1738,7.536,89.0866,N/A,15.4689,1.5751,5.8045,1.4454,7.1831,8.8443,16276.34,3.6433,84.677,1353.02,22.9951,4.7387,1.687,58.3,1.5265,36.588,17.1895,
|
||||
2022-02-18,1.1354,130.59,1.9558,N/A,24.337,7.4382,N/A,0.83425,356.37,N/A,N/A,N/A,4.5201,N/A,4.9453,10.5796,N/A,N/A,1.0452,140.8,10.1465,7.5355,86.2815,N/A,15.4678,1.5754,5.8435,1.4424,7.184,8.8566,16304.49,3.6276,84.6525,1356.45,23.027,4.7528,1.6896,58.403,1.5255,36.435,17.0858,
|
||||
2022-02-17,1.137,130.84,1.9558,N/A,24.383,7.4398,N/A,0.83493,356.08,N/A,N/A,N/A,4.5065,N/A,4.9432,10.593,N/A,N/A,1.0466,141.6,10.1225,7.533,86.388,N/A,15.4945,1.5786,5.8495,1.4439,7.206,8.8692,16291.53,3.6231,85.2935,1360.7,23.0367,4.7612,1.695,58.314,1.5278,36.537,16.9893,
|
||||
2022-02-16,1.1372,131.56,1.9558,N/A,24.365,7.441,N/A,0.8394,355.65,N/A,N/A,N/A,4.4961,N/A,4.9436,10.5363,N/A,N/A,1.0516,141.4,10.1095,7.5295,85.3679,N/A,15.481,1.5859,5.8765,1.4416,7.2101,8.8712,16232.55,3.6256,85.3885,1361.31,23.1718,4.7595,1.7108,58.346,1.5291,36.771,17.214,
|
||||
2022-02-15,1.1345,131.18,1.9558,N/A,24.419,7.4422,N/A,0.83765,355.33,N/A,N/A,N/A,4.5036,N/A,4.941,10.5739,N/A,N/A,1.0483,141.2,10.0893,7.529,85.5025,N/A,15.4716,1.5888,5.8977,1.4433,7.1969,8.8527,16187.89,3.6596,85.443,1357.5,23.1113,4.7493,1.7143,58.144,1.5265,36.724,17.1767,
|
||||
2022-02-14,1.1316,130.6,1.9558,N/A,24.527,7.4411,N/A,0.8372,357.06,N/A,N/A,N/A,4.54,N/A,4.9457,10.6158,N/A,N/A,1.0472,142.6,10.0693,7.5293,86.348,N/A,15.351,1.5902,5.8965,1.4431,7.1937,8.8283,16190.53,3.6835,85.4715,1354.5,23.1331,4.742,1.7112,58.114,1.5247,36.8,17.131,
|
||||
2022-02-11,1.1417,132.24,1.9558,N/A,24.405,7.44,N/A,0.83958,353.38,N/A,N/A,N/A,4.5204,N/A,4.9458,10.553,N/A,N/A,1.0557,141.8,10.0732,7.5312,85.855,N/A,15.4066,1.5927,5.9263,1.4498,7.2564,8.9054,16339.46,3.6958,85.8535,1363.68,23.3183,4.7832,1.7085,58.482,1.5339,37.282,17.2736,
|
||||
2022-02-10,1.1439,132.42,1.9558,N/A,24.35,7.4404,N/A,0.84248,354.02,N/A,N/A,N/A,4.4921,N/A,4.9451,10.5275,N/A,N/A,1.0571,141.8,10.0693,7.5275,85.0187,N/A,15.4838,1.5894,5.9668,1.4498,7.2722,8.9142,16390.21,3.6796,85.9373,1367.52,23.3584,4.7855,1.7076,58.583,1.5345,37.331,17.3078,
|
||||
2022-02-09,1.1435,132.04,1.9558,N/A,24.288,7.4437,N/A,0.84255,352.94,N/A,N/A,N/A,4.5135,N/A,4.9449,10.4075,N/A,N/A,1.0555,142.2,10.0585,7.5285,85.5289,N/A,15.551,1.5933,6.0198,1.4514,7.2759,8.9106,16390.99,3.6817,85.5765,1365.72,23.4719,4.7853,1.7107,58.603,1.5349,37.404,17.5281,
|
||||
2022-02-08,1.1408,131.68,1.9558,N/A,24.259,7.4437,N/A,0.84363,353.09,N/A,N/A,N/A,4.5312,N/A,4.945,10.4433,N/A,N/A,1.0545,142.4,10.0758,7.5215,85.7797,N/A,15.5558,1.6025,6.0209,1.4505,7.2636,8.8923,16418.4,3.6753,85.2545,1367.88,23.5601,4.774,1.7196,58.715,1.5349,37.606,17.638,
|
||||
2022-02-07,1.1447,131.59,1.9558,N/A,24.222,7.4443,N/A,0.84685,353.48,N/A,N/A,N/A,4.5432,N/A,4.9461,10.4483,N/A,N/A,1.0571,143.4,10.0658,7.52,86.5824,N/A,15.5235,1.6097,6.0541,1.4546,7.2807,8.9202,16478.97,3.6547,85.5345,1371.76,23.575,4.7909,1.7278,58.978,1.5389,37.735,17.7,
|
||||
2022-02-04,1.1464,131.72,1.9558,N/A,24.36,7.4432,N/A,0.84593,352.92,N/A,N/A,N/A,4.5474,N/A,4.9466,10.4465,N/A,N/A,1.0567,142.8,10.0483,7.5275,87.3095,N/A,15.5072,1.6165,6.083,1.4583,7.2923,8.9286,16493.16,3.6741,85.6445,1374.04,23.5856,4.7914,1.7287,58.754,1.5419,37.797,17.5875,
|
||||
2022-02-03,1.1286,129.63,1.9558,N/A,24.135,7.4388,N/A,0.83208,353.94,N/A,N/A,N/A,4.5315,N/A,4.9461,10.387,N/A,N/A,1.0407,142.4,9.9545,7.5295,86.1788,N/A,15.3047,1.5849,5.9843,1.4334,7.1795,8.7966,16243.28,3.5949,84.518,1358.42,23.2359,4.7215,1.7001,57.608,1.5212,37.424,17.2864,
|
||||
2022-02-02,1.1323,129.37,1.9558,N/A,24.298,7.4383,N/A,0.83395,354.45,N/A,N/A,N/A,4.5449,N/A,4.9463,10.385,N/A,N/A,1.0399,143.2,9.9228,7.526,85.815,N/A,15.3011,1.5828,5.9677,1.433,7.2026,8.8245,16243.98,3.5777,84.613,1359.23,23.2134,4.7392,1.701,57.785,1.5254,37.575,17.3459,
|
||||
2022-02-01,1.126,129.12,1.9558,N/A,24.335,7.4407,N/A,0.83498,356.38,N/A,N/A,N/A,4.5804,N/A,4.9465,10.4438,N/A,N/A,1.0374,143.8,9.9638,7.5275,86.3238,N/A,15.0644,1.5868,5.9572,1.4299,7.1625,8.7779,16126.29,3.566,84.197,1353.61,23.1296,4.7129,1.7032,57.516,1.5199,37.378,17.1633,
|
||||
2022-01-31,1.1156,128.79,1.9558,N/A,24.372,7.4419,N/A,0.83153,357.19,N/A,N/A,N/A,4.5892,N/A,4.9475,10.489,N/A,N/A,1.0404,143.2,10.0085,7.5293,86.7251,N/A,14.931,1.582,6.003,1.4233,7.0963,8.6994,16036.76,3.564,83.3655,1349.08,23.1856,4.6693,1.6983,56.985,1.511,37.144,17.3734,
|
||||
2022-01-28,1.1138,128.68,1.9558,N/A,24.443,7.4432,N/A,0.83178,358.42,N/A,N/A,N/A,4.5755,N/A,4.9463,10.552,N/A,N/A,1.0378,144,10.026,7.529,86.6113,N/A,15.1424,1.5971,6.0147,1.4239,7.0857,8.681,16047.76,3.5697,83.6015,1349.47,23.1854,4.6668,1.7031,57.028,1.5109,37.229,17.3844,
|
||||
2022-01-27,1.116,128.74,1.9558,N/A,24.427,7.4428,N/A,0.83368,358.09,N/A,N/A,N/A,4.5592,N/A,4.9466,10.445,N/A,N/A,1.0391,145.2,9.9903,7.5328,87.139,N/A,15.1946,1.5771,6.0159,1.4161,7.1061,8.6951,16052.42,3.5695,83.7893,1344.5,23.143,4.6844,1.6872,57.295,1.5089,37.124,17.1112,
|
||||
2022-01-26,1.1277,128.86,1.9558,N/A,24.531,7.442,N/A,0.83458,359.67,N/A,N/A,N/A,4.5864,N/A,4.9451,10.4493,N/A,N/A,1.0386,145.6,10.0115,7.529,89.265,N/A,15.2877,1.5727,6.1084,1.4173,7.1293,8.7791,16185.31,3.5834,84.4288,1350.03,23.1845,4.7268,1.6865,57.728,1.5162,37.169,17.0858,
|
||||
2022-01-25,1.1268,128.49,1.9558,N/A,24.5,7.4437,N/A,0.83713,359.44,N/A,N/A,N/A,4.5751,N/A,4.9448,10.502,N/A,N/A,1.0364,146,10.1385,7.5295,88.7384,N/A,15.2723,1.5814,6.2049,1.4247,7.1325,8.7725,16169.54,3.5898,84.277,1350.71,23.2909,4.7213,1.6911,57.753,1.5157,37.28,17.2669,
|
||||
2022-01-24,1.1304,128.62,1.9558,N/A,24.528,7.4431,N/A,0.83803,359.84,N/A,N/A,N/A,4.5572,N/A,4.9453,10.5038,N/A,N/A,1.0308,145.6,10.1638,7.529,88.649,N/A,15.1621,1.5866,6.1901,1.4269,7.1533,8.8003,16213.69,3.5846,84.3495,1352.41,23.2566,4.7352,1.6901,58.045,1.5216,37.382,17.2509,
|
||||
2022-01-21,1.1348,129.14,1.9558,N/A,24.347,7.4431,N/A,0.83633,358.19,N/A,N/A,N/A,4.5318,N/A,4.9453,10.414,N/A,N/A,1.0353,145.6,10.0523,7.528,86.838,N/A,15.223,1.5774,6.2063,1.4211,7.1946,8.837,16244.2,3.5668,84.419,1351.89,23.2229,4.7508,1.6884,58.171,1.526,37.358,17.1546,
|
||||
2022-01-20,1.1338,129.53,1.9558,N/A,24.263,7.4424,N/A,0.83265,355.81,N/A,N/A,N/A,4.5228,N/A,4.9453,10.3708,N/A,N/A,1.0382,145.4,9.9578,7.525,86.8952,N/A,15.2094,1.5662,6.1621,1.4158,7.1936,8.8274,16267.5,3.5564,84.362,1349.16,23.1684,4.7489,1.6731,58.285,1.5261,37.319,17.2531,
|
||||
2022-01-19,1.1345,129.86,1.9558,N/A,24.313,7.4419,N/A,0.83168,355.88,N/A,N/A,N/A,4.5229,N/A,4.9449,10.3428,N/A,N/A,1.0383,145.2,9.9368,7.5238,86.48,N/A,15.4207,1.5709,6.2657,1.4144,7.2003,8.8392,16283.78,3.5529,84.4135,1347.1,23.0922,4.757,1.6684,58.429,1.5293,37.467,17.3889,
|
||||
2022-01-18,1.1367,130.39,1.9558,N/A,24.426,7.4425,N/A,0.83673,357,N/A,N/A,N/A,4.526,N/A,4.9449,10.3185,N/A,N/A,1.0414,146,9.9638,7.5225,86.7325,N/A,15.4447,1.5833,6.2797,1.4228,7.2212,8.8576,16318.64,3.5557,84.813,1355.33,23.1443,4.7554,1.6798,58.553,1.5349,37.635,17.5398,
|
||||
2022-01-17,1.1403,130.64,1.9558,N/A,24.467,7.4417,N/A,0.83573,356.09,N/A,N/A,N/A,4.5256,N/A,4.9443,10.305,N/A,N/A,1.0429,146.8,9.9623,7.5275,87.3907,N/A,15.2757,1.5811,6.2808,1.4287,7.2402,8.8844,16337.73,3.5479,84.7295,1359.89,23.1901,4.7704,1.6765,58.496,1.5374,37.761,17.601,
|
||||
2022-01-14,1.1447,130.17,1.9558,N/A,24.493,7.4414,N/A,0.83508,356.1,N/A,N/A,N/A,4.5414,N/A,4.9429,10.2684,N/A,N/A,1.0429,147,9.9863,7.5205,88.0011,N/A,15.5256,1.5803,6.3361,1.433,7.2728,8.9119,16388.85,3.5565,84.9445,1361.27,23.2684,4.7831,1.6745,58.727,1.5418,38.033,17.6043,
|
||||
2022-01-13,1.1463,130.98,1.9558,N/A,24.458,7.4409,N/A,0.83545,355.24,N/A,N/A,N/A,4.5361,N/A,4.944,10.238,N/A,N/A,1.0453,147,9.9333,7.5202,86.603,N/A,15.5744,1.5709,6.3518,1.4304,7.2913,8.9289,16379.02,3.5638,84.717,1359.24,23.3894,4.7875,1.6676,58.572,1.5427,38.08,17.6381,
|
||||
2022-01-12,1.137,131.19,1.9558,N/A,24.423,7.4414,N/A,0.83338,355.98,N/A,N/A,N/A,4.5359,N/A,4.9453,10.264,N/A,N/A,1.0486,147,9.927,7.524,84.7559,N/A,15.5922,1.5762,6.3458,1.4261,7.2379,8.863,16295.93,3.5406,84.0285,1353.27,23.1852,4.7595,1.6775,58.08,1.5358,37.936,17.5645,
|
||||
2022-01-11,1.1336,130.95,1.9558,N/A,24.412,7.4404,N/A,0.83475,357.45,N/A,N/A,N/A,4.5438,N/A,4.945,10.3075,N/A,N/A,1.0502,147,10.0165,7.5236,84.8663,N/A,15.696,1.5804,6.389,1.4329,7.2255,8.8384,16221.39,3.549,83.7481,1352.85,23.0888,4.7515,1.6772,57.979,1.5342,37.862,17.7094,
|
||||
2022-01-10,1.1318,130.45,1.9558,N/A,24.357,7.4381,N/A,0.83398,358.4,N/A,N/A,N/A,4.5334,N/A,4.9449,10.3038,N/A,N/A,1.0446,146.2,10.0253,7.5278,84.9825,N/A,15.7183,1.5774,6.3969,1.4327,7.2128,8.8233,16181.9,3.5303,83.8,1355.68,23.0597,4.7536,1.6753,58.141,1.5344,38.074,17.6999,
|
||||
2022-01-07,1.1298,130.9,1.9558,N/A,24.439,7.438,N/A,0.8343,358.68,N/A,N/A,N/A,4.5496,N/A,4.9451,10.2839,N/A,N/A,1.0422,146,10.0288,7.5214,85.298,N/A,15.7206,1.5804,6.4343,1.4374,7.206,8.8133,16188.43,3.5135,83.978,1359.96,23.1109,4.7553,1.6748,58.046,1.5356,38.074,17.6701,
|
||||
2022-01-06,1.1315,131.05,1.9558,N/A,24.528,7.4393,N/A,0.83593,359.84,N/A,N/A,N/A,4.5614,N/A,4.9435,10.3265,N/A,N/A,1.0395,146.8,10.035,7.5197,86.5088,N/A,15.5504,1.5778,6.442,1.4451,7.2187,8.8272,16291.88,3.5247,84.2475,1362.06,23.2549,4.767,1.6752,57.943,1.5388,37.962,17.7932,
|
||||
2022-01-05,1.1319,131.03,1.9558,N/A,24.581,7.4384,N/A,0.83546,362.15,N/A,N/A,N/A,4.5666,N/A,4.946,10.2545,N/A,N/A,1.0364,146.8,9.9672,7.519,85.7275,N/A,15.2446,1.56,6.4146,1.4399,7.2087,8.8227,16263.02,3.4989,84.161,1354.61,23.1422,4.7466,1.6597,57.713,1.534,37.607,17.9369,
|
||||
2022-01-04,1.1279,131.17,1.9558,N/A,24.745,7.4378,N/A,0.83618,365.12,N/A,N/A,N/A,4.5667,N/A,4.9481,10.2808,N/A,N/A,1.0355,147.8,10.0138,7.5185,84.9202,N/A,15.1384,1.5682,6.4174,1.4382,7.1924,8.7919,16199.73,3.4909,84.2055,1352.91,23.1808,4.7214,1.6668,57.988,1.531,37.582,18.108,
|
||||
2022-01-03,1.1355,130.56,1.9558,N/A,24.818,7.4382,N/A,0.84135,367.71,N/A,N/A,N/A,4.5895,N/A,4.9483,10.2958,N/A,N/A,1.0372,147.6,10.0013,7.519,84.5313,N/A,15.0777,1.5691,6.3539,1.442,7.2174,8.8541,16202.02,3.5139,84.3949,1354.4,23.2259,4.7379,1.6651,58.051,1.5333,37.665,17.9661,
|
||||
|
59
ext/fileio/fileio.go
Normal file
59
ext/fileio/fileio.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Package fileio provides SQL functions to read, write and list files.
|
||||
//
|
||||
// https://sqlite.org/src/doc/tip/ext/misc/fileio.c
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Register registers SQL functions readfile, writefile, lsmode,
|
||||
// and the table-valued function fsdir.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
RegisterFS(db, nil)
|
||||
}
|
||||
|
||||
// Register registers SQL functions readfile, lsmode,
|
||||
// and the table-valued function fsdir;
|
||||
// fsys will be used to read files and list directories.
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
db.CreateFunction("lsmode", 1, sqlite3.DETERMINISTIC, lsmode)
|
||||
db.CreateFunction("readfile", 1, sqlite3.DIRECTONLY, readfile(fsys))
|
||||
if fsys == nil {
|
||||
db.CreateFunction("writefile", -1, sqlite3.DIRECTONLY, writefile)
|
||||
}
|
||||
sqlite3.CreateModule(db, "fsdir", nil, func(db *sqlite3.Conn, _, _, _ string, _ ...string) (fsdir, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(name,mode,mtime TIMESTAMP,data,path HIDDEN,dir HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
return fsdir{fsys}, err
|
||||
})
|
||||
}
|
||||
|
||||
func lsmode(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultText(fs.FileMode(arg[0].Int()).String())
|
||||
}
|
||||
|
||||
func readfile(fsys fs.FS) func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
return func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
var err error
|
||||
var data []byte
|
||||
|
||||
if fsys != nil {
|
||||
data, err = fs.ReadFile(fsys, arg[0].Text())
|
||||
} else {
|
||||
data, err = os.ReadFile(arg[0].Text())
|
||||
}
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
ctx.ResultBlob(data)
|
||||
case !errors.Is(err, fs.ErrNotExist):
|
||||
ctx.ResultError(fmt.Errorf("readfile: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
80
ext/fileio/fileio_test.go
Normal file
80
ext/fileio/fileio_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package fileio_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/fileio"
|
||||
)
|
||||
|
||||
func Test_lsmode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
fileio.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
d, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s, err := os.Stat(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var mode string
|
||||
err = db.QueryRow(`SELECT lsmode(?)`, s.Mode()).Scan(&mode)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(mode) != 10 || mode[0] != 'd' {
|
||||
t.Errorf("got %s", mode)
|
||||
} else {
|
||||
t.Logf("got %s", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_readfile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
|
||||
t.Run("", func(t *testing.T) {
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
fileio.RegisterFS(c, fsys)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`SELECT readfile('fileio_test.go')`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if rows.Next() {
|
||||
var data sql.RawBytes
|
||||
rows.Scan(&data)
|
||||
|
||||
if !bytes.HasPrefix(data, []byte("package fileio_test")) {
|
||||
t.Errorf("got %s", data[:min(64, len(data))])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
186
ext/fileio/fsdir.go
Normal file
186
ext/fileio/fsdir.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
type fsdir struct{ fsys fs.FS }
|
||||
|
||||
func (d fsdir) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
var root, base bool
|
||||
for i, cst := range idx.Constraint {
|
||||
switch cst.Column {
|
||||
case 4: // root
|
||||
if !cst.Usable || cst.Op != sqlite3.INDEX_CONSTRAINT_EQ {
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
Omit: true,
|
||||
ArgvIndex: 1,
|
||||
}
|
||||
root = true
|
||||
case 5: // base
|
||||
if !cst.Usable || cst.Op != sqlite3.INDEX_CONSTRAINT_EQ {
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
Omit: true,
|
||||
ArgvIndex: 2,
|
||||
}
|
||||
base = true
|
||||
}
|
||||
}
|
||||
if !root {
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
if base {
|
||||
idx.EstimatedCost = 10
|
||||
} else {
|
||||
idx.EstimatedCost = 100
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d fsdir) Open() (sqlite3.VTabCursor, error) {
|
||||
return &cursor{fsdir: d}, nil
|
||||
}
|
||||
|
||||
type cursor struct {
|
||||
fsdir
|
||||
curr entry
|
||||
next chan entry
|
||||
done chan struct{}
|
||||
base string
|
||||
rowID int64
|
||||
eof bool
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
fs.DirEntry
|
||||
err error
|
||||
path string
|
||||
}
|
||||
|
||||
func (c *cursor) Close() error {
|
||||
if c.done != nil {
|
||||
close(c.done)
|
||||
s := <-c.next
|
||||
c.done = nil
|
||||
c.next = nil
|
||||
return s.err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
if err := c.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
root := arg[0].Text()
|
||||
if len(arg) > 1 {
|
||||
base := arg[1].Text()
|
||||
if c.fsys != nil {
|
||||
root = path.Join(base, root)
|
||||
base = path.Clean(base) + "/"
|
||||
} else {
|
||||
root = filepath.Join(base, root)
|
||||
base = filepath.Clean(base) + string(filepath.Separator)
|
||||
}
|
||||
c.base = base
|
||||
}
|
||||
|
||||
c.rowID = 0
|
||||
c.eof = false
|
||||
c.next = make(chan entry)
|
||||
c.done = make(chan struct{})
|
||||
go c.WalkDir(root)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (c *cursor) Next() error {
|
||||
curr, ok := <-c.next
|
||||
c.curr = curr
|
||||
c.eof = !ok
|
||||
c.rowID++
|
||||
return c.curr.err
|
||||
}
|
||||
|
||||
func (c *cursor) EOF() bool {
|
||||
return c.eof
|
||||
}
|
||||
|
||||
func (c *cursor) RowID() (int64, error) {
|
||||
return c.rowID, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
|
||||
switch n {
|
||||
case 0: // name
|
||||
name := strings.TrimPrefix(c.curr.path, c.base)
|
||||
ctx.ResultText(name)
|
||||
|
||||
case 1: // mode
|
||||
i, err := c.curr.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.ResultInt64(int64(i.Mode()))
|
||||
|
||||
case 2: // mtime
|
||||
i, err := c.curr.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.ResultTime(i.ModTime(), sqlite3.TimeFormatUnixFrac)
|
||||
|
||||
case 3: // data
|
||||
switch typ := c.curr.Type(); {
|
||||
case typ.IsRegular():
|
||||
var data []byte
|
||||
var err error
|
||||
if c.fsys != nil {
|
||||
data, err = fs.ReadFile(c.fsys, c.curr.path)
|
||||
} else {
|
||||
data, err = os.ReadFile(c.curr.path)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.ResultBlob(data)
|
||||
|
||||
case typ&fs.ModeSymlink != 0 && c.fsys == nil:
|
||||
t, err := os.Readlink(c.curr.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.ResultText(t)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
78
ext/fileio/fsdir_test.go
Normal file
78
ext/fileio/fsdir_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package fileio_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/fileio"
|
||||
)
|
||||
|
||||
func Test_fsdir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
|
||||
t.Run("", func(t *testing.T) {
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
fileio.RegisterFS(c, fsys)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`SELECT * FROM fsdir('.', '.') LIMIT 4`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var mode fs.FileMode
|
||||
var mtime time.Time
|
||||
var data sql.RawBytes
|
||||
err := rows.Scan(&name, &mode, sqlite3.TimeFormatUnixFrac.Scanner(&mtime), &data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mode.Perm() == 0 {
|
||||
t.Errorf("got: %v", mode)
|
||||
}
|
||||
if mtime.Before(time.Unix(0, 0)) {
|
||||
t.Errorf("got: %v", mtime)
|
||||
}
|
||||
if name == "fsdir_test.go" {
|
||||
if !bytes.HasPrefix(data, []byte("package fileio_test")) {
|
||||
t.Errorf("got: %s", data[:min(64, len(data))])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fsdir_errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
fileio.Register(db)
|
||||
|
||||
err = db.Exec(`SELECT name FROM fsdir()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
}
|
||||
97
ext/fileio/write.go
Normal file
97
ext/fileio/write.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/util/fsutil"
|
||||
)
|
||||
|
||||
func writefile(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if len(arg) < 2 || len(arg) > 4 {
|
||||
ctx.ResultError(util.ErrorString("writefile: wrong number of arguments"))
|
||||
return
|
||||
}
|
||||
|
||||
file := arg[0].Text()
|
||||
|
||||
var mode fs.FileMode
|
||||
if len(arg) > 2 {
|
||||
mode = fsutil.FileModeFromValue(arg[2])
|
||||
}
|
||||
|
||||
n, err := createFileAndDir(file, mode, arg[1])
|
||||
if err != nil {
|
||||
if len(arg) > 2 {
|
||||
ctx.ResultError(fmt.Errorf("writefile: %w", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if mode&fs.ModeSymlink == 0 {
|
||||
if len(arg) > 2 {
|
||||
err := os.Chmod(file, mode.Perm())
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("writefile: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(arg) > 3 {
|
||||
mtime := arg[3].Time(sqlite3.TimeFormatUnixFrac)
|
||||
err := os.Chtimes(file, time.Time{}, mtime)
|
||||
if err != nil {
|
||||
ctx.ResultError(fmt.Errorf("writefile: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mode.IsRegular() {
|
||||
ctx.ResultInt(n)
|
||||
}
|
||||
}
|
||||
|
||||
func createFileAndDir(path string, mode fs.FileMode, data sqlite3.Value) (int, error) {
|
||||
n, err := createFile(path, mode, data)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0777); err == nil {
|
||||
return createFile(path, mode, data)
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func createFile(path string, mode fs.FileMode, data sqlite3.Value) (int, error) {
|
||||
if mode.IsRegular() {
|
||||
blob := data.RawBlob()
|
||||
return len(blob), os.WriteFile(path, blob, fixPerm(mode, 0666))
|
||||
}
|
||||
if mode.IsDir() {
|
||||
err := os.Mkdir(path, fixPerm(mode, 0777))
|
||||
if errors.Is(err, fs.ErrExist) {
|
||||
s, err := os.Lstat(path)
|
||||
if err == nil && s.IsDir() {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if mode&fs.ModeSymlink != 0 {
|
||||
return 0, os.Symlink(data.Text(), path)
|
||||
}
|
||||
return 0, fmt.Errorf("invalid mode: %v", mode)
|
||||
}
|
||||
|
||||
func fixPerm(mode fs.FileMode, def fs.FileMode) fs.FileMode {
|
||||
if mode.Perm() == 0 {
|
||||
return def
|
||||
}
|
||||
return mode.Perm()
|
||||
}
|
||||
92
ext/fileio/write_test.go
Normal file
92
ext/fileio/write_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
func Test_writefile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
dir := t.TempDir()
|
||||
link := filepath.Join(dir, "link")
|
||||
file := filepath.Join(dir, "test.txt")
|
||||
nest := filepath.Join(dir, "tmp", "test.txt")
|
||||
sock := filepath.Join(dir, "sock")
|
||||
twosday := time.Date(2022, 2, 22, 22, 22, 22, 0, time.UTC)
|
||||
|
||||
_, err = db.Exec(`SELECT writefile(?, 'Hello world!')`, file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`SELECT writefile(?, ?, ?)`, link, "test.txt", fs.ModeSymlink)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`SELECT writefile(?, ?, ?, ?)`, dir, nil, 0040700, twosday.Unix())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rows, err := db.Query(`SELECT * FROM fsdir('.', ?)`, dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var mode fs.FileMode
|
||||
var mtime time.Time
|
||||
var data sql.NullString
|
||||
err := rows.Scan(&name, &mode, &mtime, &data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mode.IsDir() && !mtime.Equal(twosday) {
|
||||
t.Errorf("got: %v", mtime)
|
||||
}
|
||||
if mode.IsRegular() && data.String != "Hello world!" {
|
||||
t.Errorf("got: %v", data)
|
||||
}
|
||||
if mode&fs.ModeSymlink != 0 && data.String != "test.txt" {
|
||||
t.Errorf("got: %v", data)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = db.Exec(`SELECT writefile(?, 'Hello world!')`, nest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`SELECT writefile(?, ?, ?)`, sock, nil, fs.ModeSocket)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`SELECT writefile()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
}
|
||||
30
ext/hash/blake2.go
Normal file
30
ext/hash/blake2.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
func blake2sFunc(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
hashFunc(ctx, arg[0], crypto.BLAKE2s_256)
|
||||
}
|
||||
|
||||
func blake2bFunc(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
size := 512
|
||||
if len(arg) > 1 {
|
||||
size = arg[1].Int()
|
||||
}
|
||||
|
||||
switch size {
|
||||
case 256:
|
||||
hashFunc(ctx, arg[0], crypto.BLAKE2b_256)
|
||||
case 384:
|
||||
hashFunc(ctx, arg[0], crypto.BLAKE2b_384)
|
||||
case 512:
|
||||
hashFunc(ctx, arg[0], crypto.BLAKE2b_512)
|
||||
default:
|
||||
ctx.ResultError(util.ErrorString("blake2b: size must be 256, 384, 512"))
|
||||
}
|
||||
}
|
||||
97
ext/hash/hash.go
Normal file
97
ext/hash/hash.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Package hash provides cryptographic hash functions.
|
||||
//
|
||||
// Provided functions:
|
||||
// - md4(data)
|
||||
// - md5(data)
|
||||
// - sha1(data)
|
||||
// - sha3(data, size) (default size 256)
|
||||
// - sha224(data)
|
||||
// - sha256(data, size) (default size 256)
|
||||
// - sha384(data)
|
||||
// - sha512(data, size) (default size 512)
|
||||
// - blake2s(data)
|
||||
// - blake2b(data, size) (default size 512)
|
||||
// - ripemd160(data)
|
||||
//
|
||||
// Each SQL function will only be registered if the corresponding
|
||||
// [crypto.Hash] function is available.
|
||||
// To ensure a specific hash function is available,
|
||||
// import the implementing package.
|
||||
package hash
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Register registers cryptographic hash functions for a database connection.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
|
||||
if crypto.MD4.Available() {
|
||||
db.CreateFunction("md4", 1, flags, md4Func)
|
||||
}
|
||||
if crypto.MD5.Available() {
|
||||
db.CreateFunction("md5", 1, flags, md5Func)
|
||||
}
|
||||
if crypto.SHA1.Available() {
|
||||
db.CreateFunction("sha1", 1, flags, sha1Func)
|
||||
}
|
||||
if crypto.SHA3_512.Available() {
|
||||
db.CreateFunction("sha3", 1, flags, sha3Func)
|
||||
db.CreateFunction("sha3", 2, flags, sha3Func)
|
||||
}
|
||||
if crypto.SHA256.Available() {
|
||||
db.CreateFunction("sha224", 1, flags, sha224Func)
|
||||
db.CreateFunction("sha256", 1, flags, sha256Func)
|
||||
db.CreateFunction("sha256", 2, flags, sha256Func)
|
||||
}
|
||||
if crypto.SHA512.Available() {
|
||||
db.CreateFunction("sha384", 1, flags, sha384Func)
|
||||
db.CreateFunction("sha512", 1, flags, sha512Func)
|
||||
db.CreateFunction("sha512", 2, flags, sha512Func)
|
||||
}
|
||||
if crypto.BLAKE2s_256.Available() {
|
||||
db.CreateFunction("blake2s", 1, flags, blake2sFunc)
|
||||
}
|
||||
if crypto.BLAKE2b_512.Available() {
|
||||
db.CreateFunction("blake2b", 1, flags, blake2bFunc)
|
||||
db.CreateFunction("blake2b", 2, flags, blake2bFunc)
|
||||
}
|
||||
if crypto.RIPEMD160.Available() {
|
||||
db.CreateFunction("ripemd160", 1, flags, ripemd160Func)
|
||||
}
|
||||
}
|
||||
|
||||
func md4Func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
hashFunc(ctx, arg[0], crypto.MD4)
|
||||
}
|
||||
|
||||
func md5Func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
hashFunc(ctx, arg[0], crypto.MD5)
|
||||
}
|
||||
|
||||
func sha1Func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
hashFunc(ctx, arg[0], crypto.SHA1)
|
||||
}
|
||||
|
||||
func ripemd160Func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
hashFunc(ctx, arg[0], crypto.RIPEMD160)
|
||||
}
|
||||
|
||||
func hashFunc(ctx sqlite3.Context, arg sqlite3.Value, fn crypto.Hash) {
|
||||
var data []byte
|
||||
switch arg.Type() {
|
||||
case sqlite3.NULL:
|
||||
return
|
||||
case sqlite3.BLOB:
|
||||
data = arg.RawBlob()
|
||||
default:
|
||||
data = arg.RawText()
|
||||
}
|
||||
|
||||
h := fn.New()
|
||||
h.Write(data)
|
||||
ctx.ResultBlob(h.Sum(nil))
|
||||
}
|
||||
98
ext/hash/hash_test.go
Normal file
98
ext/hash/hash_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
_ "crypto/md5"
|
||||
_ "crypto/sha1"
|
||||
_ "crypto/sha256"
|
||||
_ "crypto/sha512"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "golang.org/x/crypto/blake2b"
|
||||
_ "golang.org/x/crypto/blake2s"
|
||||
_ "golang.org/x/crypto/md4"
|
||||
_ "golang.org/x/crypto/ripemd160"
|
||||
_ "golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hash string
|
||||
}{
|
||||
{"md4(NULL)", ""},
|
||||
{"md4(X'')", "31D6CFE0D16AE931B73C59D7E0C089C0"},
|
||||
{"md4('The quick brown fox jumps over the lazy dog')", "1BEE69A46BA811185C194762ABAEAE90"},
|
||||
|
||||
{"md5('')", "D41D8CD98F00B204E9800998ECF8427E"},
|
||||
{"sha1('')", "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"},
|
||||
{"ripemd160('')", "9C1185A5C5E9FC54612808977EE8F548B2258D31"},
|
||||
|
||||
{"sha224('')", "D14A028C2A3A2BC9476102BB288234C415A2B01F828EA62AC5B3E42F"},
|
||||
{"sha256('')", "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"},
|
||||
{"sha256('', 224)", "D14A028C2A3A2BC9476102BB288234C415A2B01F828EA62AC5B3E42F"},
|
||||
{"sha384('')", "38B060A751AC96384CD9327EB1B1E36A21FDB71114BE07434C0CC7BF63F6E1DA274EDEBFE76F65FBD51AD2F14898B95B"},
|
||||
{"sha512('')", "CF83E1357EEFB8BDF1542850D66D8007D620E4050B5715DC83F4A921D36CE9CE47D0D13C5D85F2B0FF8318D2877EEC2F63B931BD47417A81A538327AF927DA3E"},
|
||||
{"sha512('', 224)", "6ED0DD02806FA89E25DE060C19D3AC86CABB87D6A0DDD05C333B84F4"},
|
||||
{"sha512('', 256)", "C672B8D1EF56ED28AB87C3622C5114069BDD3AD7B8F9737498D0C01ECEF0967A"},
|
||||
{"sha512('', 384)", "38B060A751AC96384CD9327EB1B1E36A21FDB71114BE07434C0CC7BF63F6E1DA274EDEBFE76F65FBD51AD2F14898B95B"},
|
||||
|
||||
{"sha3('')", "A7FFC6F8BF1ED76651C14756A061D662F580FF4DE43B49FA82D80A4B80F8434A"},
|
||||
{"sha3('', 224)", "6B4E03423667DBB73B6E15454F0EB1ABD4597F9A1B078E3F5B5A6BC7"},
|
||||
{"sha3('', 384)", "0C63A75B845E4F7D01107D852E4C2485C51A50AAAA94FC61995E71BBEE983A2AC3713831264ADB47FB6BD1E058D5F004"},
|
||||
{"sha3('', 512)", "A69F73CCA23A9AC5C8B567DC185A756E97C982164FE25859E0D1DCC1475C80A615B2123AF1F5F94C11E3E9402C3AC558F500199D95B6D3E301758586281DCD26"},
|
||||
|
||||
{"blake2s('')", "69217A3079908094E11121D042354A7C1F55B6482CA1A51E1B250DFD1ED0EEF9"},
|
||||
{"blake2b('')", "786A02F742015903C6C6FD852552D272912F4740E15847618A86E217F71F5419D25E1031AFEE585313896444934EB04B903A685B1448B755D56F701AFE9BE2CE"},
|
||||
{"blake2b('', 384)", "B32811423377F52D7862286EE1A72EE540524380FDA1724A6F25D7978C6FD3244A6CAF0498812673C5E05EF583825100"},
|
||||
{"blake2b('', 256)", "0E5751C026E543B2E8AB2EB06099DAA1D1E5DF47778F7787FAAB45CDF12FE3A8"},
|
||||
}
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var hash string
|
||||
|
||||
err = db.QueryRow(`SELECT hex(` + tt.name + `)`).Scan(&hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if hash != tt.hash {
|
||||
t.Errorf("got %s, want %s", hash, tt.hash)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_, err = db.Exec(`SELECT sha256('', 255)`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`SELECT sha512('', 255)`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`SELECT sha3('', 255)`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`SELECT blake2b('', 255)`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
}
|
||||
53
ext/hash/sha2.go
Normal file
53
ext/hash/sha2.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
func sha224Func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
hashFunc(ctx, arg[0], crypto.SHA224)
|
||||
}
|
||||
|
||||
func sha384Func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
hashFunc(ctx, arg[0], crypto.SHA384)
|
||||
}
|
||||
|
||||
func sha256Func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
size := 256
|
||||
if len(arg) > 1 {
|
||||
size = arg[1].Int()
|
||||
}
|
||||
|
||||
switch size {
|
||||
case 224:
|
||||
hashFunc(ctx, arg[0], crypto.SHA224)
|
||||
case 256:
|
||||
hashFunc(ctx, arg[0], crypto.SHA256)
|
||||
default:
|
||||
ctx.ResultError(util.ErrorString("sha256: size must be 224, 256"))
|
||||
}
|
||||
}
|
||||
|
||||
func sha512Func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
size := 512
|
||||
if len(arg) > 1 {
|
||||
size = arg[1].Int()
|
||||
}
|
||||
|
||||
switch size {
|
||||
case 224:
|
||||
hashFunc(ctx, arg[0], crypto.SHA512_224)
|
||||
case 256:
|
||||
hashFunc(ctx, arg[0], crypto.SHA512_256)
|
||||
case 384:
|
||||
hashFunc(ctx, arg[0], crypto.SHA384)
|
||||
case 512:
|
||||
hashFunc(ctx, arg[0], crypto.SHA512)
|
||||
default:
|
||||
ctx.ResultError(util.ErrorString("sha512: size must be 224, 256, 384, 512"))
|
||||
}
|
||||
|
||||
}
|
||||
28
ext/hash/sha3.go
Normal file
28
ext/hash/sha3.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
func sha3Func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
size := 256
|
||||
if len(arg) > 1 {
|
||||
size = arg[1].Int()
|
||||
}
|
||||
|
||||
switch size {
|
||||
case 224:
|
||||
hashFunc(ctx, arg[0], crypto.SHA3_224)
|
||||
case 256:
|
||||
hashFunc(ctx, arg[0], crypto.SHA3_256)
|
||||
case 384:
|
||||
hashFunc(ctx, arg[0], crypto.SHA3_384)
|
||||
case 512:
|
||||
hashFunc(ctx, arg[0], crypto.SHA3_512)
|
||||
default:
|
||||
ctx.ResultError(util.ErrorString("sha3: size must be 224, 256, 384, 512"))
|
||||
}
|
||||
}
|
||||
193
ext/lines/lines.go
Normal file
193
ext/lines/lines.go
Normal file
@@ -0,0 +1,193 @@
|
||||
// Package lines provides a virtual table to read data line-by-line.
|
||||
//
|
||||
// It is particularly useful for line-oriented datasets,
|
||||
// like [ndjson] or [JSON Lines],
|
||||
// when paired with SQLite's JSON support.
|
||||
//
|
||||
// https://github.com/asg017/sqlite-lines
|
||||
//
|
||||
// [ndjson]: https://ndjson.org/
|
||||
// [JSON Lines]: https://jsonlines.org/
|
||||
package lines
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/util/osutil"
|
||||
)
|
||||
|
||||
// Register registers the lines and lines_read table-valued functions.
|
||||
// The lines function reads from a database blob or text.
|
||||
// The lines_read function reads from a file or an [io.Reader].
|
||||
// If a filename is specified, [os.Open] is used to open the file.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
RegisterFS(db, osutil.FS{})
|
||||
}
|
||||
|
||||
// RegisterFS registers the lines and lines_read table-valued functions.
|
||||
// The lines function reads from a database blob or text.
|
||||
// The lines_read function reads from a file or an [io.Reader].
|
||||
// If a filename is specified, fsys is used to open the file.
|
||||
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
|
||||
sqlite3.CreateModule[lines](db, "lines", nil,
|
||||
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_INNOCUOUS)
|
||||
return lines{}, err
|
||||
})
|
||||
sqlite3.CreateModule[lines](db, "lines_read", nil,
|
||||
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
|
||||
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
|
||||
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
|
||||
return lines{fsys}, err
|
||||
})
|
||||
}
|
||||
|
||||
type lines struct {
|
||||
fsys fs.FS
|
||||
}
|
||||
|
||||
func (l lines) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
for i, cst := range idx.Constraint {
|
||||
if cst.Column == 1 && cst.Op == sqlite3.INDEX_CONSTRAINT_EQ && cst.Usable {
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
Omit: true,
|
||||
ArgvIndex: 1,
|
||||
}
|
||||
idx.EstimatedCost = 1e6
|
||||
idx.EstimatedRows = 100
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
|
||||
func (l lines) Open() (sqlite3.VTabCursor, error) {
|
||||
if l.fsys != nil {
|
||||
return &reader{fsys: l.fsys}, nil
|
||||
} else {
|
||||
return &buffer{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type cursor struct {
|
||||
line []byte
|
||||
rowID int64
|
||||
eof bool
|
||||
}
|
||||
|
||||
func (c *cursor) EOF() bool {
|
||||
return c.eof
|
||||
}
|
||||
|
||||
func (c *cursor) RowID() (int64, error) {
|
||||
return c.rowID, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
|
||||
if n == 0 {
|
||||
ctx.ResultRawText(c.line)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
fsys fs.FS
|
||||
reader *bufio.Reader
|
||||
closer io.Closer
|
||||
cursor
|
||||
}
|
||||
|
||||
func (c *reader) Close() (err error) {
|
||||
if c.closer != nil {
|
||||
err = c.closer.Close()
|
||||
c.closer = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *reader) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
if err := c.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var r io.Reader
|
||||
typ := arg[0].Type()
|
||||
switch typ {
|
||||
case sqlite3.NULL:
|
||||
if p, ok := arg[0].Pointer().(io.Reader); ok {
|
||||
r = p
|
||||
}
|
||||
case sqlite3.TEXT:
|
||||
f, err := c.fsys.Open(arg[0].Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r = f
|
||||
}
|
||||
if r == nil {
|
||||
return fmt.Errorf("lines: unsupported argument:%.0w %v", sqlite3.MISMATCH, typ)
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(r)
|
||||
c.closer, _ = r.(io.Closer)
|
||||
c.rowID = 0
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (c *reader) Next() (err error) {
|
||||
c.line = c.line[:0]
|
||||
for more := true; more; {
|
||||
var line []byte
|
||||
line, more, err = c.reader.ReadLine()
|
||||
c.line = append(c.line, line...)
|
||||
}
|
||||
if err == io.EOF {
|
||||
c.eof = true
|
||||
err = nil
|
||||
}
|
||||
c.rowID++
|
||||
return err
|
||||
}
|
||||
|
||||
type buffer struct {
|
||||
data []byte
|
||||
cursor
|
||||
}
|
||||
|
||||
func (c *buffer) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
typ := arg[0].Type()
|
||||
switch typ {
|
||||
case sqlite3.TEXT:
|
||||
c.data = arg[0].RawText()
|
||||
case sqlite3.BLOB:
|
||||
c.data = arg[0].RawBlob()
|
||||
default:
|
||||
return fmt.Errorf("lines: unsupported argument:%.0w %v", sqlite3.MISMATCH, typ)
|
||||
}
|
||||
|
||||
c.rowID = 0
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (c *buffer) Next() error {
|
||||
i := bytes.IndexByte(c.data, '\n')
|
||||
j := i + 1
|
||||
switch {
|
||||
case i < 0:
|
||||
i = len(c.data)
|
||||
j = i
|
||||
case i > 0 && c.data[i-1] == '\r':
|
||||
i--
|
||||
}
|
||||
c.eof = len(c.data) == 0
|
||||
c.line = c.data[:i]
|
||||
c.data = c.data[j:]
|
||||
c.rowID++
|
||||
return nil
|
||||
}
|
||||
192
ext/lines/lines_test.go
Normal file
192
ext/lines/lines_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package lines_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/lines"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
lines.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
res, err := http.Get("https://storage.googleapis.com/quickdraw_dataset/full/simplified/calendar.ndjson")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT
|
||||
line ->> '$.countrycode' as countrycode,
|
||||
COUNT(*)
|
||||
FROM lines_read(?)
|
||||
GROUP BY 1
|
||||
ORDER BY 2 DESC
|
||||
LIMIT 5`,
|
||||
sqlite3.Pointer(res.Body))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var countrycode sql.RawBytes
|
||||
var count int
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&countrycode, &count)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%s: %d\n", countrycode, count)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// US: 141001
|
||||
// GB: 22560
|
||||
// CA: 11759
|
||||
// RU: 9250
|
||||
// DE: 8748
|
||||
}
|
||||
|
||||
func Test_lines(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
lines.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
const data = "line 1\nline 2\r\nline 3\n"
|
||||
|
||||
rows, err := db.Query(`SELECT rowid, line FROM lines(?)`, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var line string
|
||||
err := rows.Scan(&id, &line)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := fmt.Sprintf("line %d", id); line != want {
|
||||
t.Errorf("got %q, want %q", line, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_lines_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
lines.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`SELECT rowid, line FROM lines(?)`, nil)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`SELECT rowid, line FROM lines_read(?)`, "xpto")
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_lines_read(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
lines.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
const data = "line 1\nline 2\r\nline 3\n"
|
||||
|
||||
rows, err := db.Query(`SELECT rowid, line FROM lines_read(?)`,
|
||||
sqlite3.Pointer(strings.NewReader(data)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var line string
|
||||
err := rows.Scan(&id, &line)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := fmt.Sprintf("line %d", id); line != want {
|
||||
t.Errorf("got %q, want %q", line, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_lines_test(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
|
||||
lines.Register(c)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`SELECT rowid, line FROM lines_read(?)`, "lines_test.go")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
t.Skip(err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var line string
|
||||
err := rows.Scan(&id, &line)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
34
ext/pivot/op_test.go
Normal file
34
ext/pivot/op_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package pivot
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
func Test_operator(t *testing.T) {
|
||||
tests := []struct {
|
||||
op sqlite3.IndexConstraintOp
|
||||
want string
|
||||
}{
|
||||
{sqlite3.INDEX_CONSTRAINT_EQ, "="},
|
||||
{sqlite3.INDEX_CONSTRAINT_LT, "<"},
|
||||
{sqlite3.INDEX_CONSTRAINT_GT, ">"},
|
||||
{sqlite3.INDEX_CONSTRAINT_LE, "<="},
|
||||
{sqlite3.INDEX_CONSTRAINT_GE, ">="},
|
||||
{sqlite3.INDEX_CONSTRAINT_NE, "<>"},
|
||||
{sqlite3.INDEX_CONSTRAINT_IS, "IS"},
|
||||
{sqlite3.INDEX_CONSTRAINT_ISNOT, "IS NOT"},
|
||||
{sqlite3.INDEX_CONSTRAINT_REGEXP, "REGEXP"},
|
||||
{sqlite3.INDEX_CONSTRAINT_MATCH, "MATCH"},
|
||||
{sqlite3.INDEX_CONSTRAINT_GLOB, "GLOB"},
|
||||
{sqlite3.INDEX_CONSTRAINT_LIKE, "LIKE"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want, func(t *testing.T) {
|
||||
if got := operator(tt.op); got != tt.want {
|
||||
t.Errorf("operator() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
274
ext/pivot/pivot.go
Normal file
274
ext/pivot/pivot.go
Normal file
@@ -0,0 +1,274 @@
|
||||
// Package pivot implements a pivot virtual table.
|
||||
//
|
||||
// https://github.com/jakethaw/pivot_vtab
|
||||
package pivot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Register registers the pivot virtual table.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
sqlite3.CreateModule(db, "pivot", declare, declare)
|
||||
}
|
||||
|
||||
type table struct {
|
||||
db *sqlite3.Conn
|
||||
scan string
|
||||
cell string
|
||||
keys []string
|
||||
cols []*sqlite3.Value
|
||||
}
|
||||
|
||||
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
|
||||
if len(arg) != 3 {
|
||||
return nil, fmt.Errorf("pivot: wrong number of arguments")
|
||||
}
|
||||
|
||||
table := &table{db: db}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
table.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var sep string
|
||||
var create strings.Builder
|
||||
create.WriteString("CREATE TABLE x(")
|
||||
|
||||
// Row key query.
|
||||
table.scan = "SELECT * FROM\n" + arg[0]
|
||||
stmt, _, err := db.Prepare(table.scan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
table.keys = make([]string, stmt.ColumnCount())
|
||||
for i := range table.keys {
|
||||
name := sqlite3.QuoteIdentifier(stmt.ColumnName(i))
|
||||
table.keys[i] = name
|
||||
create.WriteString(sep)
|
||||
create.WriteString(name)
|
||||
sep = ","
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
// Column definition query.
|
||||
stmt, _, err = db.Prepare("SELECT * FROM\n" + arg[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stmt.ColumnCount() != 2 {
|
||||
return nil, fmt.Errorf("pivot: column definition query expects 2 result columns")
|
||||
}
|
||||
for stmt.Step() {
|
||||
name := sqlite3.QuoteIdentifier(stmt.ColumnText(1))
|
||||
table.cols = append(table.cols, stmt.ColumnValue(0).Dup())
|
||||
create.WriteString(",")
|
||||
create.WriteString(name)
|
||||
}
|
||||
stmt.Close()
|
||||
|
||||
// Pivot cell query.
|
||||
table.cell = "SELECT * FROM\n" + arg[2]
|
||||
stmt, _, err = db.Prepare(table.cell)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stmt.ColumnCount() != 1 {
|
||||
return nil, fmt.Errorf("pivot: cell query expects 1 result columns")
|
||||
}
|
||||
if stmt.BindCount() != len(table.keys)+1 {
|
||||
return nil, fmt.Errorf("pivot: cell query expects %d bound parameters", len(table.keys)+1)
|
||||
}
|
||||
|
||||
create.WriteByte(')')
|
||||
err = db.DeclareVTab(create.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return table, nil
|
||||
}
|
||||
|
||||
func (t *table) Close() error {
|
||||
for i := range t.cols {
|
||||
t.cols[i].Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *table) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
var idxStr strings.Builder
|
||||
idxStr.WriteString(t.scan)
|
||||
|
||||
argvIndex := 1
|
||||
sep := " WHERE "
|
||||
for i, cst := range idx.Constraint {
|
||||
if !cst.Usable || !(0 <= cst.Column && cst.Column < len(t.keys)) {
|
||||
continue
|
||||
}
|
||||
op := operator(cst.Op)
|
||||
if op == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
idxStr.WriteString(sep)
|
||||
idxStr.WriteString(t.keys[cst.Column])
|
||||
idxStr.WriteString(" ")
|
||||
idxStr.WriteString(op)
|
||||
idxStr.WriteString(" ?")
|
||||
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
ArgvIndex: argvIndex,
|
||||
Omit: true,
|
||||
}
|
||||
sep = " AND "
|
||||
argvIndex++
|
||||
}
|
||||
|
||||
sep = " ORDER BY "
|
||||
idx.OrderByConsumed = true
|
||||
for _, ord := range idx.OrderBy {
|
||||
if !(0 <= ord.Column && ord.Column < len(t.keys)) {
|
||||
idx.OrderByConsumed = false
|
||||
continue
|
||||
}
|
||||
idxStr.WriteString(sep)
|
||||
idxStr.WriteString(t.keys[ord.Column])
|
||||
idxStr.WriteString(" COLLATE ")
|
||||
idxStr.WriteString(idx.Collation(ord.Column))
|
||||
if ord.Desc {
|
||||
idxStr.WriteString(" DESC")
|
||||
}
|
||||
sep = ","
|
||||
}
|
||||
|
||||
idx.EstimatedCost = 1e9 / float64(argvIndex)
|
||||
idx.IdxStr = idxStr.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *table) Open() (sqlite3.VTabCursor, error) {
|
||||
return &cursor{table: t}, nil
|
||||
}
|
||||
|
||||
func (t *table) Rename(new string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type cursor struct {
|
||||
table *table
|
||||
scan *sqlite3.Stmt
|
||||
cell *sqlite3.Stmt
|
||||
rowID int64
|
||||
}
|
||||
|
||||
func (c *cursor) Close() error {
|
||||
return errors.Join(c.scan.Close(), c.cell.Close())
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
err := c.scan.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.scan, _, err = c.table.db.Prepare(idxStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, arg := range arg {
|
||||
err := c.scan.BindValue(i+1, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.cell == nil {
|
||||
c.cell, _, err = c.table.db.Prepare(c.table.cell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.rowID = 0
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (c *cursor) Next() error {
|
||||
if c.scan.Step() {
|
||||
count := c.scan.ColumnCount()
|
||||
for i := 0; i < count; i++ {
|
||||
err := c.cell.BindValue(i+1, c.scan.ColumnValue(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.rowID++
|
||||
}
|
||||
return c.scan.Err()
|
||||
}
|
||||
|
||||
func (c *cursor) EOF() bool {
|
||||
return !c.scan.Busy()
|
||||
}
|
||||
|
||||
func (c *cursor) RowID() (int64, error) {
|
||||
return c.rowID, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
|
||||
count := c.scan.ColumnCount()
|
||||
if col < count {
|
||||
ctx.ResultValue(c.scan.ColumnValue(col))
|
||||
return nil
|
||||
}
|
||||
|
||||
err := c.cell.BindValue(count+1, *c.table.cols[col-count])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.cell.Step() {
|
||||
ctx.ResultValue(c.cell.ColumnValue(0))
|
||||
}
|
||||
return c.cell.Reset()
|
||||
}
|
||||
|
||||
func operator(op sqlite3.IndexConstraintOp) string {
|
||||
switch op {
|
||||
case sqlite3.INDEX_CONSTRAINT_EQ:
|
||||
return "="
|
||||
case sqlite3.INDEX_CONSTRAINT_LT:
|
||||
return "<"
|
||||
case sqlite3.INDEX_CONSTRAINT_GT:
|
||||
return ">"
|
||||
case sqlite3.INDEX_CONSTRAINT_LE:
|
||||
return "<="
|
||||
case sqlite3.INDEX_CONSTRAINT_GE:
|
||||
return ">="
|
||||
case sqlite3.INDEX_CONSTRAINT_NE:
|
||||
return "<>"
|
||||
case sqlite3.INDEX_CONSTRAINT_MATCH:
|
||||
return "MATCH"
|
||||
case sqlite3.INDEX_CONSTRAINT_LIKE:
|
||||
return "LIKE"
|
||||
case sqlite3.INDEX_CONSTRAINT_GLOB:
|
||||
return "GLOB"
|
||||
case sqlite3.INDEX_CONSTRAINT_REGEXP:
|
||||
return "REGEXP"
|
||||
case sqlite3.INDEX_CONSTRAINT_IS, sqlite3.INDEX_CONSTRAINT_ISNULL:
|
||||
return "IS"
|
||||
case sqlite3.INDEX_CONSTRAINT_ISNOT, sqlite3.INDEX_CONSTRAINT_ISNOTNULL:
|
||||
return "IS NOT"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
219
ext/pivot/pivot_test.go
Normal file
219
ext/pivot/pivot_test.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package pivot_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/pivot"
|
||||
)
|
||||
|
||||
// https://antonz.org/sqlite-pivot-table/
|
||||
func Example() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
pivot.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE sales(product TEXT, year INT, income DECIMAL);
|
||||
INSERT INTO sales(product, year, income) VALUES
|
||||
('alpha', 2020, 100),
|
||||
('alpha', 2021, 120),
|
||||
('alpha', 2022, 130),
|
||||
('alpha', 2023, 140),
|
||||
('beta', 2020, 10),
|
||||
('beta', 2021, 20),
|
||||
('beta', 2022, 40),
|
||||
('beta', 2023, 80),
|
||||
('gamma', 2020, 80),
|
||||
('gamma', 2021, 75),
|
||||
('gamma', 2022, 78),
|
||||
('gamma', 2023, 80);
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE v_sales USING pivot(
|
||||
-- rows
|
||||
(SELECT DISTINCT product FROM sales),
|
||||
-- columns
|
||||
(SELECT DISTINCT year, year FROM sales),
|
||||
-- cells
|
||||
(SELECT sum(income) FROM sales WHERE product = ? AND year = ?)
|
||||
)`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT * FROM v_sales`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
cols := make([]string, stmt.ColumnCount())
|
||||
for i := range cols {
|
||||
cols[i] = stmt.ColumnName(i)
|
||||
}
|
||||
fmt.Println(pretty(cols))
|
||||
for stmt.Step() {
|
||||
for i := range cols {
|
||||
cols[i] = stmt.ColumnText(i)
|
||||
}
|
||||
fmt.Println(pretty(cols))
|
||||
}
|
||||
if err := stmt.Reset(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// product 2020 2021 2022 2023
|
||||
// alpha 100 120 130 140
|
||||
// beta 10 20 40 80
|
||||
// gamma 80 75 78 80
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
pivot.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE TABLE r AS
|
||||
SELECT 1 id UNION SELECT 2 UNION SELECT 3;
|
||||
|
||||
CREATE TABLE c(
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
INSERT INTO c (name) VALUES
|
||||
('a'),('b'),('c'),('d');
|
||||
|
||||
CREATE TABLE x(
|
||||
r_id INT,
|
||||
c_id INT,
|
||||
val TEXT
|
||||
);
|
||||
INSERT INTO x (r_id, c_id, val)
|
||||
SELECT r.id, c.id, c.name || r.id
|
||||
FROM c, r;
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE v_x USING pivot(
|
||||
-- rows
|
||||
(SELECT id r_id FROM r),
|
||||
-- columns
|
||||
(SELECT id c_id, name FROM c),
|
||||
-- cells
|
||||
(SELECT val FROM x WHERE r_id = ?1 AND c_id = ?2)
|
||||
)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT * FROM v_x WHERE rowid <> 0 AND r_id <> 1 ORDER BY rowid, r_id DESC LIMIT 1`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
if got := stmt.ColumnInt(0); got != 3 {
|
||||
t.Errorf("got %d, want 3", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
pivot.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE pivot USING pivot()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot(SELECT 1, SELECT 2, SELECT 3)`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot((SELECT 1), SELECT 2, SELECT 3)`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot((SELECT 1), (SELECT 2), SELECT 3)`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot((SELECT 1), (SELECT 1, 2), SELECT 3)`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot((SELECT 1), (SELECT 1, 2), (SELECT 3, 4))`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING pivot((SELECT 1), (SELECT 1, 2), (SELECT 3))`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
}
|
||||
|
||||
func pretty(cols []string) string {
|
||||
var buf strings.Builder
|
||||
for i, s := range cols {
|
||||
if i != 0 {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
for buf.Len()%8 != 0 {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
buf.WriteString(s)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
213
ext/statement/stmt.go
Normal file
213
ext/statement/stmt.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// Package statement defines table-valued functions using SQL.
|
||||
//
|
||||
// It can be used to create "parametrized views":
|
||||
// pre-packaged queries that can be parametrized at query execution time.
|
||||
//
|
||||
// https://github.com/0x09/sqlite-statement-vtab
|
||||
package statement
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
// Register registers the statement virtual table.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
sqlite3.CreateModule(db, "statement", declare, declare)
|
||||
}
|
||||
|
||||
type table struct {
|
||||
stmt *sqlite3.Stmt
|
||||
sql string
|
||||
inuse bool
|
||||
}
|
||||
|
||||
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (*table, error) {
|
||||
if len(arg) != 1 {
|
||||
return nil, fmt.Errorf("statement: wrong number of arguments")
|
||||
}
|
||||
|
||||
sql := "SELECT * FROM\n" + arg[0]
|
||||
|
||||
stmt, _, err := db.Prepare(sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sep string
|
||||
var str strings.Builder
|
||||
str.WriteString("CREATE TABLE x(")
|
||||
outputs := stmt.ColumnCount()
|
||||
for i := 0; i < outputs; i++ {
|
||||
name := sqlite3.QuoteIdentifier(stmt.ColumnName(i))
|
||||
str.WriteString(sep)
|
||||
str.WriteString(name)
|
||||
str.WriteString(" ")
|
||||
str.WriteString(stmt.ColumnDeclType(i))
|
||||
sep = ","
|
||||
}
|
||||
inputs := stmt.BindCount()
|
||||
for i := 1; i <= inputs; i++ {
|
||||
str.WriteString(sep)
|
||||
name := stmt.BindName(i)
|
||||
if name == "" {
|
||||
str.WriteString("[")
|
||||
str.WriteString(strconv.Itoa(i))
|
||||
str.WriteString("] HIDDEN")
|
||||
} else {
|
||||
str.WriteString(sqlite3.QuoteIdentifier(name[1:]))
|
||||
str.WriteString(" HIDDEN")
|
||||
}
|
||||
sep = ","
|
||||
}
|
||||
str.WriteByte(')')
|
||||
|
||||
err = db.DeclareVTab(str.String())
|
||||
if err != nil {
|
||||
stmt.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &table{sql: sql, stmt: stmt}, nil
|
||||
}
|
||||
|
||||
func (t *table) Close() error {
|
||||
return t.stmt.Close()
|
||||
}
|
||||
|
||||
func (t *table) BestIndex(idx *sqlite3.IndexInfo) error {
|
||||
idx.EstimatedCost = 1000
|
||||
|
||||
var argvIndex = 1
|
||||
var needIndex bool
|
||||
var listIndex []int
|
||||
outputs := t.stmt.ColumnCount()
|
||||
for i, cst := range idx.Constraint {
|
||||
// Skip if this is a constraint on one of our output columns.
|
||||
if cst.Column < outputs {
|
||||
continue
|
||||
}
|
||||
|
||||
// A given query plan is only usable if all provided input columns
|
||||
// are usable and have equal constraints only.
|
||||
if !cst.Usable || cst.Op != sqlite3.INDEX_CONSTRAINT_EQ {
|
||||
return sqlite3.CONSTRAINT
|
||||
}
|
||||
|
||||
// The non-zero argvIdx values must be contiguous.
|
||||
// If they're not, build a list and serialize it through IdxStr.
|
||||
nextIndex := cst.Column - outputs + 1
|
||||
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
|
||||
ArgvIndex: argvIndex,
|
||||
Omit: true,
|
||||
}
|
||||
if nextIndex != argvIndex {
|
||||
needIndex = true
|
||||
}
|
||||
listIndex = append(listIndex, nextIndex)
|
||||
argvIndex++
|
||||
}
|
||||
|
||||
if needIndex {
|
||||
buf, err := json.Marshal(listIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idx.IdxStr = unsafe.String(&buf[0], len(buf))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *table) Open() (sqlite3.VTabCursor, error) {
|
||||
stmt := t.stmt
|
||||
if !t.inuse {
|
||||
t.inuse = true
|
||||
} else {
|
||||
var err error
|
||||
stmt, _, err = t.stmt.Conn().Prepare(t.sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &cursor{table: t, stmt: stmt}, nil
|
||||
}
|
||||
|
||||
func (t *table) Rename(new string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type cursor struct {
|
||||
table *table
|
||||
stmt *sqlite3.Stmt
|
||||
arg []sqlite3.Value
|
||||
rowID int64
|
||||
}
|
||||
|
||||
func (c *cursor) Close() error {
|
||||
if c.stmt == c.table.stmt {
|
||||
c.table.inuse = false
|
||||
c.stmt.ClearBindings()
|
||||
return c.stmt.Reset()
|
||||
}
|
||||
return c.stmt.Close()
|
||||
}
|
||||
|
||||
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
|
||||
c.arg = arg
|
||||
c.rowID = 0
|
||||
c.stmt.ClearBindings()
|
||||
if err := c.stmt.Reset(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var list []int
|
||||
if idxStr != "" {
|
||||
buf := unsafe.Slice(unsafe.StringData(idxStr), len(idxStr))
|
||||
err := json.Unmarshal(buf, &list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i, arg := range arg {
|
||||
param := i + 1
|
||||
if list != nil {
|
||||
param = list[i]
|
||||
}
|
||||
err := c.stmt.BindValue(param, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func (c *cursor) Next() error {
|
||||
if c.stmt.Step() {
|
||||
c.rowID++
|
||||
}
|
||||
return c.stmt.Err()
|
||||
}
|
||||
|
||||
func (c *cursor) EOF() bool {
|
||||
return !c.stmt.Busy()
|
||||
}
|
||||
|
||||
func (c *cursor) RowID() (int64, error) {
|
||||
return c.rowID, nil
|
||||
}
|
||||
|
||||
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
|
||||
switch outputs := c.stmt.ColumnCount(); {
|
||||
case col < outputs:
|
||||
ctx.ResultValue(c.stmt.ColumnValue(col))
|
||||
case col-outputs < len(c.arg):
|
||||
ctx.ResultValue(c.arg[col-outputs])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
145
ext/statement/stmt_test.go
Normal file
145
ext/statement/stmt_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package statement_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/statement"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
statement.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE split_date USING statement((
|
||||
SELECT
|
||||
strftime('%Y', :date) AS year,
|
||||
strftime('%m', :date) AS month,
|
||||
strftime('%d', :date) AS day
|
||||
))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT * FROM split_date('2022-02-22')`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
fmt.Printf("Twosday was %d-%d-%d", stmt.ColumnInt(0), stmt.ColumnInt(1), stmt.ColumnInt(2))
|
||||
}
|
||||
if err := stmt.Reset(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Twosday was 2022-2-22
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
statement.Register(db)
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE arguments USING statement((SELECT ? AS a, ? AS b, ? AS c))
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
SELECT * from arguments WHERE [2] = 'y' AND [3] = 'z'
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`
|
||||
CREATE VIRTUAL TABLE hypot USING statement((SELECT sqrt(:x * :x + :y * :y) AS hypotenuse))
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`
|
||||
SELECT x, y, * FROM hypot WHERE x = 3 AND y = 4
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
x := stmt.ColumnInt(0)
|
||||
y := stmt.ColumnInt(1)
|
||||
hypot := stmt.ColumnInt(2)
|
||||
if x != 3 || y != 4 || hypot != 5 {
|
||||
t.Errorf("hypot(%d, %d) = %d", x, y, hypot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
statement.Register(db)
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement()`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement(SELECT 1, SELECT 2)`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement((SELECT 1, SELECT 2))`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement((SELECT 1; SELECT 2))`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement((CREATE TABLE x(val)))`)
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
}
|
||||
47
ext/stats/TODO.md
Normal file
47
ext/stats/TODO.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# ANSI SQL Aggregate Functions
|
||||
|
||||
https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
|
||||
|
||||
## Built in aggregates
|
||||
|
||||
- [x] `COUNT(*)`
|
||||
- [x] `COUNT(expression)`
|
||||
- [x] `SUM(expression)`
|
||||
- [x] `AVG(expression)`
|
||||
- [x] `MIN(expression)`
|
||||
- [x] `MAX(expression)`
|
||||
|
||||
https://sqlite.org/lang_aggfunc.html
|
||||
|
||||
## Statistical aggregates
|
||||
|
||||
- [x] `STDDEV_POP(expression)`
|
||||
- [x] `STDDEV_SAMP(expression)`
|
||||
- [x] `VAR_POP(expression)`
|
||||
- [x] `VAR_SAMP(expression)`
|
||||
- [x] `COVAR_POP(dependent, independent)`
|
||||
- [x] `COVAR_SAMP(dependent, independent)`
|
||||
- [x] `CORR(dependent, independent)`
|
||||
|
||||
## Linear regression aggregates
|
||||
|
||||
- [X] `REGR_AVGX(dependent, independent)`
|
||||
- [X] `REGR_AVGY(dependent, independent)`
|
||||
- [X] `REGR_SXX(dependent, independent)`
|
||||
- [X] `REGR_SYY(dependent, independent)`
|
||||
- [X] `REGR_SXY(dependent, independent)`
|
||||
- [X] `REGR_COUNT(dependent, independent)`
|
||||
- [X] `REGR_SLOPE(dependent, independent)`
|
||||
- [X] `REGR_INTERCEPT(dependent, independent)`
|
||||
- [X] `REGR_R2(dependent, independent)`
|
||||
|
||||
## Set aggregates
|
||||
|
||||
- [X] `CUME_DIST() OVER window`
|
||||
- [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
|
||||
169
ext/stats/stats.go
Normal file
169
ext/stats/stats.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Package stats provides aggregate functions for statistics.
|
||||
//
|
||||
// Provided functions:
|
||||
// - stddev_pop: population standard deviation
|
||||
// - stddev_samp: sample standard deviation
|
||||
// - var_pop: population variance
|
||||
// - var_samp: sample variance
|
||||
// - covar_pop: population covariance
|
||||
// - covar_samp: sample covariance
|
||||
// - corr: correlation coefficient
|
||||
// - regr_r2: correlation coefficient squared
|
||||
// - regr_avgx: average of the independent variable
|
||||
// - regr_avgy: average of the dependent variable
|
||||
// - regr_sxx: sum of the squares of the independent variable
|
||||
// - regr_syy: sum of the squares of the dependent variable
|
||||
// - regr_sxy: sum of the products of each pair of variables
|
||||
// - regr_count: count non-null pairs of variables
|
||||
// - 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
|
||||
//
|
||||
// These join the [Built-in Aggregate Functions]:
|
||||
// - count: count rows/values
|
||||
// - sum: sum values
|
||||
// - avg: average value
|
||||
// - min: minimum value
|
||||
// - max: maximum value
|
||||
//
|
||||
// See: [ANSI SQL Aggregate Functions]
|
||||
//
|
||||
// [Built-in Aggregate Functions]: https://sqlite.org/lang_aggfunc.html
|
||||
// [ANSI SQL Aggregate Functions]: https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
|
||||
package stats
|
||||
|
||||
import "github.com/ncruces/go-sqlite3"
|
||||
|
||||
// Register registers statistics functions.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
db.CreateWindowFunction("var_pop", 1, flags, newVariance(var_pop))
|
||||
db.CreateWindowFunction("var_samp", 1, flags, newVariance(var_samp))
|
||||
db.CreateWindowFunction("stddev_pop", 1, flags, newVariance(stddev_pop))
|
||||
db.CreateWindowFunction("stddev_samp", 1, flags, newVariance(stddev_samp))
|
||||
db.CreateWindowFunction("covar_pop", 2, flags, newCovariance(var_pop))
|
||||
db.CreateWindowFunction("covar_samp", 2, flags, newCovariance(var_samp))
|
||||
db.CreateWindowFunction("corr", 2, flags, newCovariance(corr))
|
||||
db.CreateWindowFunction("regr_r2", 2, flags, newCovariance(regr_r2))
|
||||
db.CreateWindowFunction("regr_sxx", 2, flags, newCovariance(regr_sxx))
|
||||
db.CreateWindowFunction("regr_syy", 2, flags, newCovariance(regr_syy))
|
||||
db.CreateWindowFunction("regr_sxy", 2, flags, newCovariance(regr_sxy))
|
||||
db.CreateWindowFunction("regr_avgx", 2, flags, newCovariance(regr_avgx))
|
||||
db.CreateWindowFunction("regr_avgy", 2, flags, newCovariance(regr_avgy))
|
||||
db.CreateWindowFunction("regr_slope", 2, flags, newCovariance(regr_slope))
|
||||
db.CreateWindowFunction("regr_intercept", 2, flags, newCovariance(regr_intercept))
|
||||
db.CreateWindowFunction("regr_count", 2, flags, newCovariance(regr_count))
|
||||
db.CreateWindowFunction("regr_json", 2, flags, newCovariance(regr_json))
|
||||
}
|
||||
|
||||
const (
|
||||
var_pop = iota
|
||||
var_samp
|
||||
stddev_pop
|
||||
stddev_samp
|
||||
corr
|
||||
regr_r2
|
||||
regr_sxx
|
||||
regr_syy
|
||||
regr_sxy
|
||||
regr_avgx
|
||||
regr_avgy
|
||||
regr_slope
|
||||
regr_intercept
|
||||
regr_count
|
||||
regr_json
|
||||
)
|
||||
|
||||
func newVariance(kind int) func() sqlite3.AggregateFunction {
|
||||
return func() sqlite3.AggregateFunction { return &variance{kind: kind} }
|
||||
}
|
||||
|
||||
type variance struct {
|
||||
kind int
|
||||
welford
|
||||
}
|
||||
|
||||
func (fn *variance) Value(ctx sqlite3.Context) {
|
||||
var r float64
|
||||
switch fn.kind {
|
||||
case var_pop:
|
||||
r = fn.var_pop()
|
||||
case var_samp:
|
||||
r = fn.var_samp()
|
||||
case stddev_pop:
|
||||
r = fn.stddev_pop()
|
||||
case stddev_samp:
|
||||
r = fn.stddev_samp()
|
||||
}
|
||||
ctx.ResultFloat(r)
|
||||
}
|
||||
|
||||
func (fn *variance) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if a := arg[0]; a.NumericType() != sqlite3.NULL {
|
||||
fn.enqueue(a.Float())
|
||||
}
|
||||
}
|
||||
|
||||
func (fn *variance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if a := arg[0]; a.NumericType() != sqlite3.NULL {
|
||||
fn.dequeue(a.Float())
|
||||
}
|
||||
}
|
||||
|
||||
func newCovariance(kind int) func() sqlite3.AggregateFunction {
|
||||
return func() sqlite3.AggregateFunction { return &covariance{kind: kind} }
|
||||
}
|
||||
|
||||
type covariance struct {
|
||||
kind int
|
||||
welford2
|
||||
}
|
||||
|
||||
func (fn *covariance) Value(ctx sqlite3.Context) {
|
||||
var r float64
|
||||
switch fn.kind {
|
||||
case var_pop:
|
||||
r = fn.covar_pop()
|
||||
case var_samp:
|
||||
r = fn.covar_samp()
|
||||
case corr:
|
||||
r = fn.correlation()
|
||||
case regr_r2:
|
||||
r = fn.regr_r2()
|
||||
case regr_sxx:
|
||||
r = fn.regr_sxx()
|
||||
case regr_syy:
|
||||
r = fn.regr_syy()
|
||||
case regr_sxy:
|
||||
r = fn.regr_sxy()
|
||||
case regr_avgx:
|
||||
r = fn.regr_avgx()
|
||||
case regr_avgy:
|
||||
r = fn.regr_avgy()
|
||||
case regr_slope:
|
||||
r = fn.regr_slope()
|
||||
case regr_intercept:
|
||||
r = fn.regr_intercept()
|
||||
case regr_count:
|
||||
ctx.ResultInt64(fn.regr_count())
|
||||
return
|
||||
case regr_json:
|
||||
ctx.ResultText(fn.regr_json())
|
||||
return
|
||||
}
|
||||
ctx.ResultFloat(r)
|
||||
}
|
||||
|
||||
func (fn *covariance) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
a, b := arg[0], arg[1]
|
||||
if a.NumericType() != sqlite3.NULL && b.NumericType() != sqlite3.NULL {
|
||||
fn.enqueue(a.Float(), b.Float())
|
||||
}
|
||||
}
|
||||
|
||||
func (fn *covariance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
a, b := arg[0], arg[1]
|
||||
if a.NumericType() != sqlite3.NULL && b.NumericType() != sqlite3.NULL {
|
||||
fn.dequeue(a.Float(), b.Float())
|
||||
}
|
||||
}
|
||||
243
ext/stats/stats_test.go
Normal file
243
ext/stats/stats_test.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package stats_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/stats"
|
||||
)
|
||||
|
||||
func TestRegister_variance(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 IF NOT EXISTS 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
|
||||
sum(x), avg(x),
|
||||
var_samp(x), var_pop(x),
|
||||
stddev_samp(x), stddev_pop(x)
|
||||
FROM data`)
|
||||
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)
|
||||
}
|
||||
if got := stmt.ColumnFloat(1); got != 10 {
|
||||
t.Errorf("got %v, want 10", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(2); got != 30 {
|
||||
t.Errorf("got %v, want 30", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(3); got != 22.5 {
|
||||
t.Errorf("got %v, want 22.5", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(4); got != math.Sqrt(30) {
|
||||
t.Errorf("got %v, want √30", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(5); got != math.Sqrt(22.5) {
|
||||
t.Errorf("got %v, want √22.5", got)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
stmt, _, err := db.Prepare(`SELECT var_samp(x) OVER (ROWS 1 PRECEDING) FROM data`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_covariance(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 IF NOT EXISTS data (y, x)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO data (y, x) VALUES (3, 70), (5, 80), (2, 60), (7, 90), (4, 75)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT
|
||||
corr(y, x), covar_samp(y, x), covar_pop(y, x),
|
||||
regr_avgy(y, x), regr_avgx(y, x),
|
||||
regr_syy(y, x), regr_sxx(y, x), regr_sxy(y, x),
|
||||
regr_slope(y, x), regr_intercept(y, x), regr_r2(y, x),
|
||||
regr_count(y, x), regr_json(y, x)
|
||||
FROM data`)
|
||||
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)
|
||||
}
|
||||
if got := stmt.ColumnFloat(1); got != 21.25 {
|
||||
t.Errorf("got %v, want 21.25", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(2); got != 17 {
|
||||
t.Errorf("got %v, want 17", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(3); got != 4.2 {
|
||||
t.Errorf("got %v, want 4.2", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(4); got != 75 {
|
||||
t.Errorf("got %v, want 75", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(5); got != 14.8 {
|
||||
t.Errorf("got %v, want 14.8", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(6); got != 500 {
|
||||
t.Errorf("got %v, want 500", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(7); got != 85 {
|
||||
t.Errorf("got %v, want 85", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(8); got != 0.17 {
|
||||
t.Errorf("got %v, want 0.17", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(9); got != -8.55 {
|
||||
t.Errorf("got %v, want -8.55", got)
|
||||
}
|
||||
if got := stmt.ColumnFloat(10); got != 0.9763513513513513 {
|
||||
t.Errorf("got %v, want 0.9763513513513513", got)
|
||||
}
|
||||
if got := stmt.ColumnInt(11); got != 5 {
|
||||
t.Errorf("got %v, want 5", got)
|
||||
}
|
||||
var a map[string]float64
|
||||
if err := stmt.ColumnJSON(12, &a); err != nil {
|
||||
t.Error(err)
|
||||
} else if got := a["count"]; got != 5 {
|
||||
t.Errorf("got %v, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
stmt, _, err := db.Prepare(`SELECT covar_samp(y, x) OVER (ROWS 1 PRECEDING) FROM data`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_average(b *testing.B) {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT avg(value) FROM generate_series(0, ?)`)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
err = stmt.BindInt(1, b.N)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
if stmt.Step() {
|
||||
want := float64(b.N) / 2
|
||||
if got := stmt.ColumnFloat(0); got != want {
|
||||
b.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
err = stmt.Err()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_variance(b *testing.B) {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stats.Register(db)
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT var_pop(value) FROM generate_series(0, ?)`)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
err = stmt.BindInt(1, b.N)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
if stmt.Step() && b.N > 100 {
|
||||
want := float64(b.N*b.N) / 12
|
||||
if got := stmt.ColumnFloat(0); want > (got-want)*float64(b.N) {
|
||||
b.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
err = stmt.Err()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
181
ext/stats/welford.go
Normal file
181
ext/stats/welford.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Welford's algorithm with Kahan summation:
|
||||
// https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm
|
||||
// https://en.wikipedia.org/wiki/Kahan_summation_algorithm
|
||||
|
||||
// See also:
|
||||
// https://duckdb.org/docs/sql/aggregates.html#statistical-aggregates
|
||||
|
||||
type welford struct {
|
||||
m1, m2 kahan
|
||||
n int64
|
||||
}
|
||||
|
||||
func (w welford) average() float64 {
|
||||
return w.m1.hi
|
||||
}
|
||||
|
||||
func (w welford) var_pop() float64 {
|
||||
return w.m2.hi / float64(w.n)
|
||||
}
|
||||
|
||||
func (w welford) var_samp() float64 {
|
||||
return w.m2.hi / float64(w.n-1) // Bessel's correction
|
||||
}
|
||||
|
||||
func (w welford) stddev_pop() float64 {
|
||||
return math.Sqrt(w.var_pop())
|
||||
}
|
||||
|
||||
func (w welford) stddev_samp() float64 {
|
||||
return math.Sqrt(w.var_samp())
|
||||
}
|
||||
|
||||
func (w *welford) enqueue(x float64) {
|
||||
w.n++
|
||||
d1 := x - w.m1.hi - w.m1.lo
|
||||
w.m1.add(d1 / float64(w.n))
|
||||
d2 := x - w.m1.hi - w.m1.lo
|
||||
w.m2.add(d1 * d2)
|
||||
}
|
||||
|
||||
func (w *welford) dequeue(x float64) {
|
||||
w.n--
|
||||
d1 := x - w.m1.hi - w.m1.lo
|
||||
w.m1.sub(d1 / float64(w.n))
|
||||
d2 := x - w.m1.hi - w.m1.lo
|
||||
w.m2.sub(d1 * d2)
|
||||
}
|
||||
|
||||
type welford2 struct {
|
||||
m1y, m2y kahan
|
||||
m1x, m2x kahan
|
||||
cov kahan
|
||||
n int64
|
||||
}
|
||||
|
||||
func (w welford2) covar_pop() float64 {
|
||||
return w.cov.hi / float64(w.n)
|
||||
}
|
||||
|
||||
func (w welford2) covar_samp() float64 {
|
||||
return w.cov.hi / float64(w.n-1) // Bessel's correction
|
||||
}
|
||||
|
||||
func (w welford2) correlation() float64 {
|
||||
return w.cov.hi / math.Sqrt(w.m2y.hi*w.m2x.hi)
|
||||
}
|
||||
|
||||
func (w welford2) regr_avgy() float64 {
|
||||
return w.m1y.hi
|
||||
}
|
||||
|
||||
func (w welford2) regr_avgx() float64 {
|
||||
return w.m1x.hi
|
||||
}
|
||||
|
||||
func (w welford2) regr_syy() float64 {
|
||||
return w.m2y.hi
|
||||
}
|
||||
|
||||
func (w welford2) regr_sxx() float64 {
|
||||
return w.m2x.hi
|
||||
}
|
||||
|
||||
func (w welford2) regr_sxy() float64 {
|
||||
return w.cov.hi
|
||||
}
|
||||
|
||||
func (w welford2) regr_count() int64 {
|
||||
return w.n
|
||||
}
|
||||
|
||||
func (w welford2) regr_slope() float64 {
|
||||
return w.cov.hi / w.m2x.hi
|
||||
}
|
||||
|
||||
func (w welford2) regr_intercept() float64 {
|
||||
slope := -w.regr_slope()
|
||||
hi := math.FMA(slope, w.m1x.hi, w.m1y.hi)
|
||||
lo := math.FMA(slope, w.m1x.lo, w.m1y.lo)
|
||||
return hi + lo
|
||||
}
|
||||
|
||||
func (w welford2) regr_r2() float64 {
|
||||
return w.cov.hi * w.cov.hi / (w.m2y.hi * w.m2x.hi)
|
||||
}
|
||||
|
||||
func (w welford2) regr_json() string {
|
||||
var json strings.Builder
|
||||
var num [32]byte
|
||||
json.Grow(128)
|
||||
json.WriteString(`{"count":`)
|
||||
json.Write(strconv.AppendInt(num[:0], w.regr_count(), 10))
|
||||
json.WriteString(`,"avgy":`)
|
||||
json.Write(strconv.AppendFloat(num[:0], w.regr_avgy(), 'g', -1, 64))
|
||||
json.WriteString(`,"avgx":`)
|
||||
json.Write(strconv.AppendFloat(num[:0], w.regr_avgx(), 'g', -1, 64))
|
||||
json.WriteString(`,"syy":`)
|
||||
json.Write(strconv.AppendFloat(num[:0], w.regr_syy(), 'g', -1, 64))
|
||||
json.WriteString(`,"sxx":`)
|
||||
json.Write(strconv.AppendFloat(num[:0], w.regr_sxx(), 'g', -1, 64))
|
||||
json.WriteString(`,"sxy":`)
|
||||
json.Write(strconv.AppendFloat(num[:0], w.regr_sxy(), 'g', -1, 64))
|
||||
json.WriteString(`,"slope":`)
|
||||
json.Write(strconv.AppendFloat(num[:0], w.regr_slope(), 'g', -1, 64))
|
||||
json.WriteString(`,"intercept":`)
|
||||
json.Write(strconv.AppendFloat(num[:0], w.regr_intercept(), 'g', -1, 64))
|
||||
json.WriteString(`,"r2":`)
|
||||
json.Write(strconv.AppendFloat(num[:0], w.regr_r2(), 'g', -1, 64))
|
||||
json.WriteByte('}')
|
||||
return json.String()
|
||||
}
|
||||
|
||||
func (w *welford2) enqueue(y, x float64) {
|
||||
w.n++
|
||||
d1y := y - w.m1y.hi - w.m1y.lo
|
||||
d1x := x - w.m1x.hi - w.m1x.lo
|
||||
w.m1y.add(d1y / float64(w.n))
|
||||
w.m1x.add(d1x / float64(w.n))
|
||||
d2y := y - w.m1y.hi - w.m1y.lo
|
||||
d2x := x - w.m1x.hi - w.m1x.lo
|
||||
w.m2y.add(d1y * d2y)
|
||||
w.m2x.add(d1x * d2x)
|
||||
w.cov.add(d1y * d2x)
|
||||
}
|
||||
|
||||
func (w *welford2) dequeue(y, x float64) {
|
||||
w.n--
|
||||
d1y := y - w.m1y.hi - w.m1y.lo
|
||||
d1x := x - w.m1x.hi - w.m1x.lo
|
||||
w.m1y.sub(d1y / float64(w.n))
|
||||
w.m1x.sub(d1x / float64(w.n))
|
||||
d2y := y - w.m1y.hi - w.m1y.lo
|
||||
d2x := x - w.m1x.hi - w.m1x.lo
|
||||
w.m2y.sub(d1y * d2y)
|
||||
w.m2x.sub(d1x * d2x)
|
||||
w.cov.sub(d1y * d2x)
|
||||
}
|
||||
|
||||
type kahan struct{ hi, lo float64 }
|
||||
|
||||
func (k *kahan) add(x float64) {
|
||||
y := k.lo + x
|
||||
t := k.hi + y
|
||||
k.lo = y - (t - k.hi)
|
||||
k.hi = t
|
||||
}
|
||||
|
||||
func (k *kahan) sub(x float64) {
|
||||
y := k.lo - x
|
||||
t := k.hi + y
|
||||
k.lo = y - (t - k.hi)
|
||||
k.hi = t
|
||||
}
|
||||
81
ext/stats/welford_test.go
Normal file
81
ext/stats/welford_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_welford(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var s1, s2 welford
|
||||
|
||||
s1.enqueue(4)
|
||||
s1.enqueue(7)
|
||||
s1.enqueue(13)
|
||||
s1.enqueue(16)
|
||||
if got := s1.average(); got != 10 {
|
||||
t.Errorf("got %v, want 10", got)
|
||||
}
|
||||
if got := s1.var_samp(); got != 30 {
|
||||
t.Errorf("got %v, want 30", got)
|
||||
}
|
||||
if got := s1.var_pop(); got != 22.5 {
|
||||
t.Errorf("got %v, want 22.5", got)
|
||||
}
|
||||
if got := s1.stddev_samp(); got != math.Sqrt(30) {
|
||||
t.Errorf("got %v, want √30", got)
|
||||
}
|
||||
if got := s1.stddev_pop(); got != math.Sqrt(22.5) {
|
||||
t.Errorf("got %v, want √22.5", got)
|
||||
}
|
||||
|
||||
s1.dequeue(4)
|
||||
s2.enqueue(7)
|
||||
s2.enqueue(13)
|
||||
s2.enqueue(16)
|
||||
if s1.var_pop() != s2.var_pop() {
|
||||
t.Errorf("got %v, want %v", s1, s2)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_covar(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var c1, c2 welford2
|
||||
|
||||
c1.enqueue(3, 70)
|
||||
c1.enqueue(5, 80)
|
||||
c1.enqueue(2, 60)
|
||||
c1.enqueue(7, 90)
|
||||
c1.enqueue(4, 75)
|
||||
|
||||
if got := c1.covar_samp(); got != 21.25 {
|
||||
t.Errorf("got %v, want 21.25", got)
|
||||
}
|
||||
if got := c1.covar_pop(); got != 17 {
|
||||
t.Errorf("got %v, want 17", got)
|
||||
}
|
||||
|
||||
c1.dequeue(3, 70)
|
||||
c2.enqueue(5, 80)
|
||||
c2.enqueue(2, 60)
|
||||
c2.enqueue(7, 90)
|
||||
c2.enqueue(4, 75)
|
||||
if c1.covar_pop() != c2.covar_pop() {
|
||||
t.Errorf("got %v, want %v", c1.covar_pop(), c2.covar_pop())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_correlation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var c welford2
|
||||
c.enqueue(1, 3)
|
||||
c.enqueue(2, 2)
|
||||
c.enqueue(3, 1)
|
||||
|
||||
if got := c.correlation(); got != -1 {
|
||||
t.Errorf("got %v, want -1", got)
|
||||
}
|
||||
}
|
||||
181
ext/unicode/unicode.go
Normal file
181
ext/unicode/unicode.go
Normal file
@@ -0,0 +1,181 @@
|
||||
// Package unicode provides an alternative to the SQLite ICU extension.
|
||||
//
|
||||
// Like the [ICU extension], it provides Unicode aware:
|
||||
// - upper() and lower() functions,
|
||||
// - LIKE and REGEXP operators,
|
||||
// - collation sequences.
|
||||
//
|
||||
// The implementation is not 100% compatible with the [ICU extension]:
|
||||
// - upper() and lower() use [strings.ToUpper], [strings.ToLower] and [cases];
|
||||
// - the LIKE operator follows [strings.EqualFold] rules;
|
||||
// - the REGEXP operator uses Go [regexp/syntax];
|
||||
// - collation sequences use [collate].
|
||||
//
|
||||
// Expect subtle differences (e.g.) in the handling of Turkish case folding.
|
||||
//
|
||||
// [ICU extension]: https://sqlite.org/src/dir/ext/icu
|
||||
package unicode
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// Register registers Unicode aware functions for a database connection.
|
||||
func Register(db *sqlite3.Conn) {
|
||||
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
|
||||
|
||||
db.CreateFunction("like", 2, flags, like)
|
||||
db.CreateFunction("like", 3, flags, like)
|
||||
db.CreateFunction("upper", 1, flags, upper)
|
||||
db.CreateFunction("upper", 2, flags, upper)
|
||||
db.CreateFunction("lower", 1, flags, lower)
|
||||
db.CreateFunction("lower", 2, flags, lower)
|
||||
db.CreateFunction("regexp", 2, flags, regex)
|
||||
db.CreateFunction("icu_load_collation", 2, sqlite3.DIRECTONLY,
|
||||
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
name := arg[1].Text()
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err := RegisterCollation(db, arg[0].Text(), name)
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterCollation registers a Unicode collation sequence for a database connection.
|
||||
func RegisterCollation(db *sqlite3.Conn, locale, name string) error {
|
||||
tag, err := language.Parse(locale)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.CreateCollation(name, collate.New(tag).Compare)
|
||||
}
|
||||
|
||||
func upper(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if len(arg) == 1 {
|
||||
ctx.ResultRawText(bytes.ToUpper(arg[0].RawText()))
|
||||
return
|
||||
}
|
||||
cs, ok := ctx.GetAuxData(1).(cases.Caser)
|
||||
if !ok {
|
||||
t, err := language.Parse(arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
c := cases.Upper(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
cs = c
|
||||
}
|
||||
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
|
||||
}
|
||||
|
||||
func lower(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if len(arg) == 1 {
|
||||
ctx.ResultRawText(bytes.ToLower(arg[0].RawText()))
|
||||
return
|
||||
}
|
||||
cs, ok := ctx.GetAuxData(1).(cases.Caser)
|
||||
if !ok {
|
||||
t, err := language.Parse(arg[1].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
c := cases.Lower(t)
|
||||
ctx.SetAuxData(1, c)
|
||||
cs = c
|
||||
}
|
||||
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
|
||||
}
|
||||
|
||||
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, ok := ctx.GetAuxData(0).(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
re = r
|
||||
ctx.SetAuxData(0, re)
|
||||
}
|
||||
ctx.ResultBool(re.Match(arg[1].RawText()))
|
||||
}
|
||||
|
||||
func like(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
escape := rune(-1)
|
||||
if len(arg) == 3 {
|
||||
var size int
|
||||
b := arg[2].RawText()
|
||||
escape, size = utf8.DecodeRune(b)
|
||||
if size != len(b) {
|
||||
ctx.ResultError(util.ErrorString("ESCAPE expression must be a single character"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type likeData struct {
|
||||
*regexp.Regexp
|
||||
escape rune
|
||||
}
|
||||
|
||||
re, ok := ctx.GetAuxData(0).(likeData)
|
||||
if !ok || re.escape != escape {
|
||||
re = likeData{
|
||||
regexp.MustCompile(like2regex(arg[0].Text(), escape)),
|
||||
escape,
|
||||
}
|
||||
ctx.SetAuxData(0, re)
|
||||
}
|
||||
ctx.ResultBool(re.Match(arg[1].RawText()))
|
||||
}
|
||||
|
||||
func like2regex(pattern string, escape rune) string {
|
||||
var re strings.Builder
|
||||
start := 0
|
||||
literal := false
|
||||
re.Grow(len(pattern) + 10)
|
||||
re.WriteString(`(?is)\A`) // case insensitive, . matches any character
|
||||
for i, r := range pattern {
|
||||
if start < 0 {
|
||||
start = i
|
||||
}
|
||||
if literal {
|
||||
literal = false
|
||||
continue
|
||||
}
|
||||
var symbol string
|
||||
switch r {
|
||||
case '_':
|
||||
symbol = `.`
|
||||
case '%':
|
||||
symbol = `.*`
|
||||
case escape:
|
||||
literal = true
|
||||
default:
|
||||
continue
|
||||
}
|
||||
re.WriteString(regexp.QuoteMeta(pattern[start:i]))
|
||||
re.WriteString(symbol)
|
||||
start = -1
|
||||
}
|
||||
if start >= 0 {
|
||||
re.WriteString(regexp.QuoteMeta(pattern[start:]))
|
||||
}
|
||||
re.WriteString(`\z`)
|
||||
return re.String()
|
||||
}
|
||||
217
ext/unicode/unicode_test.go
Normal file
217
ext/unicode/unicode_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package unicode
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
exec := func(fn string) string {
|
||||
stmt, _, err := db.Prepare(`SELECT ` + fn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
return stmt.ColumnText(0)
|
||||
}
|
||||
t.Fatal(stmt.Err())
|
||||
return ""
|
||||
}
|
||||
|
||||
Register(db)
|
||||
|
||||
tests := []struct {
|
||||
test string
|
||||
want string
|
||||
}{
|
||||
{`upper('hello')`, "HELLO"},
|
||||
{`lower('HELLO')`, "hello"},
|
||||
{`upper('привет')`, "ПРИВЕТ"},
|
||||
{`lower('ПРИВЕТ')`, "привет"},
|
||||
{`upper('istanbul')`, "ISTANBUL"},
|
||||
{`upper('istanbul', 'tr-TR')`, "İSTANBUL"},
|
||||
{`lower('Dünyanın İlk Borsası', 'tr-TR')`, "dünyanın ilk borsası"},
|
||||
{`upper('Dünyanın İlk Borsası', 'tr-TR')`, "DÜNYANIN İLK BORSASI"},
|
||||
{`'Hello' REGEXP 'ell'`, "1"},
|
||||
{`'Hello' REGEXP 'el.'`, "1"},
|
||||
{`'Hello' LIKE 'hel_'`, "0"},
|
||||
{`'Hello' LIKE 'hel%'`, "1"},
|
||||
{`'Hello' LIKE 'h_llo'`, "1"},
|
||||
{`'Hello' LIKE 'hello'`, "1"},
|
||||
{`'Привет' LIKE 'ПРИВЕТ'`, "1"},
|
||||
{`'100%' LIKE '100|%' ESCAPE '|'`, "1"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.test, func(t *testing.T) {
|
||||
if got := exec(tt.test); got != tt.want {
|
||||
t.Errorf("exec(%q) = %q, want %q", tt.test, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_collation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
Register(db)
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO words (word) VALUES ('côte'), ('cote'), ('coter'), ('coté'), ('cotée'), ('côté')`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT icu_load_collation('fr_FR', 'french')`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT word FROM words ORDER BY word COLLATE french`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
got, want := []string{}, []string{"cote", "coté", "côte", "côté", "cotée", "coter"}
|
||||
|
||||
for stmt.Step() {
|
||||
got = append(got, stmt.ColumnText(0))
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Error("not equal")
|
||||
}
|
||||
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
Register(db)
|
||||
|
||||
err = db.Exec(`SELECT upper('hello', 'enUS')`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
if !errors.Is(err, sqlite3.ERROR) {
|
||||
t.Errorf("got %v, want sqlite3.ERROR", err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT lower('hello', 'enUS')`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
if !errors.Is(err, sqlite3.ERROR) {
|
||||
t.Errorf("got %v, want sqlite3.ERROR", err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT 'hello' REGEXP '\'`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
if !errors.Is(err, sqlite3.ERROR) {
|
||||
t.Errorf("got %v, want sqlite3.ERROR", err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT 'hello' LIKE 'HELLO' ESCAPE '\\'`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
if !errors.Is(err, sqlite3.ERROR) {
|
||||
t.Errorf("got %v, want sqlite3.ERROR", err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT icu_load_collation('enUS', 'error')`)
|
||||
if err == nil {
|
||||
t.Error("want error")
|
||||
}
|
||||
if !errors.Is(err, sqlite3.ERROR) {
|
||||
t.Errorf("got %v, want sqlite3.ERROR", err)
|
||||
}
|
||||
|
||||
err = db.Exec(`SELECT icu_load_collation('enUS', '')`)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_like2regex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const prefix = `(?is)\A`
|
||||
const sufix = `\z`
|
||||
tests := []struct {
|
||||
pattern string
|
||||
escape rune
|
||||
want string
|
||||
}{
|
||||
{`a`, -1, `a`},
|
||||
{`a.`, -1, `a\.`},
|
||||
{`a%`, -1, `a.*`},
|
||||
{`a\`, -1, `a\\`},
|
||||
{`a_b`, -1, `a.b`},
|
||||
{`a|b`, '|', `ab`},
|
||||
{`a|_`, '|', `a_`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.pattern, func(t *testing.T) {
|
||||
want := prefix + tt.want + sufix
|
||||
if got := like2regex(tt.pattern, tt.escape); got != want {
|
||||
t.Errorf("like2regex() = %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
106
ext/zorder/zorder_test.go
Normal file
106
ext/zorder/zorder_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
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"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
214
func.go
Normal file
214
func.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
// CollationNeeded registers a callback to be invoked
|
||||
// whenever an unknown collation sequence is required.
|
||||
//
|
||||
// https://sqlite.org/c3ref/collation_needed.html
|
||||
func (c *Conn) CollationNeeded(cb func(db *Conn, name string)) error {
|
||||
var enable uint64
|
||||
if cb != nil {
|
||||
enable = 1
|
||||
}
|
||||
r := c.call("sqlite3_collation_needed_go", uint64(c.handle), enable)
|
||||
if err := c.error(r); err != nil {
|
||||
return err
|
||||
}
|
||||
c.collation = cb
|
||||
return nil
|
||||
}
|
||||
|
||||
// AnyCollationNeeded uses [Conn.CollationNeeded] to register
|
||||
// a fake collating function for any unknown collating sequence.
|
||||
// The fake collating function works like BINARY.
|
||||
//
|
||||
// This can be used to load schemas that contain
|
||||
// one or more unknown collating sequences.
|
||||
func (c *Conn) AnyCollationNeeded() {
|
||||
c.call("sqlite3_anycollseq_init", uint64(c.handle), 0, 0)
|
||||
}
|
||||
|
||||
// CreateCollation defines a new collating sequence.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_collation.html
|
||||
func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
funcPtr := util.AddHandle(c.ctx, fn)
|
||||
r := c.call("sqlite3_create_collation_go",
|
||||
uint64(c.handle), uint64(namePtr), uint64(funcPtr))
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
// CreateFunction defines a new scalar SQL function.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
func (c *Conn) CreateFunction(name string, nArg int, flag FunctionFlag, fn ScalarFunction) error {
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
funcPtr := util.AddHandle(c.ctx, fn)
|
||||
r := c.call("sqlite3_create_function_go",
|
||||
uint64(c.handle), uint64(namePtr), uint64(nArg),
|
||||
uint64(flag), uint64(funcPtr))
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
// ScalarFunction is the type of a scalar SQL function.
|
||||
// Implementations must not retain arg.
|
||||
type ScalarFunction func(ctx Context, arg ...Value)
|
||||
|
||||
// CreateWindowFunction defines a new aggregate or aggregate window SQL function.
|
||||
// If fn returns a [WindowFunction], then an aggregate window function is created.
|
||||
// If fn returns an [io.Closer], it will be called to free resources.
|
||||
//
|
||||
// https://sqlite.org/c3ref/create_function.html
|
||||
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn func() AggregateFunction) error {
|
||||
defer c.arena.mark()()
|
||||
call := "sqlite3_create_aggregate_function_go"
|
||||
namePtr := c.arena.string(name)
|
||||
funcPtr := util.AddHandle(c.ctx, fn)
|
||||
if _, ok := fn().(WindowFunction); ok {
|
||||
call = "sqlite3_create_window_function_go"
|
||||
}
|
||||
r := c.call(call,
|
||||
uint64(c.handle), uint64(namePtr), uint64(nArg),
|
||||
uint64(flag), uint64(funcPtr))
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
// AggregateFunction is the interface an aggregate function should implement.
|
||||
//
|
||||
// https://sqlite.org/appfunc.html
|
||||
type AggregateFunction interface {
|
||||
// Step is invoked to add a row to the current window.
|
||||
// The function arguments, if any, corresponding to the row being added, are passed to Step.
|
||||
// Implementations must not retain arg.
|
||||
Step(ctx Context, arg ...Value)
|
||||
|
||||
// Value is invoked to return the current (or final) value of the aggregate.
|
||||
Value(ctx Context)
|
||||
}
|
||||
|
||||
// WindowFunction is the interface an aggregate window function should implement.
|
||||
//
|
||||
// https://sqlite.org/windowfunctions.html
|
||||
type WindowFunction interface {
|
||||
AggregateFunction
|
||||
|
||||
// Inverse is invoked to remove the oldest presently aggregated result of Step from the current window.
|
||||
// The function arguments, if any, are those passed to Step for the row being removed.
|
||||
// Implementations must not retain arg.
|
||||
Inverse(ctx Context, arg ...Value)
|
||||
}
|
||||
|
||||
// OverloadFunction overloads a function for a virtual table.
|
||||
//
|
||||
// https://sqlite.org/c3ref/overload_function.html
|
||||
func (c *Conn) OverloadFunction(name string, nArg int) error {
|
||||
defer c.arena.mark()()
|
||||
namePtr := c.arena.string(name)
|
||||
r := c.call("sqlite3_overload_function",
|
||||
uint64(c.handle), uint64(namePtr), uint64(nArg))
|
||||
return c.error(r)
|
||||
}
|
||||
|
||||
func destroyCallback(ctx context.Context, mod api.Module, pApp uint32) {
|
||||
util.DelHandle(ctx, pApp)
|
||||
}
|
||||
|
||||
func collationCallback(ctx context.Context, mod api.Module, pArg, pDB, eTextRep, zName uint32) {
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.collation != nil {
|
||||
name := util.ReadString(mod, zName, _MAX_NAME)
|
||||
c.collation(c, name)
|
||||
}
|
||||
}
|
||||
|
||||
func compareCallback(ctx context.Context, mod api.Module, pApp, nKey1, pKey1, nKey2, pKey2 uint32) uint32 {
|
||||
fn := util.GetHandle(ctx, pApp).(func(a, b []byte) int)
|
||||
return uint32(fn(util.View(mod, pKey1, uint64(nKey1)), util.View(mod, pKey2, uint64(nKey2))))
|
||||
}
|
||||
|
||||
func funcCallback(ctx context.Context, mod api.Module, pCtx, pApp, nArg, pArg uint32) {
|
||||
args := getFuncArgs()
|
||||
defer putFuncArgs(args)
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
fn := util.GetHandle(db.ctx, pApp).(ScalarFunction)
|
||||
callbackArgs(db, args[:nArg], pArg)
|
||||
fn(Context{db, pCtx}, args[:nArg]...)
|
||||
}
|
||||
|
||||
func stepCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp, nArg, pArg uint32) {
|
||||
args := getFuncArgs()
|
||||
defer putFuncArgs(args)
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
callbackArgs(db, args[:nArg], pArg)
|
||||
fn, _ := callbackAggregate(db, pAgg, pApp)
|
||||
fn.Step(Context{db, pCtx}, args[:nArg]...)
|
||||
}
|
||||
|
||||
func finalCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp uint32) {
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
fn, handle := callbackAggregate(db, pAgg, pApp)
|
||||
fn.Value(Context{db, pCtx})
|
||||
util.DelHandle(ctx, handle)
|
||||
}
|
||||
|
||||
func valueCallback(ctx context.Context, mod api.Module, pCtx, pAgg uint32) {
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
fn := util.GetHandle(db.ctx, pAgg).(AggregateFunction)
|
||||
fn.Value(Context{db, pCtx})
|
||||
}
|
||||
|
||||
func inverseCallback(ctx context.Context, mod api.Module, pCtx, pAgg, nArg, pArg uint32) {
|
||||
args := getFuncArgs()
|
||||
defer putFuncArgs(args)
|
||||
db := ctx.Value(connKey{}).(*Conn)
|
||||
callbackArgs(db, args[:nArg], pArg)
|
||||
fn := util.GetHandle(db.ctx, pAgg).(WindowFunction)
|
||||
fn.Inverse(Context{db, pCtx}, args[:nArg]...)
|
||||
}
|
||||
|
||||
func callbackAggregate(db *Conn, pAgg, pApp uint32) (AggregateFunction, uint32) {
|
||||
if pApp == 0 {
|
||||
handle := util.ReadUint32(db.mod, pAgg)
|
||||
return util.GetHandle(db.ctx, handle).(AggregateFunction), handle
|
||||
}
|
||||
|
||||
// We need to create the aggregate.
|
||||
fn := util.GetHandle(db.ctx, pApp).(func() AggregateFunction)()
|
||||
handle := util.AddHandle(db.ctx, fn)
|
||||
if pAgg != 0 {
|
||||
util.WriteUint32(db.mod, pAgg, handle)
|
||||
}
|
||||
return fn, handle
|
||||
}
|
||||
|
||||
func callbackArgs(db *Conn, arg []Value, pArg uint32) {
|
||||
for i := range arg {
|
||||
arg[i] = Value{
|
||||
c: db,
|
||||
handle: util.ReadUint32(db.mod, pArg+ptrlen*uint32(i)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var funcArgsPool sync.Pool
|
||||
|
||||
func putFuncArgs(p *[_MAX_FUNCTION_ARG]Value) {
|
||||
funcArgsPool.Put(p)
|
||||
}
|
||||
|
||||
func getFuncArgs() *[_MAX_FUNCTION_ARG]Value {
|
||||
if p := funcArgsPool.Get(); p == nil {
|
||||
return new([_MAX_FUNCTION_ARG]Value)
|
||||
} else {
|
||||
return p.(*[_MAX_FUNCTION_ARG]Value)
|
||||
}
|
||||
}
|
||||
157
func_test.go
Normal file
157
func_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package sqlite3_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/ext/unicode"
|
||||
)
|
||||
|
||||
func ExampleConn_CreateCollation() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO words (word) VALUES ('côte'), ('cote'), ('coter'), ('coté'), ('cotée'), ('côté')`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.CollationNeeded(func(db *sqlite3.Conn, name string) {
|
||||
err := unicode.RegisterCollation(db, name, name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT word FROM words ORDER BY word COLLATE fr_FR`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for stmt.Step() {
|
||||
fmt.Println(stmt.ColumnText(0))
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// cote
|
||||
// coté
|
||||
// côte
|
||||
// côté
|
||||
// cotée
|
||||
// coter
|
||||
}
|
||||
|
||||
func ExampleConn_CreateFunction() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO words (word) VALUES ('côte'), ('cote'), ('coter'), ('coté'), ('cotée'), ('côté')`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.CreateFunction("upper", 1, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS, func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultRawText(bytes.ToUpper(arg[0].RawText()))
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT upper(word) FROM words`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for stmt.Step() {
|
||||
fmt.Println(stmt.ColumnText(0))
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Unordered output:
|
||||
// COTE
|
||||
// COTÉ
|
||||
// CÔTE
|
||||
// CÔTÉ
|
||||
// COTÉE
|
||||
// COTER
|
||||
}
|
||||
|
||||
func ExampleContext_SetAuxData() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO words (word) VALUES ('côte'), ('cote'), ('coter'), ('coté'), ('cotée'), ('côté')`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.CreateFunction("regexp", 2, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS, func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
re, ok := ctx.GetAuxData(0).(*regexp.Regexp)
|
||||
if !ok {
|
||||
r, err := regexp.Compile(arg[0].Text())
|
||||
if err != nil {
|
||||
ctx.ResultError(err)
|
||||
return
|
||||
}
|
||||
ctx.SetAuxData(0, r)
|
||||
re = r
|
||||
}
|
||||
ctx.ResultBool(re.Match(arg[1].RawText()))
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT word FROM words WHERE word REGEXP '^\p{L}+e$'`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for stmt.Step() {
|
||||
fmt.Println(stmt.ColumnText(0))
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Unordered output:
|
||||
// cote
|
||||
// côte
|
||||
// cotée
|
||||
}
|
||||
87
func_win_test.go
Normal file
87
func_win_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package sqlite3_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"unicode"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
func ExampleConn_CreateWindowFunction() {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO words (word) VALUES ('côte'), ('cote'), ('coter'), ('coté'), ('cotée'), ('côté')`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.CreateWindowFunction("count_ascii", 1, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS, newASCIICounter)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT count_ascii(word) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM words`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for stmt.Step() {
|
||||
fmt.Println(stmt.ColumnInt(0))
|
||||
}
|
||||
if err := stmt.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Output:
|
||||
// 1
|
||||
// 2
|
||||
// 2
|
||||
// 1
|
||||
// 0
|
||||
// 0
|
||||
}
|
||||
|
||||
type countASCII struct{ result int }
|
||||
|
||||
func newASCIICounter() sqlite3.AggregateFunction {
|
||||
return &countASCII{}
|
||||
}
|
||||
|
||||
func (f *countASCII) Value(ctx sqlite3.Context) {
|
||||
ctx.ResultInt(f.result)
|
||||
}
|
||||
|
||||
func (f *countASCII) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if f.isASCII(arg[0]) {
|
||||
f.result++
|
||||
}
|
||||
}
|
||||
|
||||
func (f *countASCII) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
if f.isASCII(arg[0]) {
|
||||
f.result--
|
||||
}
|
||||
}
|
||||
|
||||
func (f *countASCII) isASCII(arg sqlite3.Value) bool {
|
||||
if arg.Type() != sqlite3.TEXT {
|
||||
return false
|
||||
}
|
||||
for _, c := range arg.RawBlob() {
|
||||
if c > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
13
go.mod
13
go.mod
@@ -1,12 +1,15 @@
|
||||
module github.com/ncruces/go-sqlite3
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/ncruces/julianday v0.1.5
|
||||
github.com/tetratelabs/wazero v1.1.0
|
||||
golang.org/x/sync v0.2.0
|
||||
golang.org/x/sys v0.8.0
|
||||
github.com/ncruces/julianday v1.0.0
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/tetratelabs/wazero v1.7.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/text v0.14.0
|
||||
)
|
||||
|
||||
retract v0.4.0 // tagged from the wrong branch
|
||||
|
||||
22
go.sum
22
go.sum
@@ -1,8 +1,14 @@
|
||||
github.com/ncruces/julianday v0.1.5 h1:hDJ9ejiMp3DHsoZ5KW4c1lwfMjbARS7u/gbYcd0FBZk=
|
||||
github.com/ncruces/julianday v0.1.5/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/tetratelabs/wazero v1.1.0 h1:EByoAhC+QcYpwSZJSs/aV0uokxPwBgKxfiokSUwAknQ=
|
||||
github.com/tetratelabs/wazero v1.1.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
|
||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||
github.com/tetratelabs/wazero v1.7.0 h1:jg5qPydno59wqjpGrHph81lbtHzTrWzwwtD4cD88+hQ=
|
||||
github.com/tetratelabs/wazero v1.7.0/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.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=
|
||||
|
||||
4
go.work.sum
Normal file
4
go.work.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
22
gormlite/LICENSE
Normal file
22
gormlite/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Nuno Cruces
|
||||
Copyright (c) 2023 Jinzhu <wosmvp@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
17
gormlite/README.md
Normal file
17
gormlite/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# GORM SQLite Driver
|
||||
|
||||
[](https://pkg.go.dev/github.com/ncruces/go-sqlite3/gormlite)
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/gormlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
db, err := gorm.Open(gormlite.Open("gorm.db"), &gorm.Config{})
|
||||
```
|
||||
|
||||
Checkout [https://gorm.io](https://gorm.io) for details.
|
||||
290
gormlite/ddlmod.go
Normal file
290
gormlite/ddlmod.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm/migrator"
|
||||
)
|
||||
|
||||
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))
|
||||
columnsRegexp = regexp.MustCompile(fmt.Sprintf(`[(,][%v]?(\w+)[%v]?`, sqliteSeparator, sqliteSeparator))
|
||||
columnRegexp = regexp.MustCompile(fmt.Sprintf(`^[%v]?([\w\d]+)[%v]?\s+([\w\(\)\d]+)(.*)$`, sqliteSeparator, sqliteSeparator))
|
||||
defaultValueRegexp = regexp.MustCompile(`(?i) DEFAULT \(?(.+)?\)?( |COLLATE|GENERATED|$)`)
|
||||
regRealDataType = regexp.MustCompile(`[^\d](\d+)[^\d]?`)
|
||||
)
|
||||
|
||||
func getAllColumns(s string) []string {
|
||||
allMatches := columnsRegexp.FindAllStringSubmatch(s, -1)
|
||||
columns := make([]string, 0, len(allMatches))
|
||||
for _, matches := range allMatches {
|
||||
if len(matches) > 1 {
|
||||
columns = append(columns, matches[1])
|
||||
}
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
type ddl struct {
|
||||
head string
|
||||
fields []string
|
||||
columns []migrator.ColumnType
|
||||
}
|
||||
|
||||
func parseDDL(strs ...string) (*ddl, error) {
|
||||
var result ddl
|
||||
for _, str := range strs {
|
||||
if sections := tableRegexp.FindStringSubmatch(str); len(sections) > 0 {
|
||||
var (
|
||||
ddlBody = sections[2]
|
||||
ddlBodyRunes = []rune(ddlBody)
|
||||
bracketLevel int
|
||||
quote rune
|
||||
buf string
|
||||
)
|
||||
ddlBodyRunesLen := len(ddlBodyRunes)
|
||||
|
||||
result.head = sections[1]
|
||||
|
||||
for idx := 0; idx < ddlBodyRunesLen; idx++ {
|
||||
var (
|
||||
next rune = 0
|
||||
c = ddlBodyRunes[idx]
|
||||
)
|
||||
if idx+1 < ddlBodyRunesLen {
|
||||
next = ddlBodyRunes[idx+1]
|
||||
}
|
||||
|
||||
if sc := string(c); separatorRegexp.MatchString(sc) {
|
||||
if c == next {
|
||||
buf += sc // Skip escaped quote
|
||||
idx++
|
||||
} else if quote > 0 {
|
||||
quote = 0
|
||||
} else {
|
||||
quote = c
|
||||
}
|
||||
} else if quote == 0 {
|
||||
if c == '(' {
|
||||
bracketLevel++
|
||||
} else if c == ')' {
|
||||
bracketLevel--
|
||||
} else if bracketLevel == 0 {
|
||||
if c == ',' {
|
||||
result.fields = append(result.fields, strings.TrimSpace(buf))
|
||||
buf = ""
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bracketLevel < 0 {
|
||||
return nil, errors.New("invalid DDL, unbalanced brackets")
|
||||
}
|
||||
|
||||
buf += string(c)
|
||||
}
|
||||
|
||||
if bracketLevel != 0 {
|
||||
return nil, errors.New("invalid DDL, unbalanced brackets")
|
||||
}
|
||||
|
||||
if buf != "" {
|
||||
result.fields = append(result.fields, strings.TrimSpace(buf))
|
||||
}
|
||||
|
||||
for _, f := range result.fields {
|
||||
fUpper := strings.ToUpper(f)
|
||||
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 {
|
||||
if column.NameValue.String == name {
|
||||
column.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true}
|
||||
result.columns[idx] = column
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if matches := columnRegexp.FindStringSubmatch(f); len(matches) > 0 {
|
||||
columnType := migrator.ColumnType{
|
||||
NameValue: sql.NullString{String: matches[1], Valid: true},
|
||||
DataTypeValue: sql.NullString{String: matches[2], Valid: true},
|
||||
ColumnTypeValue: sql.NullString{String: matches[2], Valid: true},
|
||||
PrimaryKeyValue: sql.NullBool{Valid: true},
|
||||
UniqueValue: sql.NullBool{Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: true, Valid: true},
|
||||
DefaultValueValue: sql.NullString{Valid: false},
|
||||
}
|
||||
|
||||
matchUpper := strings.ToUpper(matches[3])
|
||||
if strings.Contains(matchUpper, " NOT NULL") {
|
||||
columnType.NullableValue = sql.NullBool{Bool: false, Valid: true}
|
||||
} else if strings.Contains(matchUpper, " NULL") {
|
||||
columnType.NullableValue = sql.NullBool{Bool: true, Valid: true}
|
||||
}
|
||||
if strings.Contains(matchUpper, " UNIQUE") {
|
||||
columnType.UniqueValue = sql.NullBool{Bool: true, Valid: true}
|
||||
}
|
||||
if strings.Contains(matchUpper, " PRIMARY") {
|
||||
columnType.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true}
|
||||
}
|
||||
if defaultMatches := defaultValueRegexp.FindStringSubmatch(matches[3]); len(defaultMatches) > 1 {
|
||||
if strings.ToLower(defaultMatches[1]) != "null" {
|
||||
columnType.DefaultValueValue = sql.NullString{String: strings.Trim(defaultMatches[1], `"`), Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
// data type length
|
||||
matches := regRealDataType.FindAllStringSubmatch(columnType.DataTypeValue.String, -1)
|
||||
if len(matches) == 1 && len(matches[0]) == 2 {
|
||||
size, _ := strconv.Atoi(matches[0][1])
|
||||
columnType.LengthValue = sql.NullInt64{Valid: true, Int64: int64(size)}
|
||||
columnType.DataTypeValue.String = strings.TrimSuffix(columnType.DataTypeValue.String, matches[0][0])
|
||||
}
|
||||
|
||||
result.columns = append(result.columns, columnType)
|
||||
}
|
||||
}
|
||||
} else if matches := indexRegexp.FindStringSubmatch(str); len(matches) > 0 {
|
||||
// don't report Unique by UniqueIndex
|
||||
} else {
|
||||
return nil, errors.New("invalid DDL")
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (d *ddl) clone() *ddl {
|
||||
copied := new(ddl)
|
||||
*copied = *d
|
||||
|
||||
copied.fields = make([]string, len(d.fields))
|
||||
copy(copied.fields, d.fields)
|
||||
copied.columns = make([]migrator.ColumnType, len(d.columns))
|
||||
copy(copied.columns, d.columns)
|
||||
|
||||
return copied
|
||||
}
|
||||
|
||||
func (d *ddl) compile() string {
|
||||
if len(d.fields) == 0 {
|
||||
return d.head
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s (%s)", d.head, strings.Join(d.fields, ","))
|
||||
}
|
||||
|
||||
func (d *ddl) renameTable(dst, src string) error {
|
||||
tableReg, err := regexp.Compile("\\s*('|`|\")?\\b" + regexp.QuoteMeta(src) + "\\b('|`|\")?\\s*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
replaced := tableReg.ReplaceAllString(d.head, fmt.Sprintf(" `%s` ", dst))
|
||||
if replaced == d.head {
|
||||
return fmt.Errorf("failed to look up tablename `%s` from DDL head '%s'", src, d.head)
|
||||
}
|
||||
|
||||
d.head = replaced
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *ddl) addConstraint(name string, sql string) {
|
||||
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
|
||||
|
||||
for i := 0; i < len(d.fields); i++ {
|
||||
if reg.MatchString(d.fields[i]) {
|
||||
d.fields[i] = sql
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
d.fields = append(d.fields, sql)
|
||||
}
|
||||
|
||||
func (d *ddl) removeConstraint(name string) bool {
|
||||
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
|
||||
|
||||
for i := 0; i < len(d.fields); i++ {
|
||||
if reg.MatchString(d.fields[i]) {
|
||||
d.fields = append(d.fields[:i], d.fields[i+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//lint:ignore U1000 ignore unused code.
|
||||
func (d *ddl) hasConstraint(name string) bool {
|
||||
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
|
||||
|
||||
for _, f := range d.fields {
|
||||
if reg.MatchString(f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *ddl) getColumns() []string {
|
||||
res := []string{}
|
||||
|
||||
for _, f := range d.fields {
|
||||
fUpper := strings.ToUpper(f)
|
||||
if strings.HasPrefix(fUpper, "PRIMARY KEY") ||
|
||||
strings.HasPrefix(fUpper, "CHECK") ||
|
||||
strings.HasPrefix(fUpper, "CONSTRAINT") ||
|
||||
strings.Contains(fUpper, "GENERATED ALWAYS AS") {
|
||||
continue
|
||||
}
|
||||
|
||||
reg := regexp.MustCompile("^[\"`']?([\\w\\d]+)[\"`']?")
|
||||
match := reg.FindStringSubmatch(f)
|
||||
|
||||
if match != nil {
|
||||
res = append(res, "`"+match[1]+"`")
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *ddl) removeColumn(name string) bool {
|
||||
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
|
||||
|
||||
for i := 0; i < len(d.fields); i++ {
|
||||
if reg.MatchString(d.fields[i]) {
|
||||
d.fields = append(d.fields[:i], d.fields[i+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
397
gormlite/ddlmod_test.go
Normal file
397
gormlite/ddlmod_test.go
Normal file
@@ -0,0 +1,397 @@
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm/migrator"
|
||||
"gorm.io/gorm/utils/tests"
|
||||
)
|
||||
|
||||
func TestParseDDL(t *testing.T) {
|
||||
params := []struct {
|
||||
name string
|
||||
sql []string
|
||||
nFields int
|
||||
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 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: 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}},
|
||||
},
|
||||
},
|
||||
{"with_check", []string{"CREATE TABLE Persons (ID int NOT NULL,LastName varchar(255) NOT NULL,FirstName varchar(255),Age int,CHECK (Age>=18),CHECK (FirstName<>'John'))"}, 6, []migrator.ColumnType{
|
||||
{NameValue: sql.NullString{String: "ID", Valid: true}, DataTypeValue: sql.NullString{String: "int", Valid: true}, ColumnTypeValue: sql.NullString{String: "int", Valid: true}, NullableValue: sql.NullBool{Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
|
||||
{NameValue: sql.NullString{String: "LastName", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 255, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(255)", Valid: true}, NullableValue: sql.NullBool{Bool: false, Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
|
||||
{NameValue: sql.NullString{String: "FirstName", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 255, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(255)", Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
|
||||
{NameValue: sql.NullString{String: "Age", Valid: true}, DataTypeValue: sql.NullString{String: "int", Valid: true}, ColumnTypeValue: sql.NullString{String: "int", Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
|
||||
}},
|
||||
{"lowercase", []string{"create table test (ID int NOT NULL)"}, 1, []migrator.ColumnType{
|
||||
{NameValue: sql.NullString{String: "ID", Valid: true}, DataTypeValue: sql.NullString{String: "int", Valid: true}, ColumnTypeValue: sql.NullString{String: "int", Valid: true}, NullableValue: sql.NullBool{Bool: false, Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
|
||||
},
|
||||
},
|
||||
{"no brackets", []string{"create table test"}, 0, nil},
|
||||
{"with_special_characters", []string{
|
||||
"CREATE TABLE `test` (`text` varchar(10) DEFAULT \"测试, \")",
|
||||
}, 1, []migrator.ColumnType{
|
||||
{NameValue: sql.NullString{String: "text", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 10, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(10)", Valid: true}, DefaultValueValue: sql.NullString{String: "测试, ", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"table_name_with_dash",
|
||||
[]string{
|
||||
"CREATE TABLE `test-a` (`id` int NOT NULL)",
|
||||
"CREATE UNIQUE INDEX `idx_test-a_id` ON `test-a`(`id`)",
|
||||
},
|
||||
1,
|
||||
[]migrator.ColumnType{
|
||||
{
|
||||
NameValue: sql.NullString{String: "id", Valid: true},
|
||||
DataTypeValue: sql.NullString{String: "int", Valid: true},
|
||||
ColumnTypeValue: sql.NullString{String: "int", Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: false, Valid: true},
|
||||
DefaultValueValue: sql.NullString{Valid: false},
|
||||
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: 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",
|
||||
[]string{
|
||||
"CREATE TABLE `test-c` (`field` integer NOT NULL)",
|
||||
"CREATE 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: false, Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: false, Valid: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"index with \n from .schema sqlite",
|
||||
[]string{
|
||||
"CREATE TABLE `test-d` (`field` integer NOT NULL)",
|
||||
"CREATE INDEX `idx_uq`\n 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: false, Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: false, Valid: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range params {
|
||||
t.Run(p.name, func(t *testing.T) {
|
||||
ddl, err := parseDDL(p.sql...)
|
||||
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
tests.AssertEqual(t, p.sql[0], ddl.compile())
|
||||
if len(ddl.fields) != p.nFields {
|
||||
t.Fatalf("fields length doesn't match: expect: %v, got %v", p.nFields, len(ddl.fields))
|
||||
}
|
||||
tests.AssertEqual(t, ddl.columns, p.columns)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDDL_Whitespaces(t *testing.T) {
|
||||
testColumns := []migrator.ColumnType{
|
||||
{
|
||||
NameValue: sql.NullString{String: "id", Valid: true},
|
||||
DataTypeValue: sql.NullString{String: "integer", Valid: true},
|
||||
ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: true, Valid: true},
|
||||
DefaultValueValue: sql.NullString{Valid: false},
|
||||
UniqueValue: sql.NullBool{Bool: true, Valid: true},
|
||||
PrimaryKeyValue: sql.NullBool{Bool: true, Valid: true},
|
||||
},
|
||||
{
|
||||
NameValue: sql.NullString{String: "dark_mode", Valid: true},
|
||||
DataTypeValue: sql.NullString{String: "numeric", Valid: true},
|
||||
ColumnTypeValue: sql.NullString{String: "numeric", Valid: true},
|
||||
NullableValue: sql.NullBool{Bool: true, Valid: true},
|
||||
DefaultValueValue: sql.NullString{String: "true", Valid: true},
|
||||
UniqueValue: sql.NullBool{Bool: false, Valid: true},
|
||||
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
|
||||
},
|
||||
}
|
||||
|
||||
params := []struct {
|
||||
name string
|
||||
sql []string
|
||||
nFields int
|
||||
columns []migrator.ColumnType
|
||||
}{
|
||||
{
|
||||
"with_newline",
|
||||
[]string{"CREATE TABLE `users`\n(\nid integer primary key unique,\ndark_mode numeric DEFAULT true)"},
|
||||
2,
|
||||
testColumns,
|
||||
},
|
||||
{
|
||||
"with_newline_2",
|
||||
[]string{"CREATE TABLE `users` (\n\nid integer primary key unique,\ndark_mode numeric DEFAULT true)"},
|
||||
2,
|
||||
testColumns,
|
||||
},
|
||||
{
|
||||
"with_missing_space",
|
||||
[]string{"CREATE TABLE `users`(id integer primary key unique, dark_mode numeric DEFAULT true)"},
|
||||
2,
|
||||
testColumns,
|
||||
},
|
||||
{
|
||||
"with_many_spaces",
|
||||
[]string{"CREATE TABLE `users` (id integer primary key unique, dark_mode numeric DEFAULT true)"},
|
||||
2,
|
||||
testColumns,
|
||||
},
|
||||
}
|
||||
for _, p := range params {
|
||||
t.Run(p.name, func(t *testing.T) {
|
||||
ddl, err := parseDDL(p.sql...)
|
||||
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
if len(ddl.fields) != p.nFields {
|
||||
t.Fatalf("fields length doesn't match: expect: %v, got %v", p.nFields, len(ddl.fields))
|
||||
}
|
||||
tests.AssertEqual(t, ddl.columns, p.columns)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDDL_error(t *testing.T) {
|
||||
params := []struct {
|
||||
name string
|
||||
sql string
|
||||
}{
|
||||
{"invalid_cmd", "CREATE TABLE"},
|
||||
{"unbalanced_brackets", "CREATE TABLE test (ID int NOT NULL,Name varchar(255)"},
|
||||
{"unbalanced_brackets2", "CREATE TABLE test (ID int NOT NULL,Name varchar(255)))"},
|
||||
}
|
||||
|
||||
for _, p := range params {
|
||||
t.Run(p.name, func(t *testing.T) {
|
||||
_, err := parseDDL(p.sql)
|
||||
if err == nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddConstraint(t *testing.T) {
|
||||
params := []struct {
|
||||
name string
|
||||
fields []string
|
||||
cName string
|
||||
sql string
|
||||
expect []string
|
||||
}{
|
||||
{
|
||||
name: "add_new",
|
||||
fields: []string{"`id` integer NOT NULL"},
|
||||
cName: "fk_users_notes",
|
||||
sql: "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))",
|
||||
expect: []string{"`id` integer NOT NULL", "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
fields: []string{"`id` integer NOT NULL", "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
|
||||
cName: "fk_users_notes",
|
||||
sql: "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)) ON UPDATE CASCADE ON DELETE CASCADE",
|
||||
expect: []string{"`id` integer NOT NULL", "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)) ON UPDATE CASCADE ON DELETE CASCADE"},
|
||||
},
|
||||
{
|
||||
name: "add_check",
|
||||
fields: []string{"`id` integer NOT NULL"},
|
||||
cName: "name_checker",
|
||||
sql: "CONSTRAINT `name_checker` CHECK (`name` <> 'jinzhu')",
|
||||
expect: []string{"`id` integer NOT NULL", "CONSTRAINT `name_checker` CHECK (`name` <> 'jinzhu')"},
|
||||
},
|
||||
{
|
||||
name: "update_check",
|
||||
fields: []string{"`id` integer NOT NULL", "CONSTRAINT `name_checker` CHECK (`name` <> 'thetadev')"},
|
||||
cName: "name_checker",
|
||||
sql: "CONSTRAINT `name_checker` CHECK (`name` <> 'jinzhu')",
|
||||
expect: []string{"`id` integer NOT NULL", "CONSTRAINT `name_checker` CHECK (`name` <> 'jinzhu')"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range params {
|
||||
t.Run(p.name, func(t *testing.T) {
|
||||
testDDL := ddl{fields: p.fields}
|
||||
|
||||
testDDL.addConstraint(p.cName, p.sql)
|
||||
tests.AssertEqual(t, p.expect, testDDL.fields)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveConstraint(t *testing.T) {
|
||||
params := []struct {
|
||||
name string
|
||||
fields []string
|
||||
cName string
|
||||
success bool
|
||||
expect []string
|
||||
}{
|
||||
{
|
||||
name: "fk",
|
||||
fields: []string{"`id` integer NOT NULL", "CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
|
||||
cName: "fk_users_notes",
|
||||
success: true,
|
||||
expect: []string{"`id` integer NOT NULL"},
|
||||
},
|
||||
{
|
||||
name: "check",
|
||||
fields: []string{"CONSTRAINT `name_checker` CHECK (`name` <> 'thetadev')", "`id` integer NOT NULL"},
|
||||
cName: "name_checker",
|
||||
success: true,
|
||||
expect: []string{"`id` integer NOT NULL"},
|
||||
},
|
||||
{
|
||||
name: "none",
|
||||
fields: []string{"CONSTRAINT `name_checker` CHECK (`name` <> 'thetadev')", "`id` integer NOT NULL"},
|
||||
cName: "nothing",
|
||||
success: false,
|
||||
expect: []string{"CONSTRAINT `name_checker` CHECK (`name` <> 'thetadev')", "`id` integer NOT NULL"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range params {
|
||||
t.Run(p.name, func(t *testing.T) {
|
||||
testDDL := ddl{fields: p.fields}
|
||||
|
||||
success := testDDL.removeConstraint(p.cName)
|
||||
|
||||
tests.AssertEqual(t, p.success, success)
|
||||
tests.AssertEqual(t, p.expect, testDDL.fields)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetColumns(t *testing.T) {
|
||||
params := []struct {
|
||||
name string
|
||||
ddl string
|
||||
columns []string
|
||||
}{
|
||||
{
|
||||
name: "with_fk",
|
||||
ddl: "CREATE TABLE `notes` (`id` integer NOT NULL,`text` varchar(500),`user_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))",
|
||||
columns: []string{"`id`", "`text`", "`user_id`"},
|
||||
},
|
||||
{
|
||||
name: "with_check",
|
||||
ddl: "CREATE TABLE Persons (ID int NOT NULL,LastName varchar(255) NOT NULL,FirstName varchar(255),Age int,CHECK (Age>=18),CHECK (FirstName!='John'))",
|
||||
columns: []string{"`ID`", "`LastName`", "`FirstName`", "`Age`"},
|
||||
},
|
||||
{
|
||||
name: "with_escaped_quote",
|
||||
ddl: "CREATE TABLE Persons (ID int NOT NULL,LastName varchar(255) NOT NULL DEFAULT \"\",FirstName varchar(255))",
|
||||
columns: []string{"`ID`", "`LastName`", "`FirstName`"},
|
||||
},
|
||||
{
|
||||
name: "with_generated_column",
|
||||
ddl: "CREATE TABLE Persons (ID int NOT NULL,LastName varchar(255) NOT NULL,FirstName varchar(255),FullName varchar(255) GENERATED ALWAYS AS (FirstName || ' ' || LastName))",
|
||||
columns: []string{"`ID`", "`LastName`", "`FirstName`"},
|
||||
},
|
||||
{
|
||||
name: "with_new_line",
|
||||
ddl: `CREATE TABLE "tb_sys_role_menu__temp" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT,
|
||||
"created_at" datetime NOT NULL,
|
||||
"updated_at" datetime NOT NULL,
|
||||
"created_by" integer NOT NULL DEFAULT 0,
|
||||
"updated_by" integer NOT NULL DEFAULT 0,
|
||||
"role_id" integer NOT NULL,
|
||||
"menu_id" bigint NOT NULL
|
||||
)`,
|
||||
columns: []string{"`id`", "`created_at`", "`updated_at`", "`created_by`", "`updated_by`", "`role_id`", "`menu_id`"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range params {
|
||||
t.Run(p.name, func(t *testing.T) {
|
||||
testDDL, err := parseDDL(p.ddl)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
cols := testDDL.getColumns()
|
||||
|
||||
tests.AssertEqual(t, p.columns, cols)
|
||||
})
|
||||
}
|
||||
}
|
||||
11
gormlite/download.sh
Executable file
11
gormlite/download.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/ddlmod.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/ddlmod_test.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/error_translator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/migrator.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/sqlite.go"
|
||||
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.5/sqlite_test.go"
|
||||
21
gormlite/error_translator.go
Normal file
21
gormlite/error_translator.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (_Dialector) Translate(err error) error {
|
||||
switch {
|
||||
case
|
||||
errors.Is(err, sqlite3.CONSTRAINT_UNIQUE),
|
||||
errors.Is(err, sqlite3.CONSTRAINT_PRIMARYKEY):
|
||||
return gorm.ErrDuplicatedKey
|
||||
case
|
||||
errors.Is(err, sqlite3.CONSTRAINT_FOREIGNKEY):
|
||||
return gorm.ErrForeignKeyViolated
|
||||
}
|
||||
return err
|
||||
}
|
||||
16
gormlite/go.mod
Normal file
16
gormlite/go.mod
Normal file
@@ -0,0 +1,16 @@
|
||||
module github.com/ncruces/go-sqlite3/gormlite
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/ncruces/go-sqlite3 v0.13.0
|
||||
gorm.io/gorm v1.25.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
)
|
||||
16
gormlite/go.sum
Normal file
16
gormlite/go.sum
Normal file
@@ -0,0 +1,16 @@
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
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.13.0 h1:+N1VHVLnrCJasyXdAKQL9MNTKC3wHZa8pLMUuP8wb3k=
|
||||
github.com/ncruces/go-sqlite3 v0.13.0/go.mod h1:y9zPUI+C42V9xuuJeN+tGhpOjr4gUHz2Pi2RLFVEdZg=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/tetratelabs/wazero v1.7.0 h1:jg5qPydno59wqjpGrHph81lbtHzTrWzwwtD4cD88+hQ=
|
||||
github.com/tetratelabs/wazero v1.7.0/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.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.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
430
gormlite/migrator.go
Normal file
430
gormlite/migrator.go
Normal file
@@ -0,0 +1,430 @@
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/migrator"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
type _Migrator struct {
|
||||
migrator.Migrator
|
||||
}
|
||||
|
||||
func (m *_Migrator) RunWithoutForeignKey(fc func() error) error {
|
||||
var enabled int
|
||||
m.DB.Raw("PRAGMA foreign_keys").Scan(&enabled)
|
||||
if enabled == 1 {
|
||||
m.DB.Exec("PRAGMA foreign_keys = OFF")
|
||||
defer m.DB.Exec("PRAGMA foreign_keys = ON")
|
||||
}
|
||||
|
||||
return fc()
|
||||
}
|
||||
|
||||
func (m _Migrator) HasTable(value interface{}) bool {
|
||||
var count int
|
||||
m.Migrator.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
return m.DB.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", stmt.Table).Row().Scan(&count)
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m _Migrator) DropTable(values ...interface{}) error {
|
||||
return m.RunWithoutForeignKey(func() error {
|
||||
values = m.ReorderModels(values, false)
|
||||
tx := m.DB.Session(&gorm.Session{})
|
||||
|
||||
for i := len(values) - 1; i >= 0; i-- {
|
||||
if err := m.RunWithValue(values[i], func(stmt *gorm.Statement) error {
|
||||
return tx.Exec("DROP TABLE IF EXISTS ?", clause.Table{Name: stmt.Table}).Error
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m _Migrator) GetTables() (tableList []string, err error) {
|
||||
return tableList, m.DB.Raw("SELECT name FROM sqlite_master where type=?", "table").Scan(&tableList).Error
|
||||
}
|
||||
|
||||
func (m _Migrator) HasColumn(value interface{}, name string) bool {
|
||||
var count int
|
||||
m.Migrator.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if field := stmt.Schema.LookUpField(name); field != nil {
|
||||
name = field.DBName
|
||||
}
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
m.DB.Raw(
|
||||
"SELECT count(*) FROM sqlite_master WHERE type = ? AND tbl_name = ? AND (sql LIKE ? OR sql LIKE ? OR sql LIKE ? OR sql LIKE ? OR sql LIKE ?)",
|
||||
"table", stmt.Table, `%"`+name+`" %`, `%`+name+` %`, "%`"+name+"`%", "%["+name+"]%", "%\t"+name+"\t%",
|
||||
).Row().Scan(&count)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
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 {
|
||||
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, sqlArgs, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to alter field with name %v", name)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ColumnTypes return columnTypes []gorm.ColumnType and execErr error
|
||||
func (m _Migrator) ColumnTypes(value interface{}) ([]gorm.ColumnType, error) {
|
||||
columnTypes := make([]gorm.ColumnType, 0)
|
||||
execErr := m.RunWithValue(value, func(stmt *gorm.Statement) (err error) {
|
||||
var (
|
||||
sqls []string
|
||||
sqlDDL *ddl
|
||||
)
|
||||
|
||||
if err := m.DB.Raw("SELECT sql FROM sqlite_master WHERE type IN ? AND tbl_name = ? AND sql IS NOT NULL order by type = ? desc", []string{"table", "index"}, stmt.Table, "table").Scan(&sqls).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sqlDDL, err = parseDDL(sqls...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := m.DB.Session(&gorm.Session{}).Table(stmt.Table).Limit(1).Rows()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = rows.Close()
|
||||
}()
|
||||
|
||||
var rawColumnTypes []*sql.ColumnType
|
||||
rawColumnTypes, err = rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range rawColumnTypes {
|
||||
columnType := migrator.ColumnType{SQLColumnType: c}
|
||||
for _, column := range sqlDDL.columns {
|
||||
if column.NameValue.String == c.Name() {
|
||||
column.SQLColumnType = c
|
||||
columnType = column
|
||||
break
|
||||
}
|
||||
}
|
||||
columnTypes = append(columnTypes, columnType)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return columnTypes, execErr
|
||||
}
|
||||
|
||||
func (m _Migrator) DropColumn(value interface{}, name string) error {
|
||||
return m.recreateTable(value, nil, func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
|
||||
if field := stmt.Schema.LookUpField(name); field != nil {
|
||||
name = field.DBName
|
||||
}
|
||||
|
||||
ddl.removeColumn(name)
|
||||
return ddl, nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m _Migrator) CreateConstraint(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
|
||||
return m.recreateTable(value, &table,
|
||||
func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
|
||||
var (
|
||||
constraintName string
|
||||
constraintSql string
|
||||
constraintValues []interface{}
|
||||
)
|
||||
|
||||
if constraint != nil {
|
||||
constraintName = constraint.GetName()
|
||||
constraintSql, constraintValues = constraint.Build()
|
||||
} else {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
ddl.addConstraint(constraintName, constraintSql)
|
||||
return ddl, constraintValues, nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (m _Migrator) DropConstraint(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
if constraint != nil {
|
||||
name = constraint.GetName()
|
||||
}
|
||||
|
||||
return m.recreateTable(value, &table,
|
||||
func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
|
||||
ddl.removeConstraint(name)
|
||||
return ddl, nil, nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (m _Migrator) HasConstraint(value interface{}, name string) bool {
|
||||
var count int64
|
||||
m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
|
||||
if constraint != nil {
|
||||
name = constraint.GetName()
|
||||
}
|
||||
|
||||
m.DB.Raw(
|
||||
"SELECT count(*) FROM sqlite_master WHERE type = ? AND tbl_name = ? AND (sql LIKE ? OR sql LIKE ? OR sql LIKE ? OR sql LIKE ? OR sql LIKE ?)",
|
||||
"table", table, `%CONSTRAINT "`+name+`" %`, `%CONSTRAINT `+name+` %`, "%CONSTRAINT `"+name+"`%", "%CONSTRAINT ["+name+"]%", "%CONSTRAINT \t"+name+"\t%",
|
||||
).Row().Scan(&count)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m _Migrator) CurrentDatabase() (name string) {
|
||||
var null interface{}
|
||||
m.DB.Raw("PRAGMA database_list").Row().Scan(&null, &name, &null)
|
||||
return
|
||||
}
|
||||
|
||||
func (m _Migrator) BuildIndexOptions(opts []schema.IndexOption, stmt *gorm.Statement) (results []interface{}) {
|
||||
for _, opt := range opts {
|
||||
str := stmt.Quote(opt.DBName)
|
||||
if opt.Expression != "" {
|
||||
str = opt.Expression
|
||||
}
|
||||
|
||||
if opt.Collate != "" {
|
||||
str += " COLLATE " + opt.Collate
|
||||
}
|
||||
|
||||
if opt.Sort != "" {
|
||||
str += " " + opt.Sort
|
||||
}
|
||||
results = append(results, clause.Expr{SQL: str})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m _Migrator) CreateIndex(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if idx := stmt.Schema.LookIndex(name); idx != nil {
|
||||
opts := m.BuildIndexOptions(idx.Fields, stmt)
|
||||
values := []interface{}{clause.Column{Name: idx.Name}, clause.Table{Name: stmt.Table}, opts}
|
||||
|
||||
createIndexSQL := "CREATE "
|
||||
if idx.Class != "" {
|
||||
createIndexSQL += idx.Class + " "
|
||||
}
|
||||
createIndexSQL += "INDEX ?"
|
||||
|
||||
if idx.Type != "" {
|
||||
createIndexSQL += " USING " + idx.Type
|
||||
}
|
||||
createIndexSQL += " ON ??"
|
||||
|
||||
if idx.Where != "" {
|
||||
createIndexSQL += " WHERE " + idx.Where
|
||||
}
|
||||
|
||||
return m.DB.Exec(createIndexSQL, values...).Error
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to create index with name %v", name)
|
||||
})
|
||||
}
|
||||
|
||||
func (m _Migrator) HasIndex(value interface{}, name string) bool {
|
||||
var count int
|
||||
m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if idx := stmt.Schema.LookIndex(name); idx != nil {
|
||||
name = idx.Name
|
||||
}
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
m.DB.Raw(
|
||||
"SELECT count(*) FROM sqlite_master WHERE type = ? AND tbl_name = ? AND name = ?", "index", stmt.Table, name,
|
||||
).Row().Scan(&count)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (m _Migrator) RenameIndex(value interface{}, oldName, newName string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
var sql string
|
||||
m.DB.Raw("SELECT sql FROM sqlite_master WHERE type = ? AND tbl_name = ? AND name = ?", "index", stmt.Table, oldName).Row().Scan(&sql)
|
||||
if sql != "" {
|
||||
if err := m.DropIndex(value, oldName); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.DB.Exec(strings.Replace(sql, oldName, newName, 1)).Error
|
||||
}
|
||||
return fmt.Errorf("failed to find index with name %v", oldName)
|
||||
})
|
||||
}
|
||||
|
||||
func (m _Migrator) DropIndex(value interface{}, name string) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
if stmt.Schema != nil {
|
||||
if idx := stmt.Schema.LookIndex(name); idx != nil {
|
||||
name = idx.Name
|
||||
}
|
||||
}
|
||||
|
||||
return m.DB.Exec("DROP INDEX ?", clause.Column{Name: name}).Error
|
||||
})
|
||||
}
|
||||
|
||||
type _Index struct {
|
||||
Seq int
|
||||
Name string
|
||||
Unique bool
|
||||
Origin string
|
||||
Partial bool
|
||||
}
|
||||
|
||||
// GetIndexes return Indexes []gorm.Index and execErr error,
|
||||
// See the [doc]
|
||||
//
|
||||
// [doc]: https://www.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) {
|
||||
var createSQL string
|
||||
m.DB.Raw("SELECT sql FROM sqlite_master WHERE type = ? AND tbl_name = ? AND name = ?", "table", table, table).Row().Scan(&createSQL)
|
||||
|
||||
if m.DB.Error != nil {
|
||||
return "", m.DB.Error
|
||||
}
|
||||
return createSQL, nil
|
||||
}
|
||||
|
||||
func (m _Migrator) recreateTable(
|
||||
value interface{}, tablePtr *string,
|
||||
getCreateSQL func(ddl *ddl, stmt *gorm.Statement) (sql *ddl, sqlArgs []interface{}, err error),
|
||||
) error {
|
||||
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
|
||||
table := stmt.Table
|
||||
if tablePtr != nil {
|
||||
table = *tablePtr
|
||||
}
|
||||
|
||||
rawDDL, err := m.getRawDDL(table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
originDDL, err := parseDDL(rawDDL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createDDL, sqlArgs, err := getCreateSQL(originDDL.clone(), stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if createDDL == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newTableName := table + "__temp"
|
||||
if err := createDDL.renameTable(newTableName, table); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
columns := createDDL.getColumns()
|
||||
createSQL := createDDL.compile()
|
||||
|
||||
return m.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Exec(createSQL, sqlArgs...).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queries := []string{
|
||||
fmt.Sprintf("INSERT INTO `%v`(%v) SELECT %v FROM `%v`", newTableName, strings.Join(columns, ","), strings.Join(columns, ","), table),
|
||||
fmt.Sprintf("DROP TABLE `%v`", table),
|
||||
fmt.Sprintf("ALTER TABLE `%v` RENAME TO `%v`", newTableName, table),
|
||||
}
|
||||
for _, query := range queries {
|
||||
if err := tx.Exec(query).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
221
gormlite/sqlite.go
Normal file
221
gormlite/sqlite.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// Package gormlite provides a GORM driver for SQLite.
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/callbacks"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/migrator"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
)
|
||||
|
||||
// Open opens a GORM dialector from a data source name.
|
||||
func Open(dsn string) gorm.Dialector {
|
||||
return &_Dialector{DSN: dsn}
|
||||
}
|
||||
|
||||
// Open opens a GORM dialector from a database handle.
|
||||
func OpenDB(db *sql.DB) gorm.Dialector {
|
||||
return &_Dialector{Conn: db}
|
||||
}
|
||||
|
||||
type _Dialector struct {
|
||||
DSN string
|
||||
Conn gorm.ConnPool
|
||||
}
|
||||
|
||||
func (dialector _Dialector) Name() string {
|
||||
return "sqlite"
|
||||
}
|
||||
|
||||
func (dialector _Dialector) Initialize(db *gorm.DB) (err error) {
|
||||
if dialector.Conn != nil {
|
||||
db.ConnPool = dialector.Conn
|
||||
} else {
|
||||
conn, err := driver.Open(dialector.DSN, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.ConnPool = conn
|
||||
}
|
||||
|
||||
callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{
|
||||
CreateClauses: []string{"INSERT", "VALUES", "ON CONFLICT", "RETURNING"},
|
||||
UpdateClauses: []string{"UPDATE", "SET", "FROM", "WHERE", "RETURNING"},
|
||||
DeleteClauses: []string{"DELETE", "FROM", "WHERE", "RETURNING"},
|
||||
LastInsertIDReversed: true,
|
||||
})
|
||||
|
||||
for k, v := range dialector.ClauseBuilders() {
|
||||
db.ClauseBuilders[k] = v
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (dialector _Dialector) ClauseBuilders() map[string]clause.ClauseBuilder {
|
||||
return map[string]clause.ClauseBuilder{
|
||||
"INSERT": func(c clause.Clause, builder clause.Builder) {
|
||||
if insert, ok := c.Expression.(clause.Insert); ok {
|
||||
if stmt, ok := builder.(*gorm.Statement); ok {
|
||||
stmt.WriteString("INSERT ")
|
||||
if insert.Modifier != "" {
|
||||
stmt.WriteString(insert.Modifier)
|
||||
stmt.WriteByte(' ')
|
||||
}
|
||||
|
||||
stmt.WriteString("INTO ")
|
||||
if insert.Table.Name == "" {
|
||||
stmt.WriteQuoted(stmt.Table)
|
||||
} else {
|
||||
stmt.WriteQuoted(insert.Table)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Build(builder)
|
||||
},
|
||||
"LIMIT": func(c clause.Clause, builder clause.Builder) {
|
||||
if limit, ok := c.Expression.(clause.Limit); ok {
|
||||
var lmt = -1
|
||||
if limit.Limit != nil && *limit.Limit >= 0 {
|
||||
lmt = *limit.Limit
|
||||
}
|
||||
if lmt >= 0 || limit.Offset > 0 {
|
||||
builder.WriteString("LIMIT ")
|
||||
builder.WriteString(strconv.Itoa(lmt))
|
||||
}
|
||||
if limit.Offset > 0 {
|
||||
builder.WriteString(" OFFSET ")
|
||||
builder.WriteString(strconv.Itoa(limit.Offset))
|
||||
}
|
||||
}
|
||||
},
|
||||
"FOR": func(c clause.Clause, builder clause.Builder) {
|
||||
if _, ok := c.Expression.(clause.Locking); ok {
|
||||
// SQLite3 does not support row-level locking.
|
||||
return
|
||||
}
|
||||
c.Build(builder)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (dialector _Dialector) DefaultValueOf(field *schema.Field) clause.Expression {
|
||||
if field.AutoIncrement {
|
||||
return clause.Expr{SQL: "NULL"}
|
||||
}
|
||||
|
||||
// doesn't work, will raise error
|
||||
return clause.Expr{SQL: "DEFAULT"}
|
||||
}
|
||||
|
||||
func (dialector _Dialector) Migrator(db *gorm.DB) gorm.Migrator {
|
||||
return _Migrator{migrator.Migrator{Config: migrator.Config{
|
||||
DB: db,
|
||||
Dialector: dialector,
|
||||
CreateIndexAfterCreateTable: true,
|
||||
}}}
|
||||
}
|
||||
|
||||
func (dialector _Dialector) BindVarTo(writer clause.Writer, stmt *gorm.Statement, v interface{}) {
|
||||
writer.WriteByte('?')
|
||||
}
|
||||
|
||||
func (dialector _Dialector) QuoteTo(writer clause.Writer, str string) {
|
||||
var (
|
||||
underQuoted, selfQuoted bool
|
||||
continuousBacktick int8
|
||||
shiftDelimiter int8
|
||||
)
|
||||
|
||||
for _, v := range []byte(str) {
|
||||
switch v {
|
||||
case '`':
|
||||
continuousBacktick++
|
||||
if continuousBacktick == 2 {
|
||||
writer.WriteString("``")
|
||||
continuousBacktick = 0
|
||||
}
|
||||
case '.':
|
||||
if continuousBacktick > 0 || !selfQuoted {
|
||||
shiftDelimiter = 0
|
||||
underQuoted = false
|
||||
continuousBacktick = 0
|
||||
writer.WriteString("`")
|
||||
}
|
||||
writer.WriteByte(v)
|
||||
continue
|
||||
default:
|
||||
if shiftDelimiter-continuousBacktick <= 0 && !underQuoted {
|
||||
writer.WriteString("`")
|
||||
underQuoted = true
|
||||
if selfQuoted = continuousBacktick > 0; selfQuoted {
|
||||
continuousBacktick -= 1
|
||||
}
|
||||
}
|
||||
|
||||
for ; continuousBacktick > 0; continuousBacktick -= 1 {
|
||||
writer.WriteString("``")
|
||||
}
|
||||
|
||||
writer.WriteByte(v)
|
||||
}
|
||||
shiftDelimiter++
|
||||
}
|
||||
|
||||
if continuousBacktick > 0 && !selfQuoted {
|
||||
writer.WriteString("``")
|
||||
}
|
||||
writer.WriteString("`")
|
||||
}
|
||||
|
||||
func (dialector _Dialector) Explain(sql string, vars ...interface{}) string {
|
||||
return logger.ExplainSQL(sql, nil, `"`, vars...)
|
||||
}
|
||||
|
||||
func (dialector _Dialector) DataTypeOf(field *schema.Field) string {
|
||||
switch field.DataType {
|
||||
case schema.Bool:
|
||||
return "numeric"
|
||||
case schema.Int, schema.Uint:
|
||||
if field.AutoIncrement {
|
||||
// doesn't check `PrimaryKey`, to keep backward compatibility
|
||||
// https://sqlite.org/autoinc.html
|
||||
return "integer PRIMARY KEY AUTOINCREMENT"
|
||||
} else {
|
||||
return "integer"
|
||||
}
|
||||
case schema.Float:
|
||||
return "real"
|
||||
case schema.String:
|
||||
return "text"
|
||||
case schema.Time:
|
||||
// Distinguish between schema.Time and tag time
|
||||
if val, ok := field.TagSettings["TYPE"]; ok {
|
||||
return val
|
||||
} else {
|
||||
return "datetime"
|
||||
}
|
||||
case schema.Bytes:
|
||||
return "blob"
|
||||
}
|
||||
|
||||
return string(field.DataType)
|
||||
}
|
||||
|
||||
func (dialectopr _Dialector) SavePoint(tx *gorm.DB, name string) error {
|
||||
tx.Exec("SAVEPOINT " + name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialectopr _Dialector) RollbackTo(tx *gorm.DB, name string) error {
|
||||
tx.Exec("ROLLBACK TO SAVEPOINT " + name)
|
||||
return nil
|
||||
}
|
||||
96
gormlite/sqlite_test.go
Normal file
96
gormlite/sqlite_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package gormlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
)
|
||||
|
||||
func TestDialector(t *testing.T) {
|
||||
// This is the DSN of the in-memory SQLite database for these tests.
|
||||
const InMemoryDSN = "file:testdatabase?mode=memory&cache=shared"
|
||||
|
||||
// Custom connection with a custom function called "my_custom_function".
|
||||
db, err := driver.Open(InMemoryDSN, func(conn *sqlite3.Conn) error {
|
||||
return conn.CreateFunction("my_custom_function", 0, sqlite3.DETERMINISTIC,
|
||||
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
||||
ctx.ResultText("my-result")
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rows := []struct {
|
||||
description string
|
||||
dialector gorm.Dialector
|
||||
openSuccess bool
|
||||
query string
|
||||
querySuccess bool
|
||||
}{
|
||||
{
|
||||
description: "Default driver",
|
||||
dialector: Open(InMemoryDSN),
|
||||
openSuccess: true,
|
||||
query: "SELECT 1",
|
||||
querySuccess: true,
|
||||
},
|
||||
{
|
||||
description: "Custom function",
|
||||
dialector: Open(InMemoryDSN),
|
||||
openSuccess: true,
|
||||
query: "SELECT my_custom_function()",
|
||||
querySuccess: false,
|
||||
},
|
||||
{
|
||||
description: "Custom connection",
|
||||
dialector: OpenDB(db),
|
||||
openSuccess: true,
|
||||
query: "SELECT 1",
|
||||
querySuccess: true,
|
||||
},
|
||||
{
|
||||
description: "Custom connection, custom function",
|
||||
dialector: OpenDB(db),
|
||||
openSuccess: true,
|
||||
query: "SELECT my_custom_function()",
|
||||
querySuccess: true,
|
||||
},
|
||||
}
|
||||
for rowIndex, row := range rows {
|
||||
t.Run(fmt.Sprintf("%d/%s", rowIndex, row.description), func(t *testing.T) {
|
||||
db, err := gorm.Open(row.dialector, &gorm.Config{})
|
||||
if !row.openSuccess {
|
||||
if err == nil {
|
||||
t.Errorf("Expected Open to fail.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected Open to succeed; got error: %v", err)
|
||||
}
|
||||
if db == nil {
|
||||
t.Errorf("Expected db to be non-nil.")
|
||||
}
|
||||
if row.query != "" {
|
||||
err = db.Exec(row.query).Error
|
||||
if !row.querySuccess {
|
||||
if err == nil {
|
||||
t.Errorf("Expected query to fail.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected query to succeed; got error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
27
gormlite/test.sh
Executable file
27
gormlite/test.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
|
||||
rm -rf gorm/ tests/
|
||||
go work use -r .
|
||||
go test
|
||||
|
||||
git clone --branch v1.25.7 --filter=blob:none https://github.com/go-gorm/gorm.git
|
||||
mv gorm/tests tests
|
||||
rm -rf gorm/
|
||||
|
||||
patch -p1 -N < tests.patch
|
||||
|
||||
cd tests
|
||||
go mod edit \
|
||||
-require github.com/ncruces/go-sqlite3/gormlite@v0.0.0 \
|
||||
-replace github.com/ncruces/go-sqlite3/gormlite=../ \
|
||||
-replace github.com/ncruces/go-sqlite3=../../ \
|
||||
-droprequire gorm.io/driver/sqlite \
|
||||
-dropreplace gorm.io/gorm
|
||||
go mod tidy && go work use . && go test
|
||||
|
||||
cd ..
|
||||
rm -rf tests/
|
||||
go work use -r .
|
||||
22
gormlite/tests.patch
Normal file
22
gormlite/tests.patch
Normal file
@@ -0,0 +1,22 @@
|
||||
diff --git a/tests/.gitignore b/tests/.gitignore
|
||||
--- a/tests/.gitignore
|
||||
+++ b/tests/.gitignore
|
||||
@@ -1 +1 @@
|
||||
-go.sum
|
||||
+*
|
||||
diff --git a/tests/tests_test.go b/tests/tests_test.go
|
||||
--- a/tests/tests_test.go
|
||||
+++ b/tests/tests_test.go
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
+ _ "github.com/ncruces/go-sqlite3/embed"
|
||||
+ sqlite "github.com/ncruces/go-sqlite3/gormlite"
|
||||
+
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
- "gorm.io/driver/sqlite"
|
||||
"gorm.io/driver/sqlserver"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
22
internal/util/bool.go
Normal file
22
internal/util/bool.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package util
|
||||
|
||||
import "strings"
|
||||
|
||||
func ParseBool(s string) (b, ok bool) {
|
||||
if len(s) == 0 {
|
||||
return false, false
|
||||
}
|
||||
if s[0] == '0' {
|
||||
return false, true
|
||||
}
|
||||
if '1' <= s[0] && s[0] <= '9' {
|
||||
return true, true
|
||||
}
|
||||
switch strings.ToLower(s) {
|
||||
case "true", "yes", "on":
|
||||
return true, true
|
||||
case "false", "no", "off":
|
||||
return false, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
28
internal/util/bool_test.go
Normal file
28
internal/util/bool_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseBool(t *testing.T) {
|
||||
tests := []struct {
|
||||
str string
|
||||
val bool
|
||||
ok bool
|
||||
}{
|
||||
{"", false, false},
|
||||
{"0", false, true},
|
||||
{"1", true, true},
|
||||
{"9", true, true},
|
||||
{"T", false, false},
|
||||
{"true", true, true},
|
||||
{"FALSE", false, true},
|
||||
{"false?", false, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.str, func(t *testing.T) {
|
||||
gotVal, gotOK := ParseBool(tt.str)
|
||||
if gotVal != tt.val || gotOK != tt.ok {
|
||||
t.Errorf("ParseBool(%q) = (%v, %v) want (%v, %v)", tt.str, gotVal, gotOK, tt.val, tt.ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ const (
|
||||
IOERR_ROLLBACK_ATOMIC = IOERR | (31 << 8)
|
||||
IOERR_DATA = IOERR | (32 << 8)
|
||||
IOERR_CORRUPTFS = IOERR | (33 << 8)
|
||||
IOERR_IN_PAGE = IOERR | (34 << 8)
|
||||
LOCKED_SHAREDCACHE = LOCKED | (1 << 8)
|
||||
LOCKED_VTAB = LOCKED | (2 << 8)
|
||||
BUSY_RECOVERY = BUSY | (1 << 8)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
)
|
||||
@@ -15,14 +14,14 @@ const (
|
||||
OOMErr = ErrorString("sqlite3: out of memory")
|
||||
RangeErr = ErrorString("sqlite3: index out of range")
|
||||
NoNulErr = ErrorString("sqlite3: missing NUL terminator")
|
||||
NoGlobalErr = ErrorString("sqlite3: could not find global: ")
|
||||
NoFuncErr = ErrorString("sqlite3: could not find function: ")
|
||||
BinaryErr = ErrorString("sqlite3: no SQLite binary embed/set/loaded")
|
||||
NoBinaryErr = ErrorString("sqlite3: no SQLite binary embed/set/loaded")
|
||||
BadBinaryErr = ErrorString("sqlite3: invalid SQLite binary embed/set/loaded")
|
||||
TimeErr = ErrorString("sqlite3: invalid time value")
|
||||
WhenceErr = ErrorString("sqlite3: invalid whence")
|
||||
OffsetErr = ErrorString("sqlite3: invalid offset")
|
||||
TailErr = ErrorString("sqlite3: multiple statements")
|
||||
IsolationErr = ErrorString("sqlite3: unsupported isolation level")
|
||||
ValueErr = ErrorString("sqlite3: unsupported value")
|
||||
NoVFSErr = ErrorString("sqlite3: no such vfs: ")
|
||||
)
|
||||
|
||||
@@ -34,14 +33,6 @@ func AssertErr() ErrorString {
|
||||
return ErrorString(msg)
|
||||
}
|
||||
|
||||
func Finalizer[T any](skip int) func(*T) {
|
||||
msg := fmt.Sprintf("sqlite3: %T not closed", new(T))
|
||||
if _, file, line, ok := runtime.Caller(skip + 1); ok && skip >= 0 {
|
||||
msg += " (" + file + ":" + strconv.Itoa(line) + ")"
|
||||
}
|
||||
return func(*T) { panic(ErrorString(msg)) }
|
||||
}
|
||||
|
||||
func ErrorCodeString(rc uint32) string {
|
||||
switch rc {
|
||||
case ABORT_ROLLBACK:
|
||||
|
||||
@@ -10,6 +10,84 @@ import (
|
||||
type i32 interface{ ~int32 | ~uint32 }
|
||||
type i64 interface{ ~int64 | ~uint64 }
|
||||
|
||||
type funcVI[T0 i32] func(context.Context, api.Module, T0)
|
||||
|
||||
func (fn funcVI[T0]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
fn(ctx, mod, T0(stack[0]))
|
||||
}
|
||||
|
||||
func ExportFuncVI[T0 i32](mod wazero.HostModuleBuilder, name string, fn func(context.Context, api.Module, T0)) {
|
||||
mod.NewFunctionBuilder().
|
||||
WithGoModuleFunction(funcVI[T0](fn),
|
||||
[]api.ValueType{api.ValueTypeI32}, nil).
|
||||
Export(name)
|
||||
}
|
||||
|
||||
type funcVII[T0, T1 i32] func(context.Context, api.Module, T0, T1)
|
||||
|
||||
func (fn funcVII[T0, T1]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]))
|
||||
}
|
||||
|
||||
func ExportFuncVII[T0, T1 i32](mod wazero.HostModuleBuilder, name string, fn func(context.Context, api.Module, T0, T1)) {
|
||||
mod.NewFunctionBuilder().
|
||||
WithGoModuleFunction(funcVII[T0, T1](fn),
|
||||
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, nil).
|
||||
Export(name)
|
||||
}
|
||||
|
||||
type funcVIII[T0, T1, T2 i32] func(context.Context, api.Module, T0, T1, T2)
|
||||
|
||||
func (fn funcVIII[T0, T1, T2]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]))
|
||||
}
|
||||
|
||||
func ExportFuncVIII[T0, T1, T2 i32](mod wazero.HostModuleBuilder, name string, fn func(context.Context, api.Module, T0, T1, T2)) {
|
||||
mod.NewFunctionBuilder().
|
||||
WithGoModuleFunction(funcVIII[T0, T1, T2](fn),
|
||||
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32}, nil).
|
||||
Export(name)
|
||||
}
|
||||
|
||||
type funcVIIII[T0, T1, T2, T3 i32] func(context.Context, api.Module, T0, T1, T2, T3)
|
||||
|
||||
func (fn funcVIIII[T0, T1, T2, T3]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]))
|
||||
}
|
||||
|
||||
func ExportFuncVIIII[T0, T1, T2, T3 i32](mod wazero.HostModuleBuilder, name string, fn func(context.Context, api.Module, T0, T1, T2, T3)) {
|
||||
mod.NewFunctionBuilder().
|
||||
WithGoModuleFunction(funcVIIII[T0, T1, T2, T3](fn),
|
||||
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32}, nil).
|
||||
Export(name)
|
||||
}
|
||||
|
||||
type funcVIIIII[T0, T1, T2, T3, T4 i32] func(context.Context, api.Module, T0, T1, T2, T3, T4)
|
||||
|
||||
func (fn funcVIIIII[T0, T1, T2, T3, T4]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4]))
|
||||
}
|
||||
|
||||
func ExportFuncVIIIII[T0, T1, T2, T3, T4 i32](mod wazero.HostModuleBuilder, name string, fn func(context.Context, api.Module, T0, T1, T2, T3, T4)) {
|
||||
mod.NewFunctionBuilder().
|
||||
WithGoModuleFunction(funcVIIIII[T0, T1, T2, T3, T4](fn),
|
||||
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32}, nil).
|
||||
Export(name)
|
||||
}
|
||||
|
||||
type funcVIIIIJ[T0, T1, T2, T3 i32, T4 i64] func(context.Context, api.Module, T0, T1, T2, T3, T4)
|
||||
|
||||
func (fn funcVIIIIJ[T0, T1, T2, T3, T4]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4]))
|
||||
}
|
||||
|
||||
func ExportFuncVIIIIJ[T0, T1, T2, T3 i32, T4 i64](mod wazero.HostModuleBuilder, name string, fn func(context.Context, api.Module, T0, T1, T2, T3, T4)) {
|
||||
mod.NewFunctionBuilder().
|
||||
WithGoModuleFunction(funcVIIIIJ[T0, T1, T2, T3, T4](fn),
|
||||
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI64}, nil).
|
||||
Export(name)
|
||||
}
|
||||
|
||||
type funcII[TR, T0 i32] func(context.Context, api.Module, T0) TR
|
||||
|
||||
func (fn funcII[TR, T0]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
@@ -75,6 +153,19 @@ func ExportFuncIIIIII[TR, T0, T1, T2, T3, T4 i32](mod wazero.HostModuleBuilder,
|
||||
Export(name)
|
||||
}
|
||||
|
||||
type funcIIIIIII[TR, T0, T1, T2, T3, T4, T5 i32] func(context.Context, api.Module, T0, T1, T2, T3, T4, T5) TR
|
||||
|
||||
func (fn funcIIIIIII[TR, T0, T1, T2, T3, T4, T5]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
stack[0] = uint64(fn(ctx, mod, T0(stack[0]), T1(stack[1]), T2(stack[2]), T3(stack[3]), T4(stack[4]), T5(stack[5])))
|
||||
}
|
||||
|
||||
func ExportFuncIIIIIII[TR, T0, T1, T2, T3, T4, T5 i32](mod wazero.HostModuleBuilder, name string, fn func(context.Context, api.Module, T0, T1, T2, T3, T4, T5) TR) {
|
||||
mod.NewFunctionBuilder().
|
||||
WithGoModuleFunction(funcIIIIIII[TR, T0, T1, T2, T3, T4, T5](fn),
|
||||
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32}).
|
||||
Export(name)
|
||||
}
|
||||
|
||||
type funcIIIIJ[TR, T0, T1, T2 i32, T3 i64] func(context.Context, api.Module, T0, T1, T2, T3) TR
|
||||
|
||||
func (fn funcIIIIJ[TR, T0, T1, T2, T3]) Call(ctx context.Context, mod api.Module, stack []uint64) {
|
||||
|
||||
75
internal/util/handle.go
Normal file
75
internal/util/handle.go
Normal file
@@ -0,0 +1,75 @@
|
||||
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
|
||||
}
|
||||
|
||||
func (s *handleState) CloseNotify(ctx context.Context, exitCode uint32) {
|
||||
for _, h := range s.handles {
|
||||
if c, ok := h.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
s.handles = nil
|
||||
s.empty = 0
|
||||
}
|
||||
|
||||
func GetHandle(ctx context.Context, id uint32) any {
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
s := ctx.Value(handleKey{}).(*handleState)
|
||||
return s.handles[^id]
|
||||
}
|
||||
|
||||
func DelHandle(ctx context.Context, id uint32) error {
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
s := ctx.Value(handleKey{}).(*handleState)
|
||||
a := s.handles[^id]
|
||||
s.handles[^id] = nil
|
||||
s.empty++
|
||||
if c, ok := a.(io.Closer); ok {
|
||||
return c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddHandle(ctx context.Context, a any) (id uint32) {
|
||||
if a == nil {
|
||||
panic(NilErr)
|
||||
}
|
||||
s := ctx.Value(handleKey{}).(*handleState)
|
||||
|
||||
// Find an empty slot.
|
||||
if s.empty > cap(s.handles)-len(s.handles) {
|
||||
for id, h := range s.handles {
|
||||
if h == nil {
|
||||
s.empty--
|
||||
s.handles[id] = a
|
||||
return ^uint32(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new slot.
|
||||
s.handles = append(s.handles, a)
|
||||
return -uint32(len(s.handles))
|
||||
}
|
||||
35
internal/util/json.go
Normal file
35
internal/util/json.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type JSON struct{ Value any }
|
||||
|
||||
func (j JSON) Scan(value any) error {
|
||||
var buf []byte
|
||||
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
buf = v
|
||||
case string:
|
||||
buf = unsafe.Slice(unsafe.StringData(v), len(v))
|
||||
case int64:
|
||||
buf = strconv.AppendInt(nil, v, 10)
|
||||
case float64:
|
||||
buf = strconv.AppendFloat(nil, v, 'g', -1, 64)
|
||||
case time.Time:
|
||||
buf = append(buf, '"')
|
||||
buf = v.AppendFormat(buf, time.RFC3339Nano)
|
||||
buf = append(buf, '"')
|
||||
case nil:
|
||||
buf = append(buf, "null"...)
|
||||
default:
|
||||
panic(AssertErr())
|
||||
}
|
||||
|
||||
return json.Unmarshal(buf, j.Value)
|
||||
}
|
||||
@@ -14,6 +14,9 @@ func View(mod api.Module, ptr uint32, size uint64) []byte {
|
||||
if size > math.MaxUint32 {
|
||||
panic(RangeErr)
|
||||
}
|
||||
if size == 0 {
|
||||
return nil
|
||||
}
|
||||
buf, ok := mod.Memory().Read(ptr, uint32(size))
|
||||
if !ok {
|
||||
panic(RangeErr)
|
||||
@@ -21,6 +24,17 @@ func View(mod api.Module, ptr uint32, size uint64) []byte {
|
||||
return buf
|
||||
}
|
||||
|
||||
func ReadUint8(mod api.Module, ptr uint32) uint8 {
|
||||
if ptr == 0 {
|
||||
panic(NilErr)
|
||||
}
|
||||
v, ok := mod.Memory().ReadByte(ptr)
|
||||
if !ok {
|
||||
panic(RangeErr)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func ReadUint32(mod api.Module, ptr uint32) uint32 {
|
||||
if ptr == 0 {
|
||||
panic(NilErr)
|
||||
@@ -32,6 +46,16 @@ func ReadUint32(mod api.Module, ptr uint32) uint32 {
|
||||
return v
|
||||
}
|
||||
|
||||
func WriteUint8(mod api.Module, ptr uint32, v uint8) {
|
||||
if ptr == 0 {
|
||||
panic(NilErr)
|
||||
}
|
||||
ok := mod.Memory().WriteByte(ptr, v)
|
||||
if !ok {
|
||||
panic(RangeErr)
|
||||
}
|
||||
}
|
||||
|
||||
func WriteUint32(mod api.Module, ptr uint32, v uint32) {
|
||||
if ptr == 0 {
|
||||
panic(NilErr)
|
||||
|
||||
@@ -3,88 +3,118 @@ package util
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero/experimental/wazerotest"
|
||||
)
|
||||
|
||||
func TestView_nil(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
View(mock, 0, 8)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestView_range(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
View(mock, 126, 8)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
View(mock, wazerotest.PageSize-2, 8)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestView_overflow(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
View(mock, 1, math.MaxInt64)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestReadUint8_nil(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
ReadUint8(mock, 0)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestReadUint8_range(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
ReadUint8(mock, wazerotest.PageSize)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestReadUint32_nil(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
ReadUint32(mock, 0)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestReadUint32_range(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
ReadUint32(mock, 126)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
ReadUint32(mock, wazerotest.PageSize-2)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestReadUint64_nil(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
ReadUint64(mock, 0)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestReadUint64_range(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
ReadUint64(mock, 126)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
ReadUint64(mock, wazerotest.PageSize-2)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestWriteUint8_nil(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
WriteUint8(mock, 0, 1)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestWriteUint8_range(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
WriteUint8(mock, wazerotest.PageSize, 1)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestWriteUint32_nil(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
WriteUint32(mock, 0, 1)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestWriteUint32_range(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
WriteUint32(mock, 126, 1)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
WriteUint32(mock, wazerotest.PageSize-2, 1)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestWriteUint64_nil(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
WriteUint64(mock, 0, 1)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestWriteUint64_range(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
WriteUint64(mock, 126, 1)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
WriteUint64(mock, wazerotest.PageSize-2, 1)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestReadString_range(t *testing.T) {
|
||||
defer func() { _ = recover() }()
|
||||
mock := NewMockModule(128)
|
||||
ReadString(mock, 130, math.MaxUint32)
|
||||
mock := wazerotest.NewModule(wazerotest.NewFixedMemory(wazerotest.PageSize))
|
||||
ReadString(mock, wazerotest.PageSize+2, math.MaxUint32)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
)
|
||||
|
||||
func NewMockModule(size uint32) api.Module {
|
||||
mem := mockMemory{buf: make([]byte, size)}
|
||||
return mockModule{&mem, nil}
|
||||
}
|
||||
|
||||
type mockModule struct {
|
||||
memory api.Memory
|
||||
api.Module
|
||||
}
|
||||
|
||||
func (m mockModule) Memory() api.Memory { return m.memory }
|
||||
func (m mockModule) String() string { return "mockModule" }
|
||||
func (m mockModule) Name() string { return "mockModule" }
|
||||
|
||||
type mockMemory struct {
|
||||
buf []byte
|
||||
api.Memory
|
||||
}
|
||||
|
||||
func (m mockMemory) Definition() api.MemoryDefinition { return nil }
|
||||
|
||||
func (m mockMemory) Size() uint32 { return uint32(len(m.buf)) }
|
||||
|
||||
func (m mockMemory) ReadByte(offset uint32) (byte, bool) {
|
||||
if offset >= m.Size() {
|
||||
return 0, false
|
||||
}
|
||||
return m.buf[offset], true
|
||||
}
|
||||
|
||||
func (m mockMemory) ReadUint16Le(offset uint32) (uint16, bool) {
|
||||
if !m.hasSize(offset, 2) {
|
||||
return 0, false
|
||||
}
|
||||
return binary.LittleEndian.Uint16(m.buf[offset : offset+2]), true
|
||||
}
|
||||
|
||||
func (m mockMemory) ReadUint32Le(offset uint32) (uint32, bool) {
|
||||
if !m.hasSize(offset, 4) {
|
||||
return 0, false
|
||||
}
|
||||
return binary.LittleEndian.Uint32(m.buf[offset : offset+4]), true
|
||||
}
|
||||
|
||||
func (m mockMemory) ReadFloat32Le(offset uint32) (float32, bool) {
|
||||
v, ok := m.ReadUint32Le(offset)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return math.Float32frombits(v), true
|
||||
}
|
||||
|
||||
func (m mockMemory) ReadUint64Le(offset uint32) (uint64, bool) {
|
||||
if !m.hasSize(offset, 8) {
|
||||
return 0, false
|
||||
}
|
||||
return binary.LittleEndian.Uint64(m.buf[offset : offset+8]), true
|
||||
}
|
||||
|
||||
func (m mockMemory) ReadFloat64Le(offset uint32) (float64, bool) {
|
||||
v, ok := m.ReadUint64Le(offset)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return math.Float64frombits(v), true
|
||||
}
|
||||
|
||||
func (m mockMemory) Read(offset, byteCount uint32) ([]byte, bool) {
|
||||
if !m.hasSize(offset, byteCount) {
|
||||
return nil, false
|
||||
}
|
||||
return m.buf[offset : offset+byteCount : offset+byteCount], true
|
||||
}
|
||||
|
||||
func (m mockMemory) WriteByte(offset uint32, v byte) bool {
|
||||
if offset >= m.Size() {
|
||||
return false
|
||||
}
|
||||
m.buf[offset] = v
|
||||
return true
|
||||
}
|
||||
|
||||
func (m mockMemory) WriteUint16Le(offset uint32, v uint16) bool {
|
||||
if !m.hasSize(offset, 2) {
|
||||
return false
|
||||
}
|
||||
binary.LittleEndian.PutUint16(m.buf[offset:], v)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m mockMemory) WriteUint32Le(offset, v uint32) bool {
|
||||
if !m.hasSize(offset, 4) {
|
||||
return false
|
||||
}
|
||||
binary.LittleEndian.PutUint32(m.buf[offset:], v)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m mockMemory) WriteFloat32Le(offset uint32, v float32) bool {
|
||||
return m.WriteUint32Le(offset, math.Float32bits(v))
|
||||
}
|
||||
|
||||
func (m mockMemory) WriteUint64Le(offset uint32, v uint64) bool {
|
||||
if !m.hasSize(offset, 8) {
|
||||
return false
|
||||
}
|
||||
binary.LittleEndian.PutUint64(m.buf[offset:], v)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m mockMemory) WriteFloat64Le(offset uint32, v float64) bool {
|
||||
return m.WriteUint64Le(offset, math.Float64bits(v))
|
||||
}
|
||||
|
||||
func (m mockMemory) Write(offset uint32, val []byte) bool {
|
||||
if !m.hasSize(offset, uint32(len(val))) {
|
||||
return false
|
||||
}
|
||||
copy(m.buf[offset:], val)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m mockMemory) WriteString(offset uint32, val string) bool {
|
||||
if !m.hasSize(offset, uint32(len(val))) {
|
||||
return false
|
||||
}
|
||||
copy(m.buf[offset:], val)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *mockMemory) Grow(delta uint32) (result uint32, ok bool) {
|
||||
prev := (len(m.buf) + 65535) / 65536
|
||||
m.buf = append(m.buf, make([]byte, 65536*delta)...)
|
||||
return uint32(prev), true
|
||||
}
|
||||
|
||||
func (m mockMemory) hasSize(offset uint32, byteCount uint32) bool {
|
||||
return uint64(offset)+uint64(byteCount) <= uint64(len(m.buf))
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_mockMemory_byte(t *testing.T) {
|
||||
const want byte = 98
|
||||
mock := NewMockModule(128)
|
||||
|
||||
_, ok := mock.Memory().ReadByte(128)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteByte(128, 0)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteByte(0, want)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
|
||||
got, ok := mock.Memory().ReadByte(0)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mockMemory_uint16(t *testing.T) {
|
||||
const want uint16 = 9876
|
||||
mock := NewMockModule(128)
|
||||
|
||||
_, ok := mock.Memory().ReadUint16Le(128)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteUint16Le(128, 0)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteUint16Le(0, want)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
|
||||
got, ok := mock.Memory().ReadUint16Le(0)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mockMemory_uint32(t *testing.T) {
|
||||
const want uint32 = 987654321
|
||||
mock := NewMockModule(128)
|
||||
|
||||
_, ok := mock.Memory().ReadUint32Le(128)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteUint32Le(128, 0)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteUint32Le(0, want)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
|
||||
got, ok := mock.Memory().ReadUint32Le(0)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mockMemory_uint64(t *testing.T) {
|
||||
const want uint64 = 9876543210
|
||||
mock := NewMockModule(128)
|
||||
|
||||
_, ok := mock.Memory().ReadUint64Le(128)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteUint64Le(128, 0)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteUint64Le(0, want)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
|
||||
got, ok := mock.Memory().ReadUint64Le(0)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mockMemory_float32(t *testing.T) {
|
||||
const want float32 = math.Pi
|
||||
mock := NewMockModule(128)
|
||||
|
||||
_, ok := mock.Memory().ReadFloat32Le(128)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteFloat32Le(128, 0)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteFloat32Le(0, want)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
|
||||
got, ok := mock.Memory().ReadFloat32Le(0)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %f, want %f", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mockMemory_float64(t *testing.T) {
|
||||
const want float64 = math.Pi
|
||||
mock := NewMockModule(128)
|
||||
|
||||
_, ok := mock.Memory().ReadFloat64Le(128)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteFloat64Le(128, 0)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteFloat64Le(0, want)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
|
||||
got, ok := mock.Memory().ReadFloat64Le(0)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %f, want %f", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mockMemory_bytes(t *testing.T) {
|
||||
const want string = "\xca\xfe\xba\xbe"
|
||||
mock := NewMockModule(128)
|
||||
|
||||
_, ok := mock.Memory().Read(128, uint32(len(want)))
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().Write(128, []byte(want))
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteString(128, want)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
ok = mock.Memory().Write(0, []byte(want))
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
|
||||
got, ok := mock.Memory().Read(0, uint32(len(want)))
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
if string(got) != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
ok = mock.Memory().WriteString(64, want)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
|
||||
got, ok = mock.Memory().Read(64, uint32(len(want)))
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
if string(got) != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mockMemory_grow(t *testing.T) {
|
||||
mock := NewMockModule(128)
|
||||
|
||||
_, ok := mock.Memory().ReadByte(65536)
|
||||
if ok {
|
||||
t.Error("want error")
|
||||
}
|
||||
|
||||
got, ok := mock.Memory().Grow(1)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
if got != 1 {
|
||||
t.Errorf("got %d, want 1", got)
|
||||
}
|
||||
|
||||
_, ok = mock.Memory().ReadByte(65536)
|
||||
if !ok {
|
||||
t.Error("want ok")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user