Compare commits

...

250 Commits

Author SHA1 Message Date
Nuno Cruces
4142680d5a Updated modules. 2025-02-20 13:36:02 +00:00
Nuno Cruces
9f4fe6f27c SQLite 3.49.1. 2025-02-18 18:03:20 +00:00
Nuno Cruces
7870ce0690 wazero v1.9.0. 2025-02-18 16:36:22 +00:00
Nuno Cruces
ec3226e16e Fix CI. 2025-02-17 12:21:53 +00:00
Nuno Cruces
4dd7bd0ff2 More type safe. 2025-02-17 12:00:55 +00:00
Nuno Cruces
975feb2fd4 Issue #228. 2025-02-16 18:09:42 +00:00
Nuno Cruces
58f8c2d33e Ignore. 2025-02-15 01:12:38 +00:00
Nuno Cruces
019660eed6 Fix warning. 2025-02-12 09:58:58 +00:00
Nuno Cruces
30c1bcdbe9 Serdes robustness. 2025-02-12 00:41:16 +00:00
Nuno Cruces
9b4002f5ac Add missing consts. 2025-02-11 18:24:05 +00:00
Nuno Cruces
2a78d4bc2b Updated modules. 2025-02-11 18:15:14 +00:00
Nuno Cruces
c09623a903 binaryen-version_122. 2025-02-11 18:07:30 +00:00
Nuno Cruces
fa613f9ddb Remove go.work. 2025-02-11 17:50:37 +00:00
Nuno Cruces
57997201ee SQLite 3.49.0. 2025-02-10 07:20:01 +00:00
dependabot[bot]
6995cca5c0 Bump golang.org/x/crypto from 0.32.0 to 0.33.0 (#225)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/crypto/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-08 08:40:31 +00:00
dependabot[bot]
a10eef3ac8 Bump golang.org/x/sys from 0.29.0 to 0.30.0 (#223)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/sys/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 19:02:19 +01:00
dependabot[bot]
d627ca3dc1 Bump golang.org/x/text from 0.21.0 to 0.22.0 (#221)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.21.0 to 0.22.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-04 23:35:35 +01:00
Nuno Cruces
b2f7ab8335 Fix GlobPrefix. (#220) 2025-01-28 17:54:17 +00:00
Nuno Cruces
c9135b9823 UUID version and timestamp. 2025-01-28 11:51:27 +00:00
dependabot[bot]
0d9ed94aad Bump github.com/ncruces/sort from 0.1.2 to 0.1.3 (#218)
Bumps [github.com/ncruces/sort](https://github.com/ncruces/sort) from 0.1.2 to 0.1.3.
- [Commits](https://github.com/ncruces/sort/compare/v0.1.2...v0.1.3)

---
updated-dependencies:
- dependency-name: github.com/ncruces/sort
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-24 22:56:31 +00:00
Nuno Cruces
1d951ecd18 Go 1.22. 2025-01-24 10:46:05 +00:00
Nuno Cruces
c0298ad274 NetBSD 10.1. 2025-01-22 17:20:57 +00:00
Nuno Cruces
42bad5891a Skewness and excess kurtosis. 2025-01-22 12:09:20 +00:00
Nuno Cruces
40090d8250 Moments. 2025-01-21 14:11:47 +00:00
Nuno Cruces
d2f162972d More type safe. (#216) 2025-01-21 01:42:57 +00:00
Nuno Cruces
e2da469834 Fix numerical issues. 2025-01-20 14:39:36 +00:00
Nuno Cruces
1677b97fa4 Fix #215. 2025-01-19 01:30:04 +00:00
Nuno Cruces
407e13d238 Handle some errors. 2025-01-17 14:40:12 +00:00
Nuno Cruces
9132f74b69 Use Linux ARM runners. 2025-01-17 11:49:35 +00:00
Nuno Cruces
c024121fd2 C tweaks. 2025-01-17 10:51:25 +00:00
Nuno Cruces
aa8287f8e7 Allow others to enable threads. 2025-01-16 17:21:36 +00:00
Nuno Cruces
ab09da7136 More unicode. 2025-01-16 15:46:49 +00:00
Nuno Cruces
a159b548ed Dependencies. 2025-01-14 17:53:40 +00:00
Nuno Cruces
d9b37307e7 SQLite 3.48.0. 2025-01-14 17:33:53 +00:00
Nuno Cruces
3bae1d7d4b SQLITE_FCNTL_BUSYHANDLER. 2025-01-14 17:09:54 +00:00
Nuno Cruces
8887036c20 SQLITE_FCNTL_SYNC. 2025-01-14 10:05:54 +00:00
Nuno Cruces
ccb3dcd097 SQLITE_FCNTL_PDB. 2025-01-13 13:45:41 +00:00
Nuno Cruces
a9f33cc2b0 New constants. 2025-01-13 12:05:27 +00:00
Nuno Cruces
f025ffb385 Fix naming. 2025-01-13 09:28:47 +00:00
Nuno Cruces
aa4357a78f Ordered-set aggregate syntax. 2025-01-11 19:22:04 +00:00
Nuno Cruces
aef7f051a8 Prevent modification. 2025-01-10 12:38:11 +00:00
Nuno Cruces
a79ee4c2c6 Avoid weird mutex. 2025-01-09 13:44:29 +00:00
Nuno Cruces
7424747338 Update README.md 2025-01-08 23:16:25 +00:00
Nuno Cruces
11830e05a6 Remove legacy. 2025-01-08 18:34:48 +00:00
Nuno Cruces
7dc4520690 Fix #207. 2025-01-08 16:36:41 +00:00
Nuno Cruces
0c09dd89c2 Add wasmtime to CI. (#212) 2025-01-07 16:31:12 +00:00
Nuno Cruces
31c5000875 Updated GORM driver.
Fixes https://github.com/go-gorm/sqlite/issues/192.
2025-01-07 12:29:16 +00:00
Nuno Cruces
8175407754 Enable compiler on ARMv8. (#211) 2025-01-06 18:22:36 +00:00
Nuno Cruces
abfad02d95 Remove LFS test files. (#210) 2025-01-06 11:44:08 +00:00
Nuno Cruces
f7c3fb8062 Lines delimiter. 2025-01-05 19:35:07 +00:00
Nuno Cruces
c3633dda35 Update test.yml 2024-12-21 12:13:45 +00:00
Nuno Cruces
f2d894194d Avoid syscall. 2024-12-21 10:16:23 +00:00
Nuno Cruces
e08c7b3adf Refactor. 2024-12-19 15:14:20 +00:00
Nuno Cruces
66601dd3cb More BCE. 2024-12-19 14:00:46 +00:00
Nuno Cruces
58b66b75f1 Improved assertions. 2024-12-19 13:19:10 +00:00
Nuno Cruces
e0c6086aa9 Fix POSIX locks. 2024-12-18 16:21:24 +00:00
Nuno Cruces
9bc39c5b91 Remove dependency. 2024-12-17 15:43:19 +00:00
Nuno Cruces
12193cedea binaryen-version_121. 2024-12-17 15:25:25 +00:00
Nuno Cruces
71d95bf9d5 Fix #205. 2024-12-17 14:21:56 +00:00
Nuno Cruces
7e23100ff7 Help checklocks. 2024-12-16 13:47:59 +00:00
Nuno Cruces
e32d8401fb Improve Wal locking on BSD (#204) 2024-12-16 13:15:00 +00:00
Nuno Cruces
503db60927 Fix repro.sh. 2024-12-13 16:22:44 +00:00
Nuno Cruces
1227fa7a04 Skip sleeping if blocked. 2024-12-13 16:04:37 +00:00
Nuno Cruces
e455b5f729 Coverage. 2024-12-13 10:30:08 +00:00
Nuno Cruces
2bb1c8c795 Fair retry interval. 2024-12-13 10:23:43 +00:00
Nuno Cruces
844fab4167 Fix fuzzer. 2024-12-12 13:38:46 +00:00
Nuno Cruces
5ed4a6cb9d Fix #201. 2024-12-12 12:57:18 +00:00
Nuno Cruces
37f2145588 Use CancelIoEx. 2024-12-12 10:42:23 +00:00
Nuno Cruces
e17b3ef2c8 wasi-sdk-25. 2024-12-12 09:51:23 +00:00
Nuno Cruces
a75b8887db Updated dependencies. 2024-12-11 19:02:03 +00:00
Nuno Cruces
9f456fecb9 Updated dependencies. 2024-12-11 18:38:57 +00:00
Nuno Cruces
36bbd674c2 Add ColumnTypeScanType to driver (#199). 2024-12-11 18:35:50 +00:00
Nuno Cruces
7f5ea54009 Windows blocking locks. (#200) 2024-12-11 15:05:22 +00:00
Nuno Cruces
5f1d5727cd SQLite 3.47.2. 2024-12-10 18:38:49 +00:00
Nuno Cruces
6fb259e2b9 Fix. 2024-12-10 18:25:49 +00:00
Nuno Cruces
301f6bc2bd Fix. 2024-12-09 19:26:47 +00:00
Nuno Cruces
9e112c54b0 Fix, see #197. 2024-12-08 11:47:11 +00:00
Nuno Cruces
270efcb4af Updated dependencies. 2024-12-05 12:46:46 +00:00
dependabot[bot]
8252198dd2 Bump actions/attest-build-provenance from 1 to 2 (#196)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1 to 2.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 13:04:47 +00:00
dependabot[bot]
dff825ae81 Bump golang.org/x/crypto from 0.29.0 to 0.30.0 (#193)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/crypto/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 03:03:36 +00:00
Nuno Cruces
aae732e530 API fix. 2024-12-04 17:09:22 +00:00
Nuno Cruces
2e3ba3949e Remove sqlite3_nosys. 2024-11-29 15:51:51 +00:00
Nuno Cruces
a44690035f Refactor. 2024-11-28 00:15:55 +00:00
Nuno Cruces
7e12105b22 Split module. 2024-11-27 14:25:01 +00:00
Nuno Cruces
987db177ad Split module. 2024-11-27 14:22:31 +00:00
Nuno Cruces
1469cb9f1a Imports. 2024-11-27 11:42:25 +00:00
Nuno Cruces
4ede2c7216 Updated dependencies. 2024-11-26 14:18:53 +00:00
Nuno Cruces
cf14f190b2 SQLite 3.47.1. 2024-11-26 11:40:11 +00:00
dependabot[bot]
6ca92b035d Bump github.com/tetratelabs/wazero (#189)
Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.8.2-0.20241115151925-0a207958052e to 1.8.2.
- [Release notes](https://github.com/tetratelabs/wazero/releases)
- [Commits](https://github.com/tetratelabs/wazero/commits/v1.8.2)

---
updated-dependencies:
- dependency-name: github.com/tetratelabs/wazero
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 22:34:34 +00:00
Nuno Cruces
2912adf226 Update README.md 2024-11-25 14:17:08 +00:00
Nuno Cruces
b3f83a4392 Enable compiler. 2024-11-18 12:52:48 +00:00
Nuno Cruces
1223c4fc80 Flags. 2024-11-18 12:52:04 +00:00
Nuno Cruces
e9d6509577 Tweak. 2024-11-15 18:41:31 +00:00
Nuno Cruces
81dd786af7 Test more. 2024-11-12 14:14:45 +00:00
Nuno Cruces
a2253558ef Use lock file. 2024-11-11 16:55:37 +00:00
Nuno Cruces
466d14a9e0 Fix, speed. 2024-11-08 13:02:19 +00:00
Nuno Cruces
ada7b3a906 Updated dependencies. 2024-11-08 11:48:45 +00:00
Nuno Cruces
eba73a87d9 Refactor. 2024-11-07 12:18:42 +00:00
Nuno Cruces
38da27b5d1 Fix. 2024-11-06 12:10:48 +00:00
Nuno Cruces
23737a61ba Log. 2024-11-06 12:10:47 +00:00
Nuno Cruces
af473c7519 Update README.md 2024-11-06 01:34:21 +00:00
Nuno Cruces
a946c00f8e Refactor, speed. 2024-11-06 00:30:37 +00:00
Nuno Cruces
32153763a3 Update README.md 2024-11-05 17:39:36 +00:00
Nuno Cruces
a57ce87157 Windows shared memory. (#181) 2024-11-05 17:30:10 +00:00
Nuno Cruces
81e7a94ca4 Optimize regexp. 2024-11-04 19:30:10 +00:00
Nuno Cruces
034b9a3b4d More regexp. 2024-11-04 18:53:03 +00:00
Nuno Cruces
363b12ee4c Fix #178. 2024-11-03 13:14:38 +00:00
Nuno Cruces
90d6ec31b9 Refactor, speed. 2024-11-02 15:32:41 +00:00
Nuno Cruces
17f7840a83 Share memory by copying. (#180) 2024-10-31 15:21:15 +00:00
Nuno Cruces
b2e8636227 Dot file locking. (#179) 2024-10-30 12:20:21 +00:00
Nuno Cruces
1ad1608228 Refactor. 2024-10-30 00:50:28 +00:00
Nuno Cruces
9f284f0b26 Fix tests. 2024-10-30 00:44:50 +00:00
Nuno Cruces
df4e144e89 Prevent overflow. 2024-10-28 14:16:43 +00:00
Nuno Cruces
96074b24bf binaryen-version_120. 2024-10-28 10:56:10 +00:00
Nuno Cruces
4d68f8976c Dependencies. 2024-10-25 14:29:03 +01:00
Nuno Cruces
f2545534af Rename. 2024-10-25 14:20:52 +01:00
Nuno Cruces
69e5cf706b Checksums in default VFS. (#177) 2024-10-25 13:49:06 +01:00
Nuno Cruces
75c1dbb052 Checksum VFS. (#176) 2024-10-25 00:12:29 +01:00
Nuno Cruces
64e2500ca8 SQLite 3.47.0. 2024-10-24 00:34:54 +01:00
Nuno Cruces
0cd0f48365 Rename WAL, fixes. 2024-10-24 00:23:13 +01:00
Nuno Cruces
c69ee0fe8d Rename sql3util. 2024-10-22 23:36:38 +01:00
Nuno Cruces
b9b489aae9 Fixes, internal docs. 2024-10-22 13:08:31 +01:00
Nuno Cruces
21de004779 VFS API refactor. 2024-10-21 15:07:30 +01:00
Nuno Cruces
9eec439d35 More tests. 2024-10-19 08:58:55 +01:00
Nuno Cruces
fefee692db VFS utilities. 2024-10-18 18:07:48 +01:00
Nuno Cruces
f18561ee11 Lerp. 2024-10-18 13:07:39 +01:00
Nuno Cruces
ace01b2927 Split local imports. 2024-10-18 12:20:32 +01:00
Nuno Cruces
89f750a6e9 Wrap FilePersistentWAL. 2024-10-18 12:14:31 +01:00
Nuno Cruces
d6aebe67cc AES-XTS VFS (#171)
Co-authored-by: Ben Krieger <ben.krieger@intel.com>
2024-10-17 23:53:39 +01:00
Nuno Cruces
714ea0e779 Blocking locks improvements. 2024-10-17 15:39:01 +01:00
Nuno Cruces
c900889848 Configure memory, 32-bit WAL. (#170) 2024-10-17 13:04:23 +01:00
Nuno Cruces
50c8517603 Window percentil. 2024-10-17 08:15:44 +01:00
Nuno Cruces
c78d00dca0 Better percentile compatibility. 2024-10-16 14:00:22 +01:00
Nuno Cruces
ddfaf12cd8 Reorg READMEs. 2024-10-15 14:51:11 +01:00
Nuno Cruces
368c900db8 Test latest snapshot. 2024-10-10 12:58:32 +01:00
Nuno Cruces
e524fb185d More consistent interrupts. 2024-10-08 16:38:57 +01:00
Nuno Cruces
cc7bacfb9c Remove rangefunc experiment. 2024-10-07 15:13:19 +01:00
Nuno Cruces
911e497891 Improved quoting. 2024-10-07 14:42:09 +01:00
Nuno Cruces
3469460635 Improved error handling. 2024-10-07 13:22:31 +01:00
Nuno Cruces
b5adcacec4 Don't panic on memory commit failure. (#154) 2024-10-06 00:06:43 +01:00
dependabot[bot]
62d6712f82 Bump github.com/tetratelabs/wazero from 1.8.0 to 1.8.1 (#166)
Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/tetratelabs/wazero/releases)
- [Commits](https://github.com/tetratelabs/wazero/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/tetratelabs/wazero
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-06 00:02:33 +01:00
dependabot[bot]
d34e6197a8 Bump golang.org/x/crypto from 0.27.0 to 0.28.0 (#163)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.28.0.
- [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-04 23:39:43 +01:00
Nuno Cruces
f9e867be60 Time fix. 2024-10-04 18:29:35 +01:00
Nuno Cruces
96c61a2f55 Improved interrupts. 2024-10-04 16:18:24 +01:00
Nuno Cruces
ac94a5406e Clear bindings. 2024-10-04 00:35:04 +01:00
Nuno Cruces
34617e15f0 Savepoint quoting. 2024-10-03 12:01:54 +01:00
Nuno Cruces
83b3f6ce0a Time fix. 2024-10-02 12:59:59 +01:00
Nuno Cruces
f2c8aa0ddf Scanning improvements. 2024-10-02 12:59:49 +01:00
Nuno Cruces
63ea13e41e Fix examples. 2024-10-02 12:01:22 +01:00
Nuno Cruces
b1508bface Faster stats. 2024-10-01 15:16:06 +01:00
Nuno Cruces
1c6897c8e2 Multi-value Wasm. 2024-09-30 23:45:33 +01:00
Nuno Cruces
170e1dbebd Don't override LIKE. 2024-09-30 14:04:23 +01:00
Nuno Cruces
25fc5a606a Delete functions, modules, hooks. 2024-09-30 13:11:04 +01:00
Nuno Cruces
8b6c2b28fb More BCE. 2024-09-30 11:03:50 +01:00
kim
e59e2ed2a2 use "len(buf)-1" to access final slice index to eliminate boundary-checks (#160) 2024-09-29 20:01:51 +01:00
Nuno Cruces
505c6640c2 Unaccent, title case. 2024-09-29 20:01:14 +01:00
Nuno Cruces
5b0a063bfe More BCE. 2024-09-28 10:37:47 +01:00
kim
32931032d3 add compiler hints to reduce number of slice bounds checks (#158)
* add compiler hints to reduce number of slice bounds checks

* panic on assert instead of returning error for code coverage
2024-09-28 01:26:46 +01:00
Nuno Cruces
b7055ef04b Spellfix1, fix #157. 2024-09-27 16:28:17 +01:00
Nuno Cruces
167025f47a On demand collations. 2024-09-27 16:28:17 +01:00
Nuno Cruces
b4b50fc547 Context aware busy handler. (#146) 2024-09-27 12:33:57 +01:00
Nuno Cruces
08f7764fe0 Improved OPEN_NOFOLLOW. 2024-09-27 12:32:11 +01:00
Nuno Cruces
4e0b8aeaa8 Updated dependencies. 2024-09-24 14:07:45 +01:00
Nuno Cruces
d694e9718e Test ppc64le. 2024-09-24 13:20:16 +01:00
Nuno Cruces
90218c0d79 Check interfaces. 2024-09-24 12:49:53 +01:00
Nuno Cruces
44c3f9b4e7 Coverage, docs. 2024-09-22 17:34:35 +01:00
Nuno Cruces
2526fc8444 Transitive closure virtual table. 2024-09-21 11:44:17 +01:00
Nuno Cruces
d7376209ee Fix. 2024-09-21 00:51:03 +01:00
Nuno Cruces
83e2587596 Gorm v1.25.12. 2024-09-20 23:07:20 +01:00
Nuno Cruces
6101debe28 Locking improvements. 2024-09-18 11:24:46 +01:00
Nuno Cruces
06eaf41c4f Fix #151. 2024-09-16 12:05:37 +01:00
Michael Lynch
9638976991 Fix HardHeapLimit name in comment (#152) 2024-09-14 00:34:58 +01:00
Nuno Cruces
b631ff1add Configure heap limits. 2024-09-13 16:21:20 +01:00
Nuno Cruces
fdfaaa8cec Fix memory allocator issue.
Windows and unix allocators would panic when asked to allocate the maximum size.
2024-09-12 16:05:31 +01:00
Nuno Cruces
6a2827f989 Reuse blob buffer. 2024-09-10 11:48:19 +01:00
Nuno Cruces
9d77322d50 Memory management. 2024-09-09 13:21:33 +01:00
Michael Lynch
c1915feb2e Expand explanation of openblob semantics (#149)
Per @ncruces' comments in https://github.com/ncruces/go-sqlite3/issues/148#issuecomment-2334155468, this change clarifies the documentation for openblob to help clients understand the lifecycle of the blob handle within the callback.
2024-09-06 18:15:20 +01:00
Nuno Cruces
52f9af3ca0 binaryen-version_119. 2024-09-06 02:13:48 +01:00
dependabot[bot]
2f90277165 Bump golang.org/x/crypto from 0.26.0 to 0.27.0 (#147)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.26.0 to 0.27.0.
- [Commits](https://github.com/golang/crypto/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-05 23:44:46 +01:00
Nuno Cruces
356dd56e5f Improved blobio extension. 2024-09-05 12:09:21 +01:00
Nuno Cruces
e2a2d447ce Updated binaries. 2024-09-04 19:38:10 +01:00
Nuno Cruces
75190a6f98 os.Executable rather than os.Args[0] 2024-09-04 18:48:42 +01:00
Nuno Cruces
35c5619880 Tweak. 2024-09-04 18:26:57 +01:00
Nuno Cruces
b51234cc82 Reduce allocs. 2024-09-03 17:32:06 +01:00
Nuno Cruces
cf7b89d3c4 Issue #145. 2024-09-03 12:24:03 +01:00
Nuno Cruces
ff9f27a778 Fix #141. 2024-09-03 11:22:24 +01:00
Nuno Cruces
f26f1a17a9 Blocking locks (#144) 2024-09-02 23:59:26 +01:00
Nuno Cruces
b9b2ff13da Stricter test. 2024-08-30 09:32:30 +01:00
Nuno Cruces
78473b4b37 Fix BSD locks. 2024-08-30 01:27:57 +01:00
Nuno Cruces
3806c1cc23 Test tweaks. 2024-08-30 01:27:22 +01:00
Nuno Cruces
1660c41f8c Typo. 2024-08-29 10:32:10 +01:00
Nuno Cruces
62b67c937e Tweak. 2024-08-27 01:55:39 +01:00
Nuno Cruces
9e9971c292 Fix enconding. 2024-08-27 01:45:44 +01:00
Nuno Cruces
d13bf1afaa Readability. 2024-08-26 19:47:46 +01:00
Nuno Cruces
f7c9551d66 Update README.md 2024-08-15 12:42:38 +01:00
Nuno Cruces
22beef91d2 Updated dependencies. 2024-08-14 17:56:06 +01:00
Nuno Cruces
c97bbc7dab vet fix. 2024-08-14 17:20:50 +01:00
Nuno Cruces
800eb107f9 wazero v1.8.0. 2024-08-14 16:54:24 +01:00
Nuno Cruces
6a1973f530 SQLite 3.46.1. 2024-08-13 15:27:00 +01:00
Nuno Cruces
bd141fec92 Tests. 2024-08-12 17:50:23 +01:00
Nuno Cruces
e92999bfe3 Avoid alloc. 2024-08-12 17:36:06 +01:00
Nuno Cruces
d5583b6ec9 Try to fix flaky test. 2024-08-11 17:35:27 +01:00
Nuno Cruces
3649c1098e Remove unneeded check. 2024-08-11 16:18:44 +01:00
Nuno Cruces
f743639c8f Docs. 2024-08-09 14:05:49 +01:00
Nuno Cruces
7cb974fd9a Windows CI. 2024-08-09 12:16:46 +01:00
Nuno Cruces
eea6aa7493 Docs. 2024-08-09 10:06:05 +01:00
Nuno Cruces
9a610888f9 BEGIN CONCURRENT, wal2. (#138) 2024-08-09 00:48:25 +01:00
dependabot[bot]
dc4113073c Bump golang.org/x/sys from 0.23.0 to 0.24.0 (#139)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/sys/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-09 00:47:32 +01:00
Daenney
38cab3202a Add example for custom time with database/sql (#133) 2024-08-07 18:34:34 +01:00
Nuno Cruces
2068b97116 Updated dependencies. 2024-08-07 12:15:03 +01:00
Nuno Cruces
8f835eda79 Use memdb for tests. (#131) 2024-08-05 21:25:47 +01:00
Nuno Cruces
40db26c1dd wasi-sdk-24. 2024-08-03 00:13:06 +01:00
Nuno Cruces
a6815531e0 Copy blobs. 2024-08-02 13:27:01 +01:00
Daenney
6c12a8c1fa More JSON/B examples. (#127) 2024-07-31 23:31:56 +01:00
Nuno Cruces
e9de84a87f Testing. 2024-07-31 13:18:52 +01:00
Nuno Cruces
3bb1898335 More APIs. (#125)
sqlite3_db_cacheflush
sqlite3_db_status
sqlite3_expanded_sql
sqlite3_next_stmt
sqlite3_sql
sqlite3_table_column_metadata
sqlite3_trace_v2
sqlite3_value_frombind
2024-07-31 12:15:08 +01:00
Nuno Cruces
22132620b8 API tweaks, tests. 2024-07-30 14:49:58 +01:00
Nuno Cruces
c766a4fed2 Testing. 2024-07-26 23:51:35 +01:00
Nuno Cruces
73125945f8 Fix API inconsistency. 2024-07-26 12:25:15 +01:00
Nuno Cruces
32d998c84b Filenames. 2024-07-26 01:23:35 +01:00
Nuno Cruces
8d450f82fc Remove init. 2024-07-25 13:01:00 +01:00
Nuno Cruces
64b77f1a79 Concurrent transactions. 2024-07-25 01:00:31 +01:00
Nuno Cruces
19639be9f9 Gorm v1.25.11. 2024-07-24 14:25:14 +01:00
Nuno Cruces
2996e77420 Implement file control. (#123) 2024-07-24 12:37:35 +01:00
Nuno Cruces
24288c0e26 Tests. 2024-07-23 13:28:09 +01:00
Nuno Cruces
06f58c35e3 Fix Context.ResultPointer. 2024-07-20 12:52:25 +01:00
Nuno Cruces
28f225b32e Testing. 2024-07-20 01:42:50 +01:00
Nuno Cruces
b289fca3ca Everything changes, stays the same. 2024-07-20 00:43:34 +01:00
dependabot[bot]
21de85e849 Bump cross-platform-actions/action from 0.24.0 to 0.25.0 (#119)
* Bump cross-platform-actions/action from 0.24.0 to 0.25.0

Bumps [cross-platform-actions/action](https://github.com/cross-platform-actions/action) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/cross-platform-actions/action/releases)
- [Changelog](https://github.com/cross-platform-actions/action/blob/master/changelog.md)
- [Commits](https://github.com/cross-platform-actions/action/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: cross-platform-actions/action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nuno Cruces <ncruces@users.noreply.github.com>
2024-07-12 00:54:51 +01:00
Nuno Cruces
4498f35a39 wasi-sdk-23.0. 2024-07-11 15:03:16 +01:00
Nuno Cruces
0c7d0a097d Allow SQLite to use atomic operations. (#118) 2024-07-11 13:35:41 +01:00
Nuno Cruces
f537ab9a94 Tests. 2024-07-10 15:41:28 +01:00
Nuno Cruces
88b5b409df Use courtney. 2024-07-10 11:12:31 +01:00
Nuno Cruces
51c325bc5b Optimization. 2024-07-10 00:08:59 +01:00
Nuno Cruces
5872224f77 Tests. 2024-07-09 22:13:14 +01:00
Nuno Cruces
7b56989489 Use iter. 2024-07-09 15:54:45 +01:00
Nuno Cruces
bd5be4cde6 Tests. 2024-07-09 14:52:01 +01:00
Nuno Cruces
c19fec1e83 binaryen-version_118. 2024-07-09 00:51:54 +01:00
Nuno Cruces
b5f746aadf Automatically load extensions. (#115) 2024-07-08 12:06:57 +01:00
dependabot[bot]
fff8b1c74f Bump golang.org/x/crypto from 0.24.0 to 0.25.0 (#116)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/crypto/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-06 00:33:50 +01:00
Nuno Cruces
d27da3f390 Fix flaky test. 2024-07-05 00:49:22 +01:00
Nuno Cruces
a1fae26b66 Regular expression extension. (#114) 2024-07-05 00:12:26 +01:00
Nuno Cruces
806cc6677d Updated dependencies. 2024-07-04 19:38:26 +01:00
Nuno Cruces
da6e4d8b86 UUID extension (#113) 2024-07-04 15:28:49 +01:00
Nuno Cruces
72f8ad0f14 Toolchain. 2024-07-03 15:02:58 +01:00
Nuno Cruces
5a4c7a58c4 Refactor CREATE parser. (#111) 2024-07-03 14:06:07 +01:00
Nuno Cruces
90f7e502be Tweaks. 2024-07-02 15:42:20 +01:00
Nuno Cruces
c0b289d000 More BSDs. 2024-06-26 14:56:36 +01:00
Nuno Cruces
a84d905d8c Fix go:linkname for mmap (#107) 2024-06-25 10:31:11 +01:00
Nuno Cruces
aa7edb1848 Tests. 2024-06-21 16:23:56 +01:00
292 changed files with 11306 additions and 3656 deletions

View File

@@ -1,23 +0,0 @@
name: Git LFS pull
description: Cached Git LFS pull.
runs:
using: "composite"
steps:
- name: Create LFS file list
shell: bash
run: git lfs ls-files --long | cut -d ' ' -f1 | sort > .lfs-assets-id
- name: Restore LFS cache
uses: actions/cache@v4
with:
path: .git/lfs/objects
key: lfs-${{ hashFiles('.lfs-assets-id') }}
restore-keys: lfs-
enableCrossOsArchive: true
- name: Git LFS pull
shell: bash
run: |
git lfs pull
git lfs prune

11
.github/actions/vmactions/template.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
name: VM Actions matrix
description: VM Actions matrix template
runs:
using: composite
steps:
- uses: ${VMACTIONS}
with:
usesh: true
copyback: false
run: . ./test.sh

View File

@@ -6,6 +6,10 @@ echo 'set -eu' > test.sh
for p in $(go list ./...); do
dir=".${p#github.com/ncruces/go-sqlite3}"
name="$(basename "$p").test"
(cd ${dir}; go test -c)
[ -f "${dir}/${name}" ] && echo "(cd ${dir}; ./${name} ${TESTFLAGS})" >> test.sh
done
(cd ${dir}; go test -c ${BUILDFLAGS:-})
[ -f "${dir}/${name}" ] && echo "(cd ${dir}; ./${name} ${TESTFLAGS:-})" >> test.sh
done
if [[ -v VMACTIONS ]]; then
envsubst < .github/actions/vmactions/template.yml > .github/actions/vmactions/action.yml
fi

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
echo android ; GOOS=android GOARCH=amd64 go build .
echo darwin ; GOOS=darwin GOARCH=amd64 go build .
echo dragonfly ; GOOS=dragonfly GOARCH=amd64 go build .
echo freebsd ; GOOS=freebsd GOARCH=amd64 go build .
echo illumos ; GOOS=illumos GOARCH=amd64 go build .
echo ios ; GOOS=ios GOARCH=amd64 go build .
echo linux ; GOOS=linux GOARCH=amd64 go build .
echo netbsd ; GOOS=netbsd GOARCH=amd64 go build .
echo openbsd ; GOOS=openbsd GOARCH=amd64 go build .
echo plan9 ; GOOS=plan9 GOARCH=amd64 go build .
echo solaris ; GOOS=solaris GOARCH=amd64 go build .
echo windows ; GOOS=windows GOARCH=amd64 go build .
echo aix ; GOOS=aix GOARCH=ppc64 go build .
echo js ; GOOS=js GOARCH=wasm go build .
echo wasip1 ; GOOS=wasip1 GOARCH=wasm go build .
echo linux-flock ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_flock .
echo linux-noshm ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_noshm .
echo linux-nosys ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_nosys .
echo darwin-flock ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_flock .
echo darwin-noshm ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_noshm .
echo darwin-nosys ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_nosys .
echo windows-nosys ; GOOS=windows GOARCH=amd64 go build -tags sqlite3_nosys .
echo freebsd-nosys ; GOOS=freebsd GOARCH=amd64 go build -tags sqlite3_nosys .
echo solaris-flock ; GOOS=solaris GOARCH=amd64 go build -tags sqlite3_flock .

View File

@@ -1,16 +0,0 @@
name: Cross compile
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Build
run: .github/workflows/cross.sh

View File

@@ -2,29 +2,33 @@
set -euo pipefail
if [[ "$OSTYPE" == "linux"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-linux.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-linux.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-x86_64-linux.tar.gz"
elif [[ "$OSTYPE" == "darwin"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-macos.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-macos.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-arm64-macos.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-arm64-macos.tar.gz"
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0.m-mingw.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-windows.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-windows.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-x86_64-windows.tar.gz"
fi
# Download tools
mkdir -p tools/
[ -d "tools/wasi-sdk"* ] || curl -#L "$WASI_SDK" | tar xzC tools &
[ -d "tools/binaryen-version"* ] || curl -#L "$BINARYEN" | tar xzC tools &
[ -d "tools/wasi-sdk" ] || curl -#L "$WASI_SDK" | tar xzC tools &
[ -d "tools/binaryen" ] || curl -#L "$BINARYEN" | tar xzC tools &
wait
[ -d "tools/wasi-sdk" ] || mv "tools/wasi-sdk"* "tools/wasi-sdk"
[ -d "tools/binaryen" ] || mv "tools/binaryen"* "tools/binaryen"
# Download and build SQLite
sqlite3/download.sh
embed/build.sh
embed/bcw2/build.sh
# Download and build sqlite-createtable-parser
util/vtabutil/parse/download.sh
util/vtabutil/parse/build.sh
util/sql3util/wasm/download.sh
util/sql3util/wasm/build.sh
# Check diffs
git diff --exit-code
git diff --exit-code

View File

@@ -16,16 +16,17 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: ilammy/msvc-dev-cmd@v1
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Build
shell: bash
run: .github/workflows/repro.sh
- uses: actions/attest-build-provenance@v1
- uses: actions/attest-build-provenance@v2
if: matrix.os == 'ubuntu-latest'
with:
subject-path: |
embed/sqlite3.wasm
util/vtabutil/parse/sql3parse_table.wasm
embed/bcw2/bcw2.wasm
util/sql3util/wasm/sql3parse_table.wasm

View File

@@ -2,9 +2,19 @@ name: Test
on:
push:
branches: [ "main" ]
branches: [ 'main' ]
paths:
- '**.go'
- '**.mod'
- '**.wasm'
- '**.yml'
pull_request:
branches: [ "main" ]
branches: [ 'main' ]
paths:
- '**.go'
- '**.mod'
- '**.wasm'
- '**.yml'
workflow_dispatch:
jobs:
@@ -19,9 +29,6 @@ jobs:
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Git LFS pull
uses: ./.github/actions/lfs
- name: Format
run: gofmt -s -w . && git diff --exit-code
if: matrix.os != 'windows-latest'
@@ -46,69 +53,137 @@ jobs:
- name: Test BSD locks
run: go test -v -tags sqlite3_flock ./...
if: matrix.os == 'macos-latest'
if: matrix.os != 'windows-latest'
- name: Test no shared memory
run: go test -v -tags sqlite3_noshm ./...
if: matrix.os == 'ubuntu-latest'
- name: Test dot locks
run: go test -v -tags sqlite3_dotlk ./...
if: matrix.os != 'windows-latest'
- name: Test no locks
run: go test -v -tags sqlite3_nosys ./...
if: matrix.os == 'ubuntu-latest'
- name: Test modules
shell: bash
run: |
go work init .
go work use -r embed gormlite
go test -v ./embed/bcw2/...
- name: Test GORM
shell: bash
run: gormlite/test.sh
if: matrix.os != 'windows-latest'
- name: Collect coverage
run: |
go get -tool github.com/dave/courtney@v0.4.4
go tool courtney
if: |
github.event_name == 'push' &&
matrix.os == 'ubuntu-latest'
- uses: ncruces/go-coverage-report@v0
with:
coverage-file: coverage.out
chart: true
amend: true
if: |
github.event_name == 'push' &&
matrix.os == 'ubuntu-latest'
test-intel:
runs-on: macos-13
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Git LFS pull
uses: ./.github/actions/lfs
- name: Test
run: go test -v ./...
test-bsd:
strategy:
matrix:
os:
- name: freebsd
version: '14.2'
flags: '-test.v'
- name: netbsd
version: '10.1'
flags: '-test.v'
- name: freebsd
arch: arm64
version: '14.2'
flags: '-test.v -test.short'
- name: netbsd
arch: arm64
version: '10.1'
flags: '-test.v -test.short'
- name: openbsd
version: '7.6'
flags: '-test.v -test.short'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Git LFS pull
uses: ./.github/actions/lfs
- name: Build
env:
GOOS: freebsd
TESTFLAGS: '-test.v'
GOOS: ${{ matrix.os.name }}
GOARCH: ${{ matrix.os.arch }}
TESTFLAGS: ${{ matrix.os.flags }}
run: .github/workflows/build-test.sh
- name: Test
uses: cross-platform-actions/action@v0.24.0
uses: cross-platform-actions/action@v0.27.0
with:
operating_system: freebsd
version: '14.0'
operating_system: ${{ matrix.os.name }}
architecture: ${{ matrix.os.arch }}
version: ${{ matrix.os.version }}
shell: bash
run: . ./test.sh
sync_files: runner-to-vm
test-vm:
strategy:
matrix:
os:
- name: dragonfly
action: 'vmactions/dragonflybsd-vm@v1'
tflags: '-test.v'
- name: illumos
action: 'vmactions/omnios-vm@v1'
tflags: '-test.v'
- name: solaris
action: 'vmactions/solaris-vm@v1'
bflags: '-tags sqlite3_dotlk'
tflags: '-test.v'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Build
env:
GOOS: ${{ matrix.os.name }}
BUILDFLAGS: ${{ matrix.os.bflags }}
TESTFLAGS: ${{ matrix.os.tflags }}
VMACTIONS: ${{ matrix.os.action }}
run: .github/workflows/build-test.sh
- name: Test
uses: ./.github/actions/vmactions
test-wasip1:
runs-on: ubuntu-latest
needs: test
steps:
- uses: bytecodealliance/actions/wasmtime/setup@v1
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Set path
run: echo "$(go env GOROOT)/lib/wasm" >> "$GITHUB_PATH"
- name: Test wasmtime
env:
GOOS: wasip1
GOARCH: wasm
GOWASIRUNTIME: wasmtime
GOWASIRUNTIMEARGS: '--env CI=true'
run: go test -v -short -tags sqlite3_dotlk -skip Example ./...
test-qemu:
runs-on: ubuntu-latest
needs: test
@@ -119,23 +194,20 @@ jobs:
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Git LFS pull
uses: ./.github/actions/lfs
- name: Test 386 (32-bit)
run: GOARCH=386 go test -v -short ./...
- name: Test arm64 (compiler)
run: GOARCH=arm64 go test -v -short ./...
- name: Test riscv64 (interpreter)
run: GOARCH=riscv64 go test -v -short ./...
- name: Test s390x (big-endian, z/OS demo)
run: GOARCH=s390x go test -v -short -tags sqlite3_flock ./...
- name: Test ppc64le (interpreter)
run: GOARCH=ppc64le go test -v -short ./...
test-vm:
runs-on: ubuntu-latest
- name: Test s390x (big-endian)
run: GOARCH=s390x go test -v -short -tags sqlite3_dotlk ./...
test-linuxarm:
runs-on: ubuntu-24.04-arm
needs: test
steps:
@@ -143,32 +215,17 @@ jobs:
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Git LFS pull
uses: ./.github/actions/lfs
- name: Test
run: go test -v ./...
- name: Build illumos
env:
GOOS: illumos
TESTFLAGS: '-test.v -test.short'
run: .github/workflows/build-test.sh
test-macintel:
runs-on: macos-13
needs: test
- name: Test illumos
uses: vmactions/omnios-vm@v1
with:
usesh: true
copyback: false
run: . ./test.sh
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Build Solaris
env:
GOOS: solaris
TESTFLAGS: '-test.v -test.short'
run: .github/workflows/build-test.sh
- name: Test Solaris
uses: vmactions/solaris-vm@v1
with:
usesh: true
copyback: false
run: . ./test.sh
continue-on-error: true
- name: Test
run: go test -v ./...

9
.gitignore vendored
View File

@@ -13,4 +13,11 @@
# Dependency directories (remove the comment below to include it)
# vendor/
tools
tools
# Go workspace file
go.work
go.work.sum
# env file
.env

View File

@@ -1,4 +1,4 @@
# Go bindings to SQLite using Wazero
# Go bindings to SQLite using wazero
[![Go Reference](https://pkg.go.dev/badge/image)](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
[![Go Report](https://goreportcard.com/badge/github.com/ncruces/go-sqlite3)](https://goreportcard.com/report/github.com/ncruces/go-sqlite3)
@@ -10,7 +10,21 @@ as well as direct access to most of the [C SQLite API](https://sqlite.org/cintro
It wraps a [Wasm](https://webassembly.org/) [build](embed/) of SQLite,
and uses [wazero](https://wazero.io/) as the runtime.\
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ runtime dependencies [^1].
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ direct dependencies.
### Getting started
Using the [`database/sql`](https://pkg.go.dev/database/sql) driver:
```go
import "database/sql"
import _ "github.com/ncruces/go-sqlite3/driver"
import _ "github.com/ncruces/go-sqlite3/embed"
var version string
db, _ := sql.Open("sqlite3", "file:demo.db")
db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
```
### Packages
@@ -27,39 +41,6 @@ Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ run
- [`github.com/ncruces/go-sqlite3/gormlite`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/gormlite)
provides a [GORM](https://gorm.io) driver.
### Extensions
- [`github.com/ncruces/go-sqlite3/ext/array`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/array)
provides the [`array`](https://sqlite.org/carray.html) table-valued function.
- [`github.com/ncruces/go-sqlite3/ext/blobio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blobio)
simplifies [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html).
- [`github.com/ncruces/go-sqlite3/ext/bloom`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/bloom)
provides a [Bloom filter](https://github.com/nalgeon/sqlean/issues/27#issuecomment-1002267134) virtual table.
- [`github.com/ncruces/go-sqlite3/ext/csv`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/csv)
reads [comma-separated values](https://sqlite.org/csv.html).
- [`github.com/ncruces/go-sqlite3/ext/fileio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/fileio)
reads, writes and lists files.
- [`github.com/ncruces/go-sqlite3/ext/hash`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/hash)
provides cryptographic hash functions.
- [`github.com/ncruces/go-sqlite3/ext/lines`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/lines)
reads data [line-by-line](https://github.com/asg017/sqlite-lines).
- [`github.com/ncruces/go-sqlite3/ext/pivot`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/pivot)
creates [pivot tables](https://github.com/jakethaw/pivot_vtab).
- [`github.com/ncruces/go-sqlite3/ext/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/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)
wraps a VFS to offer encryption at rest.
- [`github.com/ncruces/go-sqlite3/vfs/memdb`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb)
implements an in-memory VFS.
- [`github.com/ncruces/go-sqlite3/vfs/readervfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs)
implements a VFS for immutable databases.
### Advanced features
- [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html)
@@ -72,7 +53,11 @@ Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ run
- [math functions](https://sqlite.org/lang_mathfunc.html)
- [full-text search](https://sqlite.org/fts5.html)
- [geospatial search](https://sqlite.org/geopoly.html)
- [Unicode support](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
- [statistics functions](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
- [encryption at rest](vfs/adiantum/README.md)
- [many extensions](ext/README.md)
- [custom VFSes](vfs/README.md#custom-vfses)
- [and more…](embed/README.md)
### Caveats
@@ -89,9 +74,10 @@ This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
Every commit is [tested](.github/workflows/test.yml) on
Linux (amd64/arm64/386/riscv64/s390x), macOS (amd64/arm64),
Windows (amd64), FreeBSD (amd64), illumos (amd64), and Solaris (amd64).
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) on
Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64),
Windows (amd64), FreeBSD (amd64/arm64), OpenBSD (amd64), NetBSD (amd64/arm64),
DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64).
The Go VFS is tested by running SQLite's
[mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c).
@@ -104,12 +90,20 @@ Perfomance of the [`database/sql`](https://pkg.go.dev/database/sql) driver is
The Wasm and VFS layers are also tested by running SQLite's
[speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c).
### FAQ, issues, new features
For questions, please see [Discussions](https://github.com/ncruces/go-sqlite3/discussions/categories/q-a).
Also, post there if you used this driver for something interesting
([_"Show and tell"_](https://github.com/ncruces/go-sqlite3/discussions/categories/show-and-tell)),
have an [idea](https://github.com/ncruces/go-sqlite3/discussions/categories/ideas)…
The [Issue](https://github.com/ncruces/go-sqlite3/issues) tracker is for bugs we want fixed,
and features we're working on, planning to work on, or asking for help with.
### Alternatives
- [`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite)
- [`crawshaw.io/sqlite`](https://pkg.go.dev/crawshaw.io/sqlite)
- [`github.com/mattn/go-sqlite3`](https://pkg.go.dev/github.com/mattn/go-sqlite3)
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)
[^1]: anything else you find in `go.mod` is either a test dependency,
or needed by one of the extensions.

View File

@@ -5,8 +5,8 @@ package sqlite3
// https://sqlite.org/c3ref/backup.html
type Backup struct {
c *Conn
handle uint32
otherc uint32
handle ptr_t
otherc ptr_t
}
// Backup backs up srcDB on the src connection to the "main" database in dstURI.
@@ -61,7 +61,7 @@ func (src *Conn) BackupInit(srcDB, dstURI string) (*Backup, error) {
return src.backupInit(dst, "main", src.handle, srcDB)
}
func (c *Conn) backupInit(dst uint32, dstName string, src uint32, srcName string) (*Backup, error) {
func (c *Conn) backupInit(dst ptr_t, dstName string, src ptr_t, srcName string) (*Backup, error) {
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("sqlite3_backup_init",
uint64(dst), uint64(dstPtr),
uint64(src), uint64(srcPtr))
if r == 0 {
ptr := ptr_t(c.call("sqlite3_backup_init",
stk_t(dst), stk_t(dstPtr),
stk_t(src), stk_t(srcPtr)))
if ptr == 0 {
defer c.closeDB(other)
r = c.call("sqlite3_errcode", uint64(dst))
return nil, c.sqlite.error(r, dst)
rc := res_t(c.call("sqlite3_errcode", stk_t(dst)))
return nil, c.sqlite.error(rc, dst)
}
return &Backup{
c: c,
otherc: other,
handle: uint32(r),
handle: ptr,
}, nil
}
@@ -97,10 +97,10 @@ func (b *Backup) Close() error {
return nil
}
r := b.c.call("sqlite3_backup_finish", uint64(b.handle))
rc := res_t(b.c.call("sqlite3_backup_finish", stk_t(b.handle)))
b.c.closeDB(b.otherc)
b.handle = 0
return b.c.error(r)
return b.c.error(rc)
}
// Step copies up to nPage pages between the source and destination databases.
@@ -108,11 +108,11 @@ func (b *Backup) Close() error {
//
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
func (b *Backup) Step(nPage int) (done bool, err error) {
r := b.c.call("sqlite3_backup_step", uint64(b.handle), uint64(nPage))
if r == _DONE {
rc := res_t(b.c.call("sqlite3_backup_step", stk_t(b.handle), stk_t(nPage)))
if rc == _DONE {
return true, nil
}
return false, b.c.error(r)
return false, b.c.error(rc)
}
// Remaining returns the number of pages still to be backed up
@@ -120,8 +120,8 @@ func (b *Backup) Step(nPage int) (done bool, err error) {
//
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backupremaining
func (b *Backup) Remaining() int {
r := b.c.call("sqlite3_backup_remaining", uint64(b.handle))
return int(int32(r))
n := int32(b.c.call("sqlite3_backup_remaining", stk_t(b.handle)))
return int(n)
}
// PageCount returns the total number of pages in the source database
@@ -129,6 +129,6 @@ func (b *Backup) Remaining() int {
//
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backuppagecount
func (b *Backup) PageCount() int {
r := b.c.call("sqlite3_backup_pagecount", uint64(b.handle))
return int(int32(r))
n := int32(b.c.call("sqlite3_backup_pagecount", stk_t(b.handle)))
return int(n)
}

93
blob.go
View File

@@ -20,7 +20,9 @@ type Blob struct {
c *Conn
bytes int64
offset int64
handle uint32
handle ptr_t
bufptr ptr_t
buflen int64
}
var _ io.ReadWriteSeeker = &Blob{}
@@ -29,29 +31,29 @@ var _ io.ReadWriteSeeker = &Blob{}
//
// https://sqlite.org/c3ref/blob_open.html
func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob, error) {
c.checkInterrupt()
defer c.arena.mark()()
blobPtr := c.arena.new(ptrlen)
dbPtr := c.arena.string(db)
tablePtr := c.arena.string(table)
columnPtr := c.arena.string(column)
var flags uint64
var flags int32
if write {
flags = 1
}
r := c.call("sqlite3_blob_open", uint64(c.handle),
uint64(dbPtr), uint64(tablePtr), uint64(columnPtr),
uint64(row), flags, uint64(blobPtr))
c.checkInterrupt(c.handle)
rc := res_t(c.call("sqlite3_blob_open", stk_t(c.handle),
stk_t(dbPtr), stk_t(tablePtr), stk_t(columnPtr),
stk_t(row), stk_t(flags), stk_t(blobPtr)))
if err := c.error(r); err != nil {
if err := c.error(rc); err != nil {
return nil, err
}
blob := Blob{c: c}
blob.handle = util.ReadUint32(c.mod, blobPtr)
blob.bytes = int64(c.call("sqlite3_blob_bytes", uint64(blob.handle)))
blob.handle = util.Read32[ptr_t](c.mod, blobPtr)
blob.bytes = int64(int32(c.call("sqlite3_blob_bytes", stk_t(blob.handle))))
return &blob, nil
}
@@ -65,10 +67,10 @@ func (b *Blob) Close() error {
return nil
}
r := b.c.call("sqlite3_blob_close", uint64(b.handle))
rc := res_t(b.c.call("sqlite3_blob_close", stk_t(b.handle)))
b.c.free(b.bufptr)
b.handle = 0
return b.c.error(r)
return b.c.error(rc)
}
// Size returns the size of the BLOB in bytes.
@@ -86,18 +88,19 @@ func (b *Blob) Read(p []byte) (n int, err error) {
return 0, io.EOF
}
avail := b.bytes - b.offset
want := int64(len(p))
avail := b.bytes - b.offset
if want > avail {
want = avail
}
if want > b.buflen {
b.bufptr = b.c.realloc(b.bufptr, want)
b.buflen = want
}
defer b.c.arena.mark()()
ptr := b.c.arena.new(uint64(want))
r := b.c.call("sqlite3_blob_read", uint64(b.handle),
uint64(ptr), uint64(want), uint64(b.offset))
err = b.c.error(r)
rc := res_t(b.c.call("sqlite3_blob_read", stk_t(b.handle),
stk_t(b.bufptr), stk_t(want), stk_t(b.offset)))
err = b.c.error(rc)
if err != nil {
return 0, err
}
@@ -106,7 +109,7 @@ func (b *Blob) Read(p []byte) (n int, err error) {
err = io.EOF
}
copy(p, util.View(b.c.mod, ptr, uint64(want)))
copy(p, util.View(b.c.mod, b.bufptr, want))
return int(want), err
}
@@ -123,19 +126,20 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
if want > avail {
want = avail
}
defer b.c.arena.mark()()
ptr := b.c.arena.new(uint64(want))
if want > b.buflen {
b.bufptr = b.c.realloc(b.bufptr, want)
b.buflen = want
}
for want > 0 {
r := b.c.call("sqlite3_blob_read", uint64(b.handle),
uint64(ptr), uint64(want), uint64(b.offset))
err = b.c.error(r)
rc := res_t(b.c.call("sqlite3_blob_read", stk_t(b.handle),
stk_t(b.bufptr), stk_t(want), stk_t(b.offset)))
err = b.c.error(rc)
if err != nil {
return n, err
}
mem := util.View(b.c.mod, ptr, uint64(want))
mem := util.View(b.c.mod, b.bufptr, want)
m, err := w.Write(mem[:want])
b.offset += int64(m)
n += int64(m)
@@ -143,6 +147,7 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
return n, err
}
if int64(m) != want {
// notest // Write misbehaving
return n, io.ErrShortWrite
}
@@ -158,12 +163,16 @@ func (b *Blob) WriteTo(w io.Writer) (n int64, err error) {
//
// https://sqlite.org/c3ref/blob_write.html
func (b *Blob) Write(p []byte) (n int, err error) {
defer b.c.arena.mark()()
ptr := b.c.arena.bytes(p)
want := int64(len(p))
if want > b.buflen {
b.bufptr = b.c.realloc(b.bufptr, want)
b.buflen = want
}
util.WriteBytes(b.c.mod, b.bufptr, p)
r := b.c.call("sqlite3_blob_write", uint64(b.handle),
uint64(ptr), uint64(len(p)), uint64(b.offset))
err = b.c.error(r)
rc := res_t(b.c.call("sqlite3_blob_write", stk_t(b.handle),
stk_t(b.bufptr), stk_t(want), stk_t(b.offset)))
err = b.c.error(rc)
if err != nil {
return 0, err
}
@@ -186,17 +195,18 @@ func (b *Blob) ReadFrom(r io.Reader) (n int64, err error) {
if want < 1 {
want = 1
}
defer b.c.arena.mark()()
ptr := b.c.arena.new(uint64(want))
if want > b.buflen {
b.bufptr = b.c.realloc(b.bufptr, want)
b.buflen = want
}
for {
mem := util.View(b.c.mod, ptr, uint64(want))
mem := util.View(b.c.mod, b.bufptr, want)
m, err := r.Read(mem[:want])
if m > 0 {
r := b.c.call("sqlite3_blob_write", uint64(b.handle),
uint64(ptr), uint64(m), uint64(b.offset))
err := b.c.error(r)
rc := res_t(b.c.call("sqlite3_blob_write", stk_t(b.handle),
stk_t(b.bufptr), stk_t(m), stk_t(b.offset)))
err := b.c.error(rc)
if err != nil {
return n, err
}
@@ -243,8 +253,9 @@ func (b *Blob) Seek(offset int64, whence int) (int64, error) {
//
// https://sqlite.org/c3ref/blob_reopen.html
func (b *Blob) Reopen(row int64) error {
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.c.checkInterrupt(b.c.handle)
err := b.c.error(res_t(b.c.call("sqlite3_blob_reopen", stk_t(b.handle), stk_t(row))))
b.bytes = int64(int32(b.c.call("sqlite3_blob_bytes", stk_t(b.handle))))
b.offset = 0
return err
}

309
config.go
View File

@@ -2,9 +2,13 @@ package sqlite3
import (
"context"
"fmt"
"strconv"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/vfs"
)
// Config makes configuration changes to a database connection.
@@ -14,10 +18,21 @@ import (
//
// 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)
if op < DBCONFIG_ENABLE_FKEY || op > DBCONFIG_REVERSE_SCANORDER {
return false, MISUSE
}
var flag int
// We need to call sqlite3_db_config, a variadic function.
// We only support the `int int*` variants.
// The int is a three-valued bool: -1 queries, 0/1 sets false/true.
// The int* points to where new state will be written to.
// The vararg is a pointer to an array containing these arguments:
// an int and an int* pointing to that int.
defer c.arena.mark()()
argsPtr := c.arena.new(intlen + ptrlen)
var flag int32
switch {
case len(arg) == 0:
flag = -1
@@ -25,56 +40,162 @@ func (c *Conn) Config(op DBConfig, arg ...bool) (bool, error) {
flag = 1
}
util.WriteUint32(c.mod, argsPtr+0*ptrlen, uint32(flag))
util.WriteUint32(c.mod, argsPtr+1*ptrlen, argsPtr)
util.Write32(c.mod, argsPtr+0*ptrlen, flag)
util.Write32(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)
rc := res_t(c.call("sqlite3_db_config", stk_t(c.handle),
stk_t(op), stk_t(argsPtr)))
return util.ReadBool(c.mod, argsPtr), c.error(rc)
}
// 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
var enable int32
if cb != nil {
enable = 1
}
r := c.call("sqlite3_config_log_go", enable)
if err := c.error(r); err != nil {
rc := res_t(c.call("sqlite3_config_log_go", stk_t(enable)))
if err := c.error(rc); err != nil {
return err
}
c.log = cb
return nil
}
func logCallback(ctx context.Context, mod api.Module, _, iCode, zMsg uint32) {
func logCallback(ctx context.Context, mod api.Module, _ ptr_t, iCode res_t, zMsg ptr_t) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.log != nil {
msg := util.ReadString(mod, zMsg, _MAX_LENGTH)
c.log(xErrorCode(iCode), msg)
}
}
// Log writes a message into the error log established by [Conn.ConfigLog].
//
// https://sqlite.org/c3ref/log.html
func (c *Conn) Log(code ExtendedErrorCode, format string, a ...any) {
if c.log != nil {
c.log(code, fmt.Sprintf(format, a...))
}
}
// FileControl allows low-level control of database files.
// Only a subset of opcodes are supported.
//
// https://sqlite.org/c3ref/file_control.html
func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, error) {
defer c.arena.mark()()
ptr := c.arena.new(max(ptrlen, intlen))
var schemaPtr ptr_t
if schema != "" {
schemaPtr = c.arena.string(schema)
}
var rc res_t
var ret any
switch op {
default:
return nil, MISUSE
case FCNTL_RESET_CACHE:
rc = res_t(c.call("sqlite3_file_control",
stk_t(c.handle), stk_t(schemaPtr),
stk_t(op), 0))
case FCNTL_PERSIST_WAL, FCNTL_POWERSAFE_OVERWRITE:
var flag int32
switch {
case len(arg) == 0:
flag = -1
case arg[0]:
flag = 1
}
util.Write32(c.mod, ptr, flag)
rc = res_t(c.call("sqlite3_file_control",
stk_t(c.handle), stk_t(schemaPtr),
stk_t(op), stk_t(ptr)))
ret = util.ReadBool(c.mod, ptr)
case FCNTL_CHUNK_SIZE:
util.Write32(c.mod, ptr, int32(arg[0].(int)))
rc = res_t(c.call("sqlite3_file_control",
stk_t(c.handle), stk_t(schemaPtr),
stk_t(op), stk_t(ptr)))
case FCNTL_RESERVE_BYTES:
bytes := -1
if len(arg) > 0 {
bytes = arg[0].(int)
}
util.Write32(c.mod, ptr, int32(bytes))
rc = res_t(c.call("sqlite3_file_control",
stk_t(c.handle), stk_t(schemaPtr),
stk_t(op), stk_t(ptr)))
ret = int(util.Read32[int32](c.mod, ptr))
case FCNTL_DATA_VERSION:
rc = res_t(c.call("sqlite3_file_control",
stk_t(c.handle), stk_t(schemaPtr),
stk_t(op), stk_t(ptr)))
ret = util.Read32[uint32](c.mod, ptr)
case FCNTL_LOCKSTATE:
rc = res_t(c.call("sqlite3_file_control",
stk_t(c.handle), stk_t(schemaPtr),
stk_t(op), stk_t(ptr)))
ret = util.Read32[vfs.LockLevel](c.mod, ptr)
case FCNTL_VFS_POINTER:
rc = res_t(c.call("sqlite3_file_control",
stk_t(c.handle), stk_t(schemaPtr),
stk_t(op), stk_t(ptr)))
if rc == _OK {
const zNameOffset = 16
ptr = util.Read32[ptr_t](c.mod, ptr)
ptr = util.Read32[ptr_t](c.mod, ptr+zNameOffset)
name := util.ReadString(c.mod, ptr, _MAX_NAME)
ret = vfs.Find(name)
}
case FCNTL_FILE_POINTER, FCNTL_JOURNAL_POINTER:
rc = res_t(c.call("sqlite3_file_control",
stk_t(c.handle), stk_t(schemaPtr),
stk_t(op), stk_t(ptr)))
if rc == _OK {
const fileHandleOffset = 4
ptr = util.Read32[ptr_t](c.mod, ptr)
ptr = util.Read32[ptr_t](c.mod, ptr+fileHandleOffset)
ret = util.GetHandle(c.ctx, ptr)
}
}
if err := c.error(rc); err != nil {
return nil, err
}
return ret, nil
}
// 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))
v := int32(c.call("sqlite3_limit", stk_t(c.handle), stk_t(id), stk_t(value)))
return int(v)
}
// 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
func (c *Conn) SetAuthorizer(cb func(action AuthorizerActionCode, name3rd, name4th, schema, inner string) AuthorizerReturnCode) error {
var enable int32
if cb != nil {
enable = 1
}
r := c.call("sqlite3_set_authorizer_go", uint64(c.handle), enable)
if err := c.error(r); err != nil {
rc := res_t(c.call("sqlite3_set_authorizer_go", stk_t(c.handle), stk_t(enable)))
if err := c.error(rc); err != nil {
return err
}
c.authorizer = cb
@@ -82,9 +203,9 @@ func (c *Conn) SetAuthorizer(cb func(action AuthorizerActionCode, name3rd, name4
}
func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action AuthorizerActionCode, zName3rd, zName4th, zSchema, zNameInner uint32) (rc AuthorizerReturnCode) {
func authorizerCallback(ctx context.Context, mod api.Module, pDB ptr_t, action AuthorizerActionCode, zName3rd, zName4th, zSchema, zInner ptr_t) (rc AuthorizerReturnCode) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.authorizer != nil {
var name3rd, name4th, schema, nameInner string
var name3rd, name4th, schema, inner string
if zName3rd != 0 {
name3rd = util.ReadString(mod, zName3rd, _MAX_NAME)
}
@@ -94,52 +215,90 @@ func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action
if zSchema != 0 {
schema = util.ReadString(mod, zSchema, _MAX_NAME)
}
if zNameInner != 0 {
nameInner = util.ReadString(mod, zNameInner, _MAX_NAME)
if zInner != 0 {
inner = util.ReadString(mod, zInner, _MAX_NAME)
}
rc = c.authorizer(action, name3rd, name4th, schema, nameInner)
rc = c.authorizer(action, name3rd, name4th, schema, inner)
}
return rc
}
// WalCheckpoint checkpoints a WAL database.
// Trace registers a trace callback function against the database connection.
//
// https://sqlite.org/c3ref/trace_v2.html
func (c *Conn) Trace(mask TraceEvent, cb func(evt TraceEvent, arg1 any, arg2 any) error) error {
rc := res_t(c.call("sqlite3_trace_go", stk_t(c.handle), stk_t(mask)))
if err := c.error(rc); err != nil {
return err
}
c.trace = cb
return nil
}
func traceCallback(ctx context.Context, mod api.Module, evt TraceEvent, pDB, pArg1, pArg2 ptr_t) (rc res_t) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.trace != nil {
var arg1, arg2 any
if evt == TRACE_CLOSE {
arg1 = c
} else {
for _, s := range c.stmts {
if pArg1 == s.handle {
arg1 = s
switch evt {
case TRACE_STMT:
arg2 = s.SQL()
case TRACE_PROFILE:
arg2 = util.Read64[int64](mod, pArg2)
}
break
}
}
}
if arg1 != nil {
_, rc = errorCode(c.trace(evt, arg1, arg2), ERROR)
}
}
return rc
}
// WALCheckpoint checkpoints a WAL database.
//
// https://sqlite.org/c3ref/wal_checkpoint_v2.html
func (c *Conn) WalCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt int, err error) {
func (c *Conn) WALCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt int, err error) {
defer c.arena.mark()()
nLogPtr := c.arena.new(ptrlen)
nCkptPtr := c.arena.new(ptrlen)
schemaPtr := c.arena.string(schema)
r := c.call("sqlite3_wal_checkpoint_v2",
uint64(c.handle), uint64(schemaPtr), uint64(mode),
uint64(nLogPtr), uint64(nCkptPtr))
nLog = int(int32(util.ReadUint32(c.mod, nLogPtr)))
nCkpt = int(int32(util.ReadUint32(c.mod, nCkptPtr)))
return nLog, nCkpt, c.error(r)
rc := res_t(c.call("sqlite3_wal_checkpoint_v2",
stk_t(c.handle), stk_t(schemaPtr), stk_t(mode),
stk_t(nLogPtr), stk_t(nCkptPtr)))
nLog = int(util.Read32[int32](c.mod, nLogPtr))
nCkpt = int(util.Read32[int32](c.mod, nCkptPtr))
return nLog, nCkpt, c.error(rc)
}
// WalAutoCheckpoint configures WAL auto-checkpoints.
// WALAutoCheckpoint configures WAL auto-checkpoints.
//
// https://sqlite.org/c3ref/wal_autocheckpoint.html
func (c *Conn) WalAutoCheckpoint(pages int) error {
r := c.call("sqlite3_wal_autocheckpoint", uint64(c.handle), uint64(pages))
return c.error(r)
func (c *Conn) WALAutoCheckpoint(pages int) error {
rc := res_t(c.call("sqlite3_wal_autocheckpoint", stk_t(c.handle), stk_t(pages)))
return c.error(rc)
}
// WalHook registers a callback function to be invoked
// WALHook registers a callback function to be invoked
// each time data is committed to a database in WAL mode.
//
// https://sqlite.org/c3ref/wal_hook.html
func (c *Conn) WalHook(cb func(db *Conn, schema string, pages int) error) {
var enable uint64
func (c *Conn) WALHook(cb func(db *Conn, schema string, pages int) error) {
var enable int32
if cb != nil {
enable = 1
}
c.call("sqlite3_wal_hook_go", uint64(c.handle), enable)
c.call("sqlite3_wal_hook_go", stk_t(c.handle), stk_t(enable))
c.wal = cb
}
func walCallback(ctx context.Context, mod api.Module, _, pDB, zSchema uint32, pages int32) (rc uint32) {
func walCallback(ctx context.Context, mod api.Module, _, pDB, zSchema ptr_t, pages int32) (rc res_t) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.wal != nil {
schema := util.ReadString(mod, zSchema, _MAX_NAME)
err := c.wal(c, schema, int(pages))
@@ -152,13 +311,73 @@ func walCallback(ctx context.Context, mod api.Module, _, pDB, zSchema uint32, pa
//
// https://sqlite.org/c3ref/autovacuum_pages.html
func (c *Conn) AutoVacuumPages(cb func(schema string, dbPages, freePages, bytesPerPage uint) uint) error {
funcPtr := util.AddHandle(c.ctx, cb)
r := c.call("sqlite3_autovacuum_pages_go", uint64(c.handle), uint64(funcPtr))
return c.error(r)
var funcPtr ptr_t
if cb != nil {
funcPtr = util.AddHandle(c.ctx, cb)
}
rc := res_t(c.call("sqlite3_autovacuum_pages_go", stk_t(c.handle), stk_t(funcPtr)))
return c.error(rc)
}
func autoVacuumCallback(ctx context.Context, mod api.Module, pApp, zSchema, nDbPage, nFreePage, nBytePerPage uint32) uint32 {
func autoVacuumCallback(ctx context.Context, mod api.Module, pApp, zSchema ptr_t, nDbPage, nFreePage, nBytePerPage uint32) uint32 {
fn := util.GetHandle(ctx, pApp).(func(schema string, dbPages, freePages, bytesPerPage uint) uint)
schema := util.ReadString(mod, zSchema, _MAX_NAME)
return uint32(fn(schema, uint(nDbPage), uint(nFreePage), uint(nBytePerPage)))
}
// SoftHeapLimit imposes a soft limit on heap size.
//
// https://sqlite.org/c3ref/hard_heap_limit64.html
func (c *Conn) SoftHeapLimit(n int64) int64 {
return int64(c.call("sqlite3_soft_heap_limit64", stk_t(n)))
}
// HardHeapLimit imposes a hard limit on heap size.
//
// https://sqlite.org/c3ref/hard_heap_limit64.html
func (c *Conn) HardHeapLimit(n int64) int64 {
return int64(c.call("sqlite3_hard_heap_limit64", stk_t(n)))
}
// EnableChecksums enables checksums on a database.
//
// https://sqlite.org/cksumvfs.html
func (c *Conn) EnableChecksums(schema string) error {
r, err := c.FileControl(schema, FCNTL_RESERVE_BYTES)
if err != nil {
return err
}
if r == 8 {
// Correct value, enabled.
return nil
}
if r == 0 {
// Default value, enable.
_, err = c.FileControl(schema, FCNTL_RESERVE_BYTES, 8)
if err != nil {
return err
}
r, err = c.FileControl(schema, FCNTL_RESERVE_BYTES)
if err != nil {
return err
}
}
if r != 8 {
// Invalid value.
return util.ErrorString("sqlite3: reserve bytes must be 8, is: " + strconv.Itoa(r.(int)))
}
// VACUUM the database.
if schema != "" {
err = c.Exec(`VACUUM ` + QuoteIdentifier(schema))
} else {
err = c.Exec(`VACUUM`)
}
if err != nil {
return err
}
// Checkpoint the WAL.
_, _, err = c.WALCheckpoint(schema, CHECKPOINT_RESTART)
return err
}

312
conn.go
View File

@@ -4,13 +4,16 @@ import (
"context"
"fmt"
"math"
"math/rand"
"net/url"
"runtime"
"strings"
"time"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/vfs"
"github.com/tetratelabs/wazero/api"
)
// Conn is a database connection handle.
@@ -22,27 +25,37 @@ type Conn struct {
interrupt context.Context
pending *Stmt
busy func(int) bool
stmts []*Stmt
busy func(context.Context, int) bool
log func(xErrorCode, string)
collation func(*Conn, string)
wal func(*Conn, string, int) error
trace func(TraceEvent, any, any) error
authorizer func(AuthorizerActionCode, string, string, string, string) AuthorizerReturnCode
update func(AuthorizerActionCode, string, string, int64)
commit func() bool
rollback func()
wal func(*Conn, string, int) error
arena arena
handle uint32
busy1st time.Time
busylst time.Time
arena arena
handle ptr_t
}
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE], [OPEN_URI] and [OPEN_NOFOLLOW].
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE] and [OPEN_URI].
func Open(filename string) (*Conn, error) {
return newConn(filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI|OPEN_NOFOLLOW)
return newConn(context.Background(), filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
}
// OpenContext is like [Open] but includes a context,
// which is used to interrupt the process of opening the connectiton.
func OpenContext(ctx context.Context, filename string) (*Conn, error) {
return newConn(ctx, filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
}
// OpenFlags opens an SQLite database file as specified by the filename argument.
//
// If none of the required flags is used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
// If none of the required flags are used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
// If a URI filename is used, PRAGMA statements to execute can be specified using "_pragma":
//
// sqlite3.Open("file:demo.db?_pragma=busy_timeout(10000)")
@@ -52,46 +65,58 @@ func OpenFlags(filename string, flags OpenFlag) (*Conn, error) {
if flags&(OPEN_READONLY|OPEN_READWRITE|OPEN_CREATE) == 0 {
flags |= OPEN_READWRITE | OPEN_CREATE
}
return newConn(filename, flags)
return newConn(context.Background(), filename, flags)
}
type connKey struct{}
type connKey = util.ConnKey
func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
sqlite, err := instantiateSQLite()
func newConn(ctx context.Context, filename string, flags OpenFlag) (ret *Conn, _ error) {
err := ctx.Err()
if err != nil {
return nil, err
}
c := &Conn{interrupt: ctx}
c.sqlite, err = instantiateSQLite()
if err != nil {
return nil, err
}
defer func() {
if conn == nil {
sqlite.close()
if ret == nil {
c.Close()
c.sqlite.close()
} else {
c.interrupt = context.Background()
}
}()
c := &Conn{sqlite: sqlite}
c.arena = c.newArena(1024)
c.ctx = context.WithValue(c.ctx, connKey{}, c)
c.arena = c.newArena()
c.handle, err = c.openDB(filename, flags)
if err == nil {
err = initExtensions(c)
}
if err != nil {
return nil, err
}
return c, nil
}
func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
func (c *Conn) openDB(filename string, flags OpenFlag) (ptr_t, error) {
defer c.arena.mark()()
connPtr := c.arena.new(ptrlen)
namePtr := c.arena.string(filename)
flags |= OPEN_EXRESCODE
r := c.call("sqlite3_open_v2", uint64(namePtr), uint64(connPtr), uint64(flags), 0)
rc := res_t(c.call("sqlite3_open_v2", stk_t(namePtr), stk_t(connPtr), stk_t(flags), 0))
handle := util.ReadUint32(c.mod, connPtr)
if err := c.sqlite.error(r, handle); err != nil {
handle := util.Read32[ptr_t](c.mod, connPtr)
if err := c.sqlite.error(rc, handle); err != nil {
c.closeDB(handle)
return 0, err
}
c.call("sqlite3_progress_handler_go", stk_t(handle), 100)
if flags|OPEN_URI != 0 && strings.HasPrefix(filename, "file:") {
var pragmas strings.Builder
if _, after, ok := strings.Cut(filename, "?"); ok {
@@ -103,22 +128,22 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
}
}
if pragmas.Len() != 0 {
c.checkInterrupt(handle)
pragmaPtr := c.arena.string(pragmas.String())
r := c.call("sqlite3_exec", uint64(handle), uint64(pragmaPtr), 0, 0, 0)
if err := c.sqlite.error(r, handle, pragmas.String()); err != nil {
rc := res_t(c.call("sqlite3_exec", stk_t(handle), stk_t(pragmaPtr), 0, 0, 0))
if err := c.sqlite.error(rc, handle, pragmas.String()); err != nil {
err = fmt.Errorf("sqlite3: invalid _pragma: %w", err)
c.closeDB(handle)
return 0, err
}
}
}
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
return handle, nil
}
func (c *Conn) closeDB(handle uint32) {
r := c.call("sqlite3_close_v2", uint64(handle))
if err := c.sqlite.error(r, handle); err != nil {
func (c *Conn) closeDB(handle ptr_t) {
rc := res_t(c.call("sqlite3_close_v2", stk_t(handle)))
if err := c.sqlite.error(rc, handle); err != nil {
panic(err)
}
}
@@ -140,8 +165,8 @@ func (c *Conn) Close() error {
c.pending.Close()
c.pending = nil
r := c.call("sqlite3_close", uint64(c.handle))
if err := c.error(r); err != nil {
rc := res_t(c.call("sqlite3_close", stk_t(c.handle)))
if err := c.error(rc); err != nil {
return err
}
@@ -154,12 +179,12 @@ func (c *Conn) Close() error {
//
// https://sqlite.org/c3ref/exec.html
func (c *Conn) Exec(sql string) error {
c.checkInterrupt()
defer c.arena.mark()()
sqlPtr := c.arena.string(sql)
r := c.call("sqlite3_exec", uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
return c.error(r, sql)
c.checkInterrupt(c.handle)
rc := res_t(c.call("sqlite3_exec", stk_t(c.handle), stk_t(sqlPtr), 0, 0, 0))
return c.error(rc, sql)
}
// Prepare calls [Conn.PrepareFlags] with no flags.
@@ -183,22 +208,24 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
tailPtr := c.arena.new(ptrlen)
sqlPtr := c.arena.string(sql)
r := c.call("sqlite3_prepare_v3", uint64(c.handle),
uint64(sqlPtr), uint64(len(sql)+1), uint64(flags),
uint64(stmtPtr), uint64(tailPtr))
c.checkInterrupt(c.handle)
rc := res_t(c.call("sqlite3_prepare_v3", stk_t(c.handle),
stk_t(sqlPtr), stk_t(len(sql)+1), stk_t(flags),
stk_t(stmtPtr), stk_t(tailPtr)))
stmt = &Stmt{c: c}
stmt.handle = util.ReadUint32(c.mod, stmtPtr)
if sql := sql[util.ReadUint32(c.mod, tailPtr)-sqlPtr:]; sql != "" {
stmt.handle = util.Read32[ptr_t](c.mod, stmtPtr)
if sql := sql[util.Read32[ptr_t](c.mod, tailPtr)-sqlPtr:]; sql != "" {
tail = sql
}
if err := c.error(r, sql); err != nil {
if err := c.error(rc, sql); err != nil {
return nil, "", err
}
if stmt.handle == 0 {
return nil, "", nil
}
c.stmts = append(c.stmts, stmt)
return stmt, tail, nil
}
@@ -206,9 +233,7 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
//
// 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)
ptr := ptr_t(c.call("sqlite3_db_name", stk_t(c.handle), stk_t(n)))
if ptr == 0 {
return ""
}
@@ -219,35 +244,34 @@ func (c *Conn) DBName(n int) string {
//
// https://sqlite.org/c3ref/db_filename.html
func (c *Conn) Filename(schema string) *vfs.Filename {
var ptr uint32
var ptr ptr_t
if schema != "" {
defer c.arena.mark()()
ptr = c.arena.string(schema)
}
r := c.call("sqlite3_db_filename", uint64(c.handle), uint64(ptr))
return vfs.OpenFilename(c.ctx, c.mod, uint32(r), vfs.OPEN_MAIN_DB)
ptr = ptr_t(c.call("sqlite3_db_filename", stk_t(c.handle), stk_t(ptr)))
return vfs.GetFilename(c.ctx, c.mod, ptr, vfs.OPEN_MAIN_DB)
}
// 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
var ptr ptr_t
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
b := int32(c.call("sqlite3_db_readonly", stk_t(c.handle), stk_t(ptr)))
return b > 0, b < 0
}
// GetAutocommit tests the connection for auto-commit mode.
//
// https://sqlite.org/c3ref/get_autocommit.html
func (c *Conn) GetAutocommit() bool {
r := c.call("sqlite3_get_autocommit", uint64(c.handle))
return r != 0
b := int32(c.call("sqlite3_get_autocommit", stk_t(c.handle)))
return b != 0
}
// LastInsertRowID returns the rowid of the most recent successful INSERT
@@ -255,8 +279,7 @@ func (c *Conn) GetAutocommit() bool {
//
// https://sqlite.org/c3ref/last_insert_rowid.html
func (c *Conn) LastInsertRowID() int64 {
r := c.call("sqlite3_last_insert_rowid", uint64(c.handle))
return int64(r)
return int64(c.call("sqlite3_last_insert_rowid", stk_t(c.handle)))
}
// SetLastInsertRowID allows the application to set the value returned by
@@ -264,7 +287,7 @@ func (c *Conn) LastInsertRowID() int64 {
//
// 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))
c.call("sqlite3_set_last_insert_rowid", stk_t(c.handle), stk_t(id))
}
// Changes returns the number of rows modified, inserted or deleted
@@ -273,8 +296,7 @@ func (c *Conn) SetLastInsertRowID(id int64) {
//
// https://sqlite.org/c3ref/changes.html
func (c *Conn) Changes() int64 {
r := c.call("sqlite3_changes64", uint64(c.handle))
return int64(r)
return int64(c.call("sqlite3_changes64", stk_t(c.handle)))
}
// TotalChanges returns the number of rows modified, inserted or deleted
@@ -283,20 +305,18 @@ func (c *Conn) Changes() int64 {
//
// https://sqlite.org/c3ref/total_changes.html
func (c *Conn) TotalChanges() int64 {
r := c.call("sqlite3_total_changes64", uint64(c.handle))
return int64(r)
return int64(c.call("sqlite3_total_changes64", stk_t(c.handle)))
}
// 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)
rc := res_t(c.call("sqlite3_db_release_memory", stk_t(c.handle)))
return c.error(rc)
}
// GetInterrupt gets the context set with [Conn.SetInterrupt],
// or nil if none was set.
// GetInterrupt gets the context set with [Conn.SetInterrupt].
func (c *Conn) GetInterrupt() context.Context {
return c.interrupt
}
@@ -316,39 +336,48 @@ func (c *Conn) GetInterrupt() context.Context {
//
// https://sqlite.org/c3ref/interrupt.html
func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
// Is it the same context?
if ctx == c.interrupt {
return ctx
old = c.interrupt
c.interrupt = ctx
if ctx == old || ctx.Done() == old.Done() {
return old
}
// A busy SQL statement prevents SQLite from ignoring an interrupt
// that comes before any other statements are started.
if c.pending == nil {
c.pending, _, _ = c.Prepare(`WITH RECURSIVE c(x) AS (VALUES(0) UNION ALL SELECT x FROM c) SELECT x FROM c`)
defer c.arena.mark()()
stmtPtr := c.arena.new(ptrlen)
loopPtr := c.arena.string(`WITH RECURSIVE c(x) AS (VALUES(0) UNION ALL SELECT x FROM c) SELECT x FROM c`)
c.call("sqlite3_prepare_v3", stk_t(c.handle), stk_t(loopPtr), math.MaxUint64,
stk_t(PREPARE_PERSISTENT), stk_t(stmtPtr), 0)
c.pending = &Stmt{c: c}
c.pending.handle = util.Read32[ptr_t](c.mod, stmtPtr)
}
old = c.interrupt
c.interrupt = ctx
if old != nil && old.Done() != nil && (ctx == nil || ctx.Err() == nil) {
if old.Done() != nil && ctx.Err() == nil {
c.pending.Reset()
}
if ctx != nil && ctx.Done() != nil {
if ctx.Done() != nil {
c.pending.Step()
}
return old
}
func (c *Conn) checkInterrupt() {
if c.interrupt != nil && c.interrupt.Err() != nil {
c.call("sqlite3_interrupt", uint64(c.handle))
func (c *Conn) checkInterrupt(handle ptr_t) {
if c.interrupt.Err() != nil {
c.call("sqlite3_interrupt", stk_t(handle))
}
}
func progressCallback(ctx context.Context, mod api.Module, pDB uint32) (interrupt uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB &&
c.interrupt != nil && c.interrupt.Err() != nil {
interrupt = 1
func progressCallback(ctx context.Context, mod api.Module, _ ptr_t) (interrupt int32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok {
if c.interrupt.Done() != nil {
runtime.Gosched()
}
if c.interrupt.Err() != nil {
interrupt = 1
}
}
return interrupt
}
@@ -358,69 +387,126 @@ func progressCallback(ctx context.Context, mod api.Module, pDB uint32) (interrup
// 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)
rc := res_t(c.call("sqlite3_busy_timeout", stk_t(c.handle), stk_t(ms)))
return c.error(rc)
}
func timeoutCallback(ctx context.Context, mod api.Module, pDB uint32, count, tmout int32) (retry uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok &&
(c.interrupt == nil || c.interrupt.Err() == nil) {
const delays = "\x01\x02\x05\x0a\x0f\x14\x19\x19\x19\x32\x32\x64"
const totals = "\x00\x01\x03\x08\x12\x21\x35\x4e\x67\x80\xb2\xe4"
const ndelay = int32(len(delays) - 1)
var delay, prior int32
if count <= ndelay {
delay = int32(delays[count])
prior = int32(totals[count])
} else {
delay = int32(delays[ndelay])
prior = int32(totals[ndelay]) + delay*(count-ndelay)
func timeoutCallback(ctx context.Context, mod api.Module, count, tmout int32) (retry int32) {
// https://fractaledmind.github.io/2024/04/15/sqlite-on-rails-the-how-and-why-of-optimal-performance/
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.interrupt.Err() == nil {
switch {
case count == 0:
c.busy1st = time.Now()
case time.Since(c.busy1st) >= time.Duration(tmout)*time.Millisecond:
return 0
}
if delay = min(delay, tmout-prior); delay > 0 {
time.Sleep(time.Duration(delay) * time.Millisecond)
retry = 1
if time.Since(c.busylst) < time.Millisecond {
const sleepIncrement = 2*1024*1024 - 1 // power of two, ~2ms
time.Sleep(time.Duration(rand.Int63() & sleepIncrement))
}
c.busylst = time.Now()
return 1
}
return retry
return 0
}
// BusyHandler registers a callback to handle [BUSY] errors.
//
// https://sqlite.org/c3ref/busy_handler.html
func (c *Conn) BusyHandler(cb func(count int) (retry bool)) error {
var enable uint64
func (c *Conn) BusyHandler(cb func(ctx context.Context, count int) (retry bool)) error {
var enable int32
if cb != nil {
enable = 1
}
r := c.call("sqlite3_busy_handler_go", uint64(c.handle), enable)
if err := c.error(r); err != nil {
rc := res_t(c.call("sqlite3_busy_handler_go", stk_t(c.handle), stk_t(enable)))
if err := c.error(rc); err != nil {
return err
}
c.busy = cb
return nil
}
func busyCallback(ctx context.Context, mod api.Module, pDB uint32, count int32) (retry uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil &&
(c.interrupt == nil || c.interrupt.Err() == nil) {
if c.busy(int(count)) {
func busyCallback(ctx context.Context, mod api.Module, pDB ptr_t, count int32) (retry int32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil {
interrupt := c.interrupt
if interrupt == nil {
interrupt = context.Background()
}
if interrupt.Err() == nil && c.busy(interrupt, int(count)) {
retry = 1
}
}
return retry
}
func (c *Conn) error(rc uint64, sql ...string) error {
// Status retrieves runtime status information about a database connection.
//
// https://sqlite.org/c3ref/db_status.html
func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int, err error) {
defer c.arena.mark()()
hiPtr := c.arena.new(intlen)
curPtr := c.arena.new(intlen)
var i int32
if reset {
i = 1
}
rc := res_t(c.call("sqlite3_db_status", stk_t(c.handle),
stk_t(op), stk_t(curPtr), stk_t(hiPtr), stk_t(i)))
if err = c.error(rc); err == nil {
current = int(util.Read32[int32](c.mod, curPtr))
highwater = int(util.Read32[int32](c.mod, hiPtr))
}
return
}
// TableColumnMetadata extracts metadata about a column of a table.
//
// https://sqlite.org/c3ref/table_column_metadata.html
func (c *Conn) TableColumnMetadata(schema, table, column string) (declType, collSeq string, notNull, primaryKey, autoInc bool, err error) {
defer c.arena.mark()()
var schemaPtr, columnPtr ptr_t
declTypePtr := c.arena.new(ptrlen)
collSeqPtr := c.arena.new(ptrlen)
notNullPtr := c.arena.new(ptrlen)
autoIncPtr := c.arena.new(ptrlen)
primaryKeyPtr := c.arena.new(ptrlen)
if schema != "" {
schemaPtr = c.arena.string(schema)
}
tablePtr := c.arena.string(table)
if column != "" {
columnPtr = c.arena.string(column)
}
rc := res_t(c.call("sqlite3_table_column_metadata", stk_t(c.handle),
stk_t(schemaPtr), stk_t(tablePtr), stk_t(columnPtr),
stk_t(declTypePtr), stk_t(collSeqPtr),
stk_t(notNullPtr), stk_t(primaryKeyPtr), stk_t(autoIncPtr)))
if err = c.error(rc); err == nil && column != "" {
if ptr := util.Read32[ptr_t](c.mod, declTypePtr); ptr != 0 {
declType = util.ReadString(c.mod, ptr, _MAX_NAME)
}
if ptr := util.Read32[ptr_t](c.mod, collSeqPtr); ptr != 0 {
collSeq = util.ReadString(c.mod, ptr, _MAX_NAME)
}
notNull = util.ReadBool(c.mod, notNullPtr)
autoInc = util.ReadBool(c.mod, autoIncPtr)
primaryKey = util.ReadBool(c.mod, primaryKeyPtr)
}
return
}
func (c *Conn) error(rc res_t, sql ...string) error {
return c.sqlite.error(rc, c.handle, sql...)
}
// DriverConn is implemented by the SQLite [database/sql] driver connection.
//
// It can be used to access SQLite features like [online backup].
//
// [online backup]: https://sqlite.org/backup.html
type DriverConn interface {
Raw() *Conn
func (c *Conn) stmtsIter(yield func(*Stmt) bool) {
for _, s := range c.stmts {
if !yield(s) {
break
}
}
}

11
conn_iter.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build go1.23
package sqlite3
import "iter"
// Stmts returns an iterator for the prepared statements
// associated with the database connection.
//
// https://sqlite.org/c3ref/next_stmt.html
func (c *Conn) Stmts() iter.Seq[*Stmt] { return c.stmtsIter }

9
conn_old.go Normal file
View File

@@ -0,0 +1,9 @@
//go:build !go1.23
package sqlite3
// Stmts returns an iterator for the prepared statements
// associated with the database connection.
//
// https://sqlite.org/c3ref/next_stmt.html
func (c *Conn) Stmts() func(func(*Stmt) bool) { return c.stmtsIter }

View File

@@ -1,21 +1,29 @@
package sqlite3
import "strconv"
import (
"strconv"
"github.com/ncruces/go-sqlite3/internal/util"
)
const (
_OK = 0 /* Successful result */
_ROW = 100 /* sqlite3_step() has another row ready */
_DONE = 101 /* sqlite3_step() has finished executing */
_UTF8 = 1
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
_MAX_LENGTH = 1e9
_MAX_SQL_LENGTH = 1e9
_MAX_FUNCTION_ARG = 100
_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 = util.PtrLen
intlen = util.IntLen
)
ptrlen = 4
type (
stk_t = util.Stk_t
ptr_t = util.Ptr_t
res_t = util.Res_t
)
// ErrorCode is a result code that [Error.Code] might return.
@@ -109,7 +117,7 @@ const (
CANTOPEN_ISDIR ExtendedErrorCode = xErrorCode(CANTOPEN) | (2 << 8)
CANTOPEN_FULLPATH ExtendedErrorCode = xErrorCode(CANTOPEN) | (3 << 8)
CANTOPEN_CONVPATH ExtendedErrorCode = xErrorCode(CANTOPEN) | (4 << 8)
CANTOPEN_DIRTYWAL ExtendedErrorCode = xErrorCode(CANTOPEN) | (5 << 8) /* Not Used */
// CANTOPEN_DIRTYWAL ExtendedErrorCode = xErrorCode(CANTOPEN) | (5 << 8) /* Not Used */
CANTOPEN_SYMLINK ExtendedErrorCode = xErrorCode(CANTOPEN) | (6 << 8)
CORRUPT_VTAB ExtendedErrorCode = xErrorCode(CORRUPT) | (1 << 8)
CORRUPT_SEQUENCE ExtendedErrorCode = xErrorCode(CORRUPT) | (2 << 8)
@@ -168,6 +176,7 @@ const (
PREPARE_PERSISTENT PrepareFlag = 0x01
PREPARE_NORMALIZE PrepareFlag = 0x02
PREPARE_NO_VTAB PrepareFlag = 0x04
PREPARE_DONT_LOG PrepareFlag = 0x10
)
// FunctionFlag is a flag that can be passed to
@@ -177,11 +186,12 @@ const (
type FunctionFlag uint32
const (
DETERMINISTIC FunctionFlag = 0x000000800
DIRECTONLY FunctionFlag = 0x000080000
SUBTYPE FunctionFlag = 0x000100000
INNOCUOUS FunctionFlag = 0x000200000
RESULT_SUBTYPE FunctionFlag = 0x001000000
DETERMINISTIC FunctionFlag = 0x000000800
DIRECTONLY FunctionFlag = 0x000080000
INNOCUOUS FunctionFlag = 0x000200000
SELFORDER1 FunctionFlag = 0x002000000
// SUBTYPE FunctionFlag = 0x000100000
// RESULT_SUBTYPE FunctionFlag = 0x001000000
)
// StmtStatus name counter values associated with the [Stmt.Status] method.
@@ -201,6 +211,28 @@ const (
STMTSTATUS_MEMUSED StmtStatus = 99
)
// DBStatus are the available "verbs" that can be passed to the [Conn.Status] method.
//
// https://sqlite.org/c3ref/c_dbstatus_options.html
type DBStatus uint32
const (
DBSTATUS_LOOKASIDE_USED DBStatus = 0
DBSTATUS_CACHE_USED DBStatus = 1
DBSTATUS_SCHEMA_USED DBStatus = 2
DBSTATUS_STMT_USED DBStatus = 3
DBSTATUS_LOOKASIDE_HIT DBStatus = 4
DBSTATUS_LOOKASIDE_MISS_SIZE DBStatus = 5
DBSTATUS_LOOKASIDE_MISS_FULL DBStatus = 6
DBSTATUS_CACHE_HIT DBStatus = 7
DBSTATUS_CACHE_MISS DBStatus = 8
DBSTATUS_CACHE_WRITE DBStatus = 9
DBSTATUS_DEFERRED_FKS DBStatus = 10
DBSTATUS_CACHE_USED_SHARED DBStatus = 11
DBSTATUS_CACHE_SPILL DBStatus = 12
// DBSTATUS_MAX DBStatus = 12
)
// DBConfig are the available database connection configuration options.
//
// https://sqlite.org/c3ref/c_dbconfig_defensive.html
@@ -227,6 +259,28 @@ const (
DBCONFIG_TRUSTED_SCHEMA DBConfig = 1017
DBCONFIG_STMT_SCANSTATUS DBConfig = 1018
DBCONFIG_REVERSE_SCANORDER DBConfig = 1019
DBCONFIG_ENABLE_ATTACH_CREATE DBConfig = 1020
DBCONFIG_ENABLE_ATTACH_WRITE DBConfig = 1021
DBCONFIG_ENABLE_COMMENTS DBConfig = 1022
// DBCONFIG_MAX DBConfig = 1022
)
// FcntlOpcode are the available opcodes for [Conn.FileControl].
//
// https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html
type FcntlOpcode uint32
const (
FCNTL_LOCKSTATE FcntlOpcode = 1
FCNTL_CHUNK_SIZE FcntlOpcode = 6
FCNTL_FILE_POINTER FcntlOpcode = 7
FCNTL_PERSIST_WAL FcntlOpcode = 10
FCNTL_POWERSAFE_OVERWRITE FcntlOpcode = 13
FCNTL_VFS_POINTER FcntlOpcode = 27
FCNTL_JOURNAL_POINTER FcntlOpcode = 28
FCNTL_DATA_VERSION FcntlOpcode = 35
FCNTL_RESERVE_BYTES FcntlOpcode = 38
FCNTL_RESET_CACHE FcntlOpcode = 42
)
// LimitCategory are the available run-time limit categories.
@@ -289,8 +343,8 @@ const (
AUTH_DROP_VTABLE AuthorizerActionCode = 30 /* Table Name Module Name */
AUTH_FUNCTION AuthorizerActionCode = 31 /* NULL Function Name */
AUTH_SAVEPOINT AuthorizerActionCode = 32 /* Operation Savepoint Name */
AUTH_COPY AuthorizerActionCode = 0 /* No longer used */
AUTH_RECURSIVE AuthorizerActionCode = 33 /* NULL NULL */
// AUTH_COPY AuthorizerActionCode = 0 /* No longer used */
)
// AuthorizerReturnCode are the integer codes
@@ -328,6 +382,18 @@ const (
TXN_WRITE TxnState = 2
)
// TraceEvent identify classes of events that can be monitored with [Conn.Trace].
//
// https://sqlite.org/c3ref/c_trace.html
type TraceEvent uint32
const (
TRACE_STMT TraceEvent = 0x01
TRACE_PROFILE TraceEvent = 0x02
TRACE_ROW TraceEvent = 0x04
TRACE_CLOSE TraceEvent = 0x08
)
// Datatype is a fundamental datatype of SQLite.
//
// https://sqlite.org/c3ref/c_blob.html

View File

@@ -15,7 +15,7 @@ import (
// https://sqlite.org/c3ref/context.html
type Context struct {
c *Conn
handle uint32
handle ptr_t
}
// Conn returns the database connection of the
@@ -32,14 +32,14 @@ func (ctx Context) Conn() *Conn {
// 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))
ctx.c.call("sqlite3_set_auxdata_go", stk_t(ctx.handle), stk_t(n), stk_t(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)))
ptr := ptr_t(ctx.c.call("sqlite3_get_auxdata", stk_t(ctx.handle), stk_t(n)))
return util.GetHandle(ctx.c.ctx, ptr)
}
@@ -68,7 +68,7 @@ func (ctx Context) ResultInt(value int) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultInt64(value int64) {
ctx.c.call("sqlite3_result_int64",
uint64(ctx.handle), uint64(value))
stk_t(ctx.handle), stk_t(value))
}
// ResultFloat sets the result of the function to a float64.
@@ -76,7 +76,7 @@ func (ctx Context) ResultInt64(value int64) {
// 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))
stk_t(ctx.handle), stk_t(math.Float64bits(value)))
}
// ResultText sets the result of the function to a string.
@@ -84,19 +84,18 @@ func (ctx Context) ResultFloat(value float64) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultText(value string) {
ptr := ctx.c.newString(value)
ctx.c.call("sqlite3_result_text64",
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
uint64(ctx.c.freer), _UTF8)
ctx.c.call("sqlite3_result_text_go",
stk_t(ctx.handle), stk_t(ptr), stk_t(len(value)))
}
// ResultRawText sets the text result of the function to a []byte.
// Returning a nil slice is the same as calling [Context.ResultNull].
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultRawText(value []byte) {
ptr := ctx.c.newBytes(value)
ctx.c.call("sqlite3_result_text64",
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
uint64(ctx.c.freer), _UTF8)
ctx.c.call("sqlite3_result_text_go",
stk_t(ctx.handle), stk_t(ptr), stk_t(len(value)))
}
// ResultBlob sets the result of the function to a []byte.
@@ -105,9 +104,8 @@ func (ctx Context) ResultRawText(value []byte) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultBlob(value []byte) {
ptr := ctx.c.newBytes(value)
ctx.c.call("sqlite3_result_blob64",
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
uint64(ctx.c.freer))
ctx.c.call("sqlite3_result_blob_go",
stk_t(ctx.handle), stk_t(ptr), stk_t(len(value)))
}
// ResultZeroBlob sets the result of the function to a zero-filled, length n BLOB.
@@ -115,7 +113,7 @@ func (ctx Context) ResultBlob(value []byte) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultZeroBlob(n int64) {
ctx.c.call("sqlite3_result_zeroblob64",
uint64(ctx.handle), uint64(n))
stk_t(ctx.handle), stk_t(n))
}
// ResultNull sets the result of the function to NULL.
@@ -123,14 +121,15 @@ func (ctx Context) ResultZeroBlob(n int64) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultNull() {
ctx.c.call("sqlite3_result_null",
uint64(ctx.handle))
stk_t(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 {
switch format {
case TimeFormatDefault, TimeFormatAuto, time.RFC3339Nano:
ctx.resultRFC3339Nano(value)
return
}
@@ -147,15 +146,14 @@ func (ctx Context) ResultTime(value time.Time, format TimeFormat) {
}
func (ctx Context) resultRFC3339Nano(value time.Time) {
const maxlen = uint64(len(time.RFC3339Nano)) + 5
const maxlen = int64(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)
ctx.c.call("sqlite3_result_text_go",
stk_t(ctx.handle), stk_t(ptr), stk_t(len(buf)))
}
// ResultPointer sets the result of the function to NULL, just like [Context.ResultNull],
@@ -165,7 +163,8 @@ func (ctx Context) resultRFC3339Nano(value time.Time) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultPointer(ptr any) {
valPtr := util.AddHandle(ctx.c.ctx, ptr)
ctx.c.call("sqlite3_result_pointer_go", uint64(valPtr))
ctx.c.call("sqlite3_result_pointer_go",
stk_t(ctx.handle), stk_t(valPtr))
}
// ResultJSON sets the result of the function to the JSON encoding of value.
@@ -175,7 +174,7 @@ func (ctx Context) ResultJSON(value any) {
data, err := json.Marshal(value)
if err != nil {
ctx.ResultError(err)
return
return // notest
}
ctx.ResultRawText(data)
}
@@ -189,7 +188,7 @@ func (ctx Context) ResultValue(value Value) {
return
}
ctx.c.call("sqlite3_result_value",
uint64(ctx.handle), uint64(value.handle))
stk_t(ctx.handle), stk_t(value.handle))
}
// ResultError sets the result of the function an error.
@@ -197,12 +196,12 @@ func (ctx Context) ResultValue(value Value) {
// 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))
ctx.c.call("sqlite3_result_error_nomem", stk_t(ctx.handle))
return
}
if errors.Is(err, TOOBIG) {
ctx.c.call("sqlite3_result_error_toobig", uint64(ctx.handle))
ctx.c.call("sqlite3_result_error_toobig", stk_t(ctx.handle))
return
}
@@ -211,11 +210,11 @@ func (ctx Context) ResultError(err error) {
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)))
stk_t(ctx.handle), stk_t(ptr), stk_t(len(msg)))
}
if code != _OK {
ctx.c.call("sqlite3_result_error_code",
uint64(ctx.handle), uint64(code))
stk_t(ctx.handle), stk_t(code))
}
}
@@ -224,6 +223,6 @@ func (ctx Context) ResultError(err error) {
//
// 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
b := int32(ctx.c.call("sqlite3_vtab_nochange", stk_t(ctx.handle)))
return b != 0
}

View File

@@ -8,21 +8,50 @@
//
// The data source name for "sqlite3" databases can be a filename or a "file:" [URI].
//
// # Default transaction mode
//
// The [TRANSACTION] mode can be specified using "_txlock":
//
// sql.Open("sqlite3", "file:demo.db?_txlock=immediate")
//
// Possible values are: "deferred", "immediate", "exclusive".
// A [read-only] transaction is always "deferred", regardless of "_txlock".
// Possible values are: "deferred" (the default), "immediate", "exclusive".
// Regardless of "_txlock":
// - a [linearizable] transaction is always "exclusive";
// - a [serializable] transaction is always "immediate";
// - a [read-only] transaction is always "deferred".
//
// # Working with time
//
// The time encoding/decoding format can be specified using "_timefmt":
//
// sql.Open("sqlite3", "file:demo.db?_timefmt=sqlite")
//
// Possible values are: "auto" (the default), "sqlite", "rfc3339";
// "auto" encodes as RFC 3339 and decodes any [format] supported by SQLite;
// "sqlite" encodes as SQLite and decodes any [format] supported by SQLite;
// "rfc3339" encodes and decodes RFC 3339 only.
// - "auto" encodes as RFC 3339 and decodes any [format] supported by SQLite;
// - "sqlite" encodes as SQLite and decodes any [format] supported by SQLite;
// - "rfc3339" encodes and decodes RFC 3339 only.
//
// If you encode as RFC 3339 (the default),
// consider using the TIME [collating sequence] to produce a time-ordered sequence.
//
// To scan values in other formats, [sqlite3.TimeFormat.Scanner] may be helpful.
// To bind values in other formats, [sqlite3.TimeFormat.Encode] them before binding.
//
// When using a custom time struct, you'll have to implement
// [database/sql/driver.Valuer] and [database/sql.Scanner].
//
// The Value method should ideally encode to a time [format] supported by SQLite.
// This ensures SQL date and time functions work as they should,
// and that your schema works with other SQLite tools.
// [sqlite3.TimeFormat.Encode] may help.
//
// The Scan method needs to take into account that the value it receives can be of differing types.
// It can already be a [time.Time], if the driver decoded the value according to "_timefmt" rules.
// Or it can be a: string, int64, float64, []byte, or nil,
// depending on the column type and what whoever wrote the value.
// [sqlite3.TimeFormat.Decode] may help.
//
// # Setting PRAGMAs
//
// [PRAGMA] statements can be specified using "_pragma":
//
@@ -31,13 +60,17 @@
// If no PRAGMAs are specified, a busy timeout of 1 minute is set.
//
// Order matters:
// busy timeout and locking mode should be the first PRAGMAs set, in that order.
// encryption keys, busy timeout and locking mode should be the first PRAGMAs set,
// in that order.
//
// [URI]: https://sqlite.org/uri.html
// [PRAGMA]: https://sqlite.org/pragma.html
// [format]: https://sqlite.org/lang_datefunc.html#time_values
// [TRANSACTION]: https://sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions
// [linearizable]: https://pkg.go.dev/database/sql#TxOptions
// [serializable]: https://pkg.go.dev/database/sql#TxOptions
// [read-only]: https://pkg.go.dev/database/sql#TxOptions
// [format]: https://sqlite.org/lang_datefunc.html#time_values
// [collating sequence]: https://sqlite.org/datatype3.html#collating_sequences
package driver
import (
@@ -48,6 +81,7 @@ import (
"fmt"
"io"
"net/url"
"reflect"
"strings"
"time"
"unsafe"
@@ -69,11 +103,22 @@ func init() {
// Open opens the SQLite database specified by dataSourceName as a [database/sql.DB].
//
// The init function is called by the driver on new connections.
// Open accepts zero, one, or two callbacks (nil callbacks are ignored).
// The first callback is called when the driver opens a new connection.
// The second callback is called before the driver closes a connection.
// The [sqlite3.Conn] can be used to execute queries, register functions, etc.
// Any error returned closes the connection and is returned to [database/sql].
func Open(dataSourceName string, init func(*sqlite3.Conn) error) (*sql.DB, error) {
c, err := (&SQLite{Init: init}).OpenConnector(dataSourceName)
func Open(dataSourceName string, fn ...func(*sqlite3.Conn) error) (*sql.DB, error) {
if len(fn) > 2 {
return nil, sqlite3.MISUSE
}
var init, term func(*sqlite3.Conn) error
if len(fn) > 1 {
term = fn[1]
}
if len(fn) > 0 {
init = fn[0]
}
c, err := newConnector(dataSourceName, init, term)
if err != nil {
return nil, err
}
@@ -81,16 +126,16 @@ func Open(dataSourceName string, init func(*sqlite3.Conn) error) (*sql.DB, error
}
// SQLite implements [database/sql/driver.Driver].
type SQLite struct {
// Init function is called by the driver on new connections.
// The [sqlite3.Conn] can be used to execute queries, register functions, etc.
// Any error returned closes the connection and is returned to [database/sql].
Init func(*sqlite3.Conn) error
}
type SQLite struct{}
var (
// Ensure these interfaces are implemented:
_ driver.DriverContext = &SQLite{}
)
// Open implements [database/sql/driver.Driver].
func (d *SQLite) Open(name string) (driver.Conn, error) {
c, err := d.newConnector(name)
c, err := newConnector(name, nil, nil)
if err != nil {
return nil, err
}
@@ -99,11 +144,11 @@ func (d *SQLite) Open(name string) (driver.Conn, error) {
// OpenConnector implements [database/sql/driver.DriverContext].
func (d *SQLite) OpenConnector(name string) (driver.Connector, error) {
return d.newConnector(name)
return newConnector(name, nil, nil)
}
func (d *SQLite) newConnector(name string) (*connector, error) {
c := connector{driver: d, name: name}
func newConnector(name string, init, term func(*sqlite3.Conn) error) (*connector, error) {
c := connector{name: name, init: init, term: term}
var txlock, timefmt string
if strings.HasPrefix(name, "file:") {
@@ -119,10 +164,8 @@ func (d *SQLite) newConnector(name string) (*connector, error) {
}
switch txlock {
case "":
c.txBegin = "BEGIN"
case "deferred", "immediate", "exclusive":
c.txBegin = "BEGIN " + txlock
case "", "deferred", "concurrent", "immediate", "exclusive":
c.txLock = txlock
default:
return nil, fmt.Errorf("sqlite3: invalid _txlock: %s", txlock)
}
@@ -145,31 +188,32 @@ func (d *SQLite) newConnector(name string) (*connector, error) {
}
type connector struct {
driver *SQLite
init func(*sqlite3.Conn) error
term func(*sqlite3.Conn) error
name string
txBegin string
txLock string
tmRead sqlite3.TimeFormat
tmWrite sqlite3.TimeFormat
pragmas bool
}
func (n *connector) Driver() driver.Driver {
return n.driver
return &SQLite{}
}
func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
func (n *connector) Connect(ctx context.Context) (ret driver.Conn, err error) {
c := &conn{
txBegin: n.txBegin,
txLock: n.txLock,
tmRead: n.tmRead,
tmWrite: n.tmWrite,
}
c.Conn, err = sqlite3.Open(n.name)
c.Conn, err = sqlite3.OpenContext(ctx, n.name)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
if ret == nil {
c.Close()
}
}()
@@ -178,22 +222,23 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
defer c.Conn.SetInterrupt(old)
if !n.pragmas {
err = c.Conn.BusyTimeout(60 * time.Second)
err = c.Conn.BusyTimeout(time.Minute)
if err != nil {
return nil, err
}
}
if n.driver.Init != nil {
err = n.driver.Init(c.Conn)
if n.init != nil {
err = n.init(c.Conn)
if err != nil {
return nil, err
}
}
if n.pragmas || n.driver.Init != nil {
if n.pragmas || n.init != nil {
s, _, err := c.Conn.Prepare(`PRAGMA query_only`)
if err != nil {
return nil, err
}
defer s.Close()
if s.Step() && s.ColumnBool(0) {
c.readOnly = '1'
} else {
@@ -204,25 +249,62 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
return nil, err
}
}
if n.term != nil {
err = c.Conn.Trace(sqlite3.TRACE_CLOSE, func(sqlite3.TraceEvent, any, any) error {
return n.term(c.Conn)
})
if err != nil {
return nil, err
}
}
return c, nil
}
// Conn is implemented by the SQLite [database/sql] driver connections.
//
// It can be used to access SQLite features like [online backup]:
//
// db, err := driver.Open("temp.db")
// if err != nil {
// log.Fatal(err)
// }
// defer db.Close()
//
// conn, err := db.Conn(context.TODO())
// if err != nil {
// log.Fatal(err)
// }
// defer conn.Close()
//
// err = conn.Raw(func(driverConn any) error {
// conn := driverConn.(driver.Conn)
// return conn.Raw().Backup("main", "backup.db")
// })
// if err != nil {
// log.Fatal(err)
// }
//
// [online backup]: https://sqlite.org/backup.html
type Conn interface {
Raw() *sqlite3.Conn
driver.Conn
driver.ConnBeginTx
driver.ConnPrepareContext
}
type conn struct {
*sqlite3.Conn
txBegin string
txCommit string
txRollback string
tmRead sqlite3.TimeFormat
tmWrite sqlite3.TimeFormat
readOnly byte
txLock string
txReset string
tmRead sqlite3.TimeFormat
tmWrite sqlite3.TimeFormat
readOnly byte
}
var (
// Ensure these interfaces are implemented:
_ driver.ConnPrepareContext = &conn{}
_ driver.ExecerContext = &conn{}
_ driver.ConnBeginTx = &conn{}
_ sqlite3.DriverConn = &conn{}
_ Conn = &conn{}
_ driver.ExecerContext = &conn{}
)
func (c *conn) Raw() *sqlite3.Conn {
@@ -231,31 +313,30 @@ func (c *conn) Raw() *sqlite3.Conn {
// Deprecated: use BeginTx instead.
func (c *conn) Begin() (driver.Tx, error) {
// notest
return c.BeginTx(context.Background(), driver.TxOptions{})
}
func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
txBegin := c.txBegin
c.txCommit = `COMMIT`
c.txRollback = `ROLLBACK`
if opts.ReadOnly {
txBegin = `
BEGIN deferred;
PRAGMA query_only=on`
c.txRollback = `
ROLLBACK;
PRAGMA query_only=` + string(c.readOnly)
c.txCommit = c.txRollback
}
var txLock string
switch opts.Isolation {
default:
return nil, util.IsolationErr
case
driver.IsolationLevel(sql.LevelDefault),
driver.IsolationLevel(sql.LevelSerializable):
break
case driver.IsolationLevel(sql.LevelLinearizable):
txLock = "exclusive"
case driver.IsolationLevel(sql.LevelSerializable):
txLock = "immediate"
case driver.IsolationLevel(sql.LevelDefault):
if !opts.ReadOnly {
txLock = c.txLock
}
}
c.txReset = ``
txBegin := `BEGIN ` + txLock
if opts.ReadOnly {
txBegin += ` ; PRAGMA query_only=on`
c.txReset = `; PRAGMA query_only=` + string(c.readOnly)
}
old := c.Conn.SetInterrupt(ctx)
@@ -269,7 +350,7 @@ func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, e
}
func (c *conn) Commit() error {
err := c.Conn.Exec(c.txCommit)
err := c.Conn.Exec(`COMMIT` + c.txReset)
if err != nil && !c.Conn.GetAutocommit() {
c.Rollback()
}
@@ -277,16 +358,17 @@ func (c *conn) Commit() error {
}
func (c *conn) Rollback() error {
err := c.Conn.Exec(c.txRollback)
err := c.Conn.Exec(`ROLLBACK` + c.txReset)
if errors.Is(err, sqlite3.INTERRUPT) {
old := c.Conn.SetInterrupt(context.Background())
defer c.Conn.SetInterrupt(old)
err = c.Conn.Exec(c.txRollback)
err = c.Conn.Exec(`ROLLBACK` + c.txReset)
}
return err
}
func (c *conn) Prepare(query string) (driver.Stmt, error) {
// notest
return c.PrepareContext(context.Background(), query)
}
@@ -298,7 +380,7 @@ func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, e
if err != nil {
return nil, err
}
if tail != "" {
if notWhitespace(tail) {
s.Close()
return nil, util.TailErr
}
@@ -329,6 +411,8 @@ func (c *conn) ExecContext(ctx context.Context, query string, args []driver.Name
}
func (c *conn) CheckNamedValue(arg *driver.NamedValue) error {
// Fast path: short circuit argument verification.
// Arguments will be rejected by conn.ExecContext.
return nil
}
@@ -363,11 +447,13 @@ func (s *stmt) NumInput() int {
// Deprecated: use ExecContext instead.
func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {
// notest
return s.ExecContext(context.Background(), namedValues(args))
}
// Deprecated: use QueryContext instead.
func (s *stmt) Query(args []driver.Value) (driver.Rows, error) {
// notest
return s.QueryContext(context.Background(), namedValues(args))
}
@@ -380,7 +466,9 @@ func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (drive
old := s.Stmt.Conn().SetInterrupt(ctx)
defer s.Stmt.Conn().SetInterrupt(old)
err = s.Stmt.Exec()
err = errors.Join(
s.Stmt.Exec(),
s.Stmt.ClearBindings())
if err != nil {
return nil, err
}
@@ -403,7 +491,7 @@ func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
if arg.Name == "" {
ids = append(ids, arg.Ordinal)
} else {
for _, prefix := range []string{":", "@", "$"} {
for _, prefix := range [...]string{":", "@", "$"} {
if id := s.Stmt.BindIndex(prefix + arg.Name); id != 0 {
ids = append(ids, id)
}
@@ -437,9 +525,9 @@ func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
default:
panic(util.AssertErr())
}
}
if err != nil {
return err
if err != nil {
return err
}
}
}
return nil
@@ -493,37 +581,101 @@ type rows struct {
*stmt
names []string
types []string
nulls []bool
scans []scantype
}
type scantype byte
const (
_ANY scantype = iota
_INT scantype = scantype(sqlite3.INTEGER)
_REAL scantype = scantype(sqlite3.FLOAT)
_TEXT scantype = scantype(sqlite3.TEXT)
_BLOB scantype = scantype(sqlite3.BLOB)
_NULL scantype = scantype(sqlite3.NULL)
_BOOL scantype = iota
_TIME
)
var (
// Ensure these interfaces are implemented:
_ driver.RowsColumnTypeDatabaseTypeName = &rows{}
_ driver.RowsColumnTypeNullable = &rows{}
)
func (r *rows) Close() error {
r.Stmt.ClearBindings()
return r.Stmt.Reset()
return errors.Join(
r.Stmt.Reset(),
r.Stmt.ClearBindings())
}
func (r *rows) Columns() []string {
if r.names == nil {
count := r.Stmt.ColumnCount()
r.names = make([]string, count)
for i := range r.names {
r.names[i] = r.Stmt.ColumnName(i)
names := make([]string, count)
for i := range names {
names[i] = r.Stmt.ColumnName(i)
}
r.names = names
}
return r.names
}
func (r *rows) loadColumnMetadata() {
if r.nulls == nil {
count := r.Stmt.ColumnCount()
nulls := make([]bool, count)
types := make([]string, count)
scans := make([]scantype, count)
for i := range nulls {
if col := r.Stmt.ColumnOriginName(i); col != "" {
types[i], _, nulls[i], _, _, _ = r.Stmt.Conn().TableColumnMetadata(
r.Stmt.ColumnDatabaseName(i),
r.Stmt.ColumnTableName(i),
col)
types[i] = strings.ToUpper(types[i])
// These types are only used before we have rows,
// and otherwise as type hints.
// The first few ensure STRICT tables are strictly typed.
// The other two are type hints for booleans and time.
switch types[i] {
case "INT", "INTEGER":
scans[i] = _INT
case "REAL":
scans[i] = _REAL
case "TEXT":
scans[i] = _TEXT
case "BLOB":
scans[i] = _BLOB
case "BOOLEAN":
scans[i] = _BOOL
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
scans[i] = _TIME
}
}
}
r.nulls = nulls
r.types = types
r.scans = scans
}
}
func (r *rows) declType(index int) string {
if r.types == nil {
count := r.Stmt.ColumnCount()
r.types = make([]string, count)
for i := range r.types {
r.types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
types := make([]string, count)
for i := range types {
types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
}
r.types = types
}
return r.types[index]
}
func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
decltype := r.declType(index)
r.loadColumnMetadata()
decltype := r.types[index]
if len := len(decltype); len > 0 && decltype[len-1] == ')' {
if i := strings.LastIndexByte(decltype, '('); i >= 0 {
decltype = decltype[:i]
@@ -532,6 +684,58 @@ func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
return strings.TrimSpace(decltype)
}
func (r *rows) ColumnTypeNullable(index int) (nullable, ok bool) {
r.loadColumnMetadata()
if r.nulls[index] {
return false, true
}
return true, false
}
func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
r.loadColumnMetadata()
scan := r.scans[index]
if r.Stmt.Busy() {
// SQLite is dynamically typed and we now have a row.
// Always use the type of the value itself,
// unless the scan type is more specific
// and can scan the actual value.
val := scantype(r.Stmt.ColumnType(index))
useValType := true
switch {
case scan == _TIME && val != _BLOB && val != _NULL:
t := r.Stmt.ColumnTime(index, r.tmRead)
useValType = t == time.Time{}
case scan == _BOOL && val == _INT:
i := r.Stmt.ColumnInt64(index)
useValType = i != 0 && i != 1
case scan == _BLOB && val == _NULL:
useValType = false
}
if useValType {
scan = val
}
}
switch scan {
case _INT:
return reflect.TypeFor[int64]()
case _REAL:
return reflect.TypeFor[float64]()
case _TEXT:
return reflect.TypeFor[string]()
case _BLOB:
return reflect.TypeFor[[]byte]()
case _BOOL:
return reflect.TypeFor[bool]()
case _TIME:
return reflect.TypeFor[time.Time]()
default:
return reflect.TypeFor[any]()
}
}
func (r *rows) Next(dest []driver.Value) error {
old := r.Stmt.Conn().SetInterrupt(r.ctx)
defer r.Stmt.Conn().SetInterrupt(old)
@@ -544,30 +748,27 @@ func (r *rows) Next(dest []driver.Value) error {
}
data := unsafe.Slice((*any)(unsafe.SliceData(dest)), len(dest))
err := r.Stmt.Columns(data)
err := r.Stmt.Columns(data...)
for i := range dest {
if t, ok := r.decodeTime(i, dest[i]); ok {
dest[i] = t
continue
}
if s, ok := dest[i].(string); ok {
t, ok := maybeTime(s)
if ok {
dest[i] = t
}
}
}
return err
}
func (r *rows) decodeTime(i int, v any) (_ time.Time, ok bool) {
if r.tmRead == sqlite3.TimeFormatDefault {
// handled by maybeTime
return
}
switch v.(type) {
case int64, float64, string:
switch v := v.(type) {
case int64, float64:
// could be a time value
case string:
if r.tmWrite != "" && r.tmWrite != time.RFC3339 && r.tmWrite != time.RFC3339Nano {
break
}
t, ok := maybeTime(v)
if ok {
return t, true
}
default:
return
}

View File

@@ -7,7 +7,7 @@ import (
"errors"
"math"
"net/url"
"path/filepath"
"reflect"
"testing"
"time"
@@ -15,9 +15,21 @@ import (
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/vfs"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Test_Open_error(t *testing.T) {
t.Parallel()
_, err := Open("", nil, nil, nil)
if err == nil {
t.Error("want error")
}
if !errors.Is(err, sqlite3.MISUSE) {
t.Errorf("got %v, want sqlite3.MISUSE", err)
}
}
func Test_Open_dir(t *testing.T) {
t.Parallel()
@@ -38,8 +50,11 @@ func Test_Open_dir(t *testing.T) {
func Test_Open_pragma(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
"_pragma": {"busy_timeout(1000)"},
})
db, err := sql.Open("sqlite3", "file::memory:?_pragma=busy_timeout(1000)")
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
@@ -57,8 +72,11 @@ func Test_Open_pragma(t *testing.T) {
func Test_Open_pragma_invalid(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
"_pragma": {"busy_timeout 1000"},
})
db, err := sql.Open("sqlite3", "file::memory:?_pragma=busy_timeout+1000")
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
@@ -81,14 +99,13 @@ func Test_Open_pragma_invalid(t *testing.T) {
}
func Test_Open_txLock(t *testing.T) {
if !vfs.SupportsFileLocking {
t.Skip("skipping without locks")
}
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
"_txlock": {"exclusive"},
"_pragma": {"busy_timeout(1000)"},
})
db, err := sql.Open("sqlite3", "file:"+
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db"))+
"?_txlock=exclusive&_pragma=busy_timeout(0)")
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
@@ -119,8 +136,11 @@ func Test_Open_txLock(t *testing.T) {
func Test_Open_txLock_invalid(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
"_txlock": {"xclusive"},
})
_, err := sql.Open("sqlite3", "file::memory:?_txlock=xclusive")
_, err := sql.Open("sqlite3", tmp+"_txlock=xclusive")
if err == nil {
t.Fatal("want error")
}
@@ -130,17 +150,16 @@ func Test_Open_txLock_invalid(t *testing.T) {
}
func Test_BeginTx(t *testing.T) {
if !vfs.SupportsFileLocking {
t.Skip("skipping without locks")
}
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
"_txlock": {"exclusive"},
"_pragma": {"busy_timeout(0)"},
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := sql.Open("sqlite3", "file:"+
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db"))+
"?_txlock=exclusive&_pragma=busy_timeout(0)")
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
@@ -182,8 +201,9 @@ func Test_BeginTx(t *testing.T) {
func Test_Prepare(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := sql.Open("sqlite3", ":memory:")
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
@@ -205,8 +225,8 @@ func Test_Prepare(t *testing.T) {
}
_, err = db.Prepare(`SELECT 1; `)
if err.Error() != string(util.TailErr) {
t.Error("want tailErr")
if err != nil {
t.Error(err)
}
_, err = db.Prepare(`SELECT 1; SELECT`)
@@ -222,11 +242,12 @@ func Test_Prepare(t *testing.T) {
func Test_QueryRow_named(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := sql.Open("sqlite3", ":memory:")
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
@@ -274,8 +295,9 @@ func Test_QueryRow_named(t *testing.T) {
func Test_QueryRow_blob_null(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := sql.Open("sqlite3", ":memory:")
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
@@ -310,7 +332,11 @@ func Test_time(t *testing.T) {
for _, fmt := range []string{"auto", "sqlite", "rfc3339", time.ANSIC} {
t.Run(fmt, func(t *testing.T) {
db, err := sql.Open("sqlite3", "file::memory:?_timefmt="+url.QueryEscape(fmt))
tmp := memdb.TestDB(t, url.Values{
"_timefmt": {fmt},
})
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
@@ -340,3 +366,104 @@ func Test_time(t *testing.T) {
})
}
}
func Test_ColumnType_ScanType(t *testing.T) {
var (
INT = reflect.TypeFor[int64]()
REAL = reflect.TypeFor[float64]()
TEXT = reflect.TypeFor[string]()
BLOB = reflect.TypeFor[[]byte]()
BOOL = reflect.TypeFor[bool]()
TIME = reflect.TypeFor[time.Time]()
ANY = reflect.TypeFor[any]()
)
t.Parallel()
tmp := memdb.TestDB(t)
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE test (
col_int INTEGER,
col_real REAL,
col_text TEXT,
col_blob BLOB,
col_bool BOOLEAN,
col_time DATETIME,
col_decimal DECIMAL
);
INSERT INTO test VALUES
(1, 1, 1, 1, 1, 1, 1),
(2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0),
('1', '1', '1', '1', '1', '1', '1'),
('x', 'x', 'x', 'x', 'x', 'x', 'x'),
(x'', x'', x'', x'', x'', x'', x''),
('2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z',
'2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z'),
(TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
(NULL, NULL, NULL, NULL, NULL, NULL, NULL);
`)
if err != nil {
t.Fatal(err)
}
rows, err := db.Query(`SELECT * FROM test`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
cols, err := rows.ColumnTypes()
if err != nil {
t.Fatal(err)
}
want := [][]reflect.Type{
{INT, REAL, TEXT, BLOB, BOOL, TIME, ANY},
{INT, REAL, TEXT, INT, BOOL, TIME, INT},
{INT, REAL, TEXT, REAL, INT, TIME, INT},
{INT, REAL, TEXT, TEXT, BOOL, TIME, INT},
{TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT},
{BLOB, BLOB, BLOB, BLOB, BLOB, BLOB, BLOB},
{TEXT, TEXT, TEXT, TEXT, TEXT, TIME, TEXT},
{INT, REAL, TEXT, INT, BOOL, TIME, INT},
{ANY, ANY, ANY, BLOB, ANY, ANY, ANY},
}
for j, c := range cols {
got := c.ScanType()
if got != want[0][j] {
t.Errorf("want %v, got %v, at column %d", want[0][j], got, j)
}
}
dest := make([]any, len(cols))
for i := 1; rows.Next(); i++ {
cols, err := rows.ColumnTypes()
if err != nil {
t.Fatal(err)
}
for j, c := range cols {
got := c.ScanType()
if got != want[i][j] {
t.Errorf("want %v, got %v, at row %d column %d", want[i][j], got, i, j)
}
dest[j] = reflect.New(got).Interface()
}
err = rows.Scan(dest...)
if err != nil {
t.Error(err)
}
}
err = rows.Err()
if err != nil {
t.Fatal(err)
}
}

144
driver/example2_test.go Normal file
View File

@@ -0,0 +1,144 @@
//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk
package driver_test
// Adapted from: https://go.dev/doc/tutorial/database-access
import (
"database/sql"
"database/sql/driver"
"fmt"
"log"
"time"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Example_customTime() {
db, err := sql.Open("sqlite3", "file:/time.db?vfs=memdb")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE data (
id INTEGER PRIMARY KEY,
date_time TEXT
) STRICT;
`)
if err != nil {
log.Fatal(err)
}
// This one will be returned as string to [sql.Scanner] because it doesn't
// pass the driver's round-trip test when it tries to figure out if it's
// a time. 2009-11-17T20:34:58.650Z goes in, but parsing and formatting
// it with [time.RFC3338Nano] results in 2009-11-17T20:34:58.65Z. Though
// the times are identical, the trailing zero is lost in the string
// representation so the driver considers the conversion unsuccessful.
c1 := CustomTime{time.Date(
2009, 11, 17, 20, 34, 58, 650000000, time.UTC)}
// Store our custom time in the database.
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c1)
if err != nil {
log.Fatal(err)
}
var strc1 string
// Retrieve it as a string, the result of Value().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&strc1)
if err != nil {
log.Fatal(err)
}
fmt.Println("in db:", strc1)
var resc1 CustomTime
// Retrieve it as our custom time type, going through Scan().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&resc1)
if err != nil {
log.Fatal(err)
}
fmt.Println("custom time:", resc1)
// This one will be returned as [time.Time] to [sql.Scanner] because it does
// pass the driver's round-trip test when it tries to figure out if it's
// a time. 2009-11-17T20:34:58.651Z goes in, and parsing and formatting
// it with [time.RFC3339Nano] results in 2009-11-17T20:34:58.651Z.
c2 := CustomTime{time.Date(
2009, 11, 17, 20, 34, 58, 651000000, time.UTC)}
// Store our custom time in the database.
_, err = db.Exec(`INSERT INTO data (date_time) VALUES(?)`, c2)
if err != nil {
log.Fatal(err)
}
var strc2 string
// Retrieve it as a string, the result of Value().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&strc2)
if err != nil {
log.Fatal(err)
}
fmt.Println("in db:", strc2)
var resc2 CustomTime
// Retrieve it as our custom time type, going through Scan().
err = db.QueryRow(`
SELECT date_time
FROM data
WHERE id = last_insert_rowid()
`).Scan(&resc2)
if err != nil {
log.Fatal(err)
}
fmt.Println("custom time:", resc2)
// Output:
// in db: 2009-11-17T20:34:58.650Z
// scan type string: 2009-11-17T20:34:58.650Z
// custom time: 2009-11-17 20:34:58.65 +0000 UTC
// in db: 2009-11-17T20:34:58.651Z
// scan type time: 2009-11-17 20:34:58.651 +0000 UTC
// custom time: 2009-11-17 20:34:58.651 +0000 UTC
}
type CustomTime struct{ time.Time }
func (c CustomTime) Value() (driver.Value, error) {
return sqlite3.TimeFormat7TZ.Encode(c.UTC()), nil
}
func (c *CustomTime) Scan(value any) error {
switch v := value.(type) {
case nil:
*c = CustomTime{time.Time{}}
case time.Time:
fmt.Println("scan type time:", v)
*c = CustomTime{v}
case string:
fmt.Println("scan type string:", v)
t, err := sqlite3.TimeFormat7TZ.Decode(v)
if err != nil {
return err
}
*c = CustomTime{t}
default:
panic("unsupported value type")
}
return nil
}

View File

@@ -1,4 +1,4 @@
//go:build (linux || darwin || windows || freebsd || illumos) && !sqlite3_nosys
//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk
package driver_test
@@ -12,6 +12,7 @@ import (
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
)
var db *sql.DB

View File

@@ -11,7 +11,7 @@ import (
)
func Example_json() {
db, err := driver.Open("file:/test.db?vfs=memdb", nil)
db, err := driver.Open("file:/json.db?vfs=memdb")
if err != nil {
log.Fatal(err)
}
@@ -21,8 +21,8 @@ func Example_json() {
CREATE TABLE orders (
cart_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
cart TEXT
);
cart BLOB -- stored as JSONB
) STRICT;
`)
if err != nil {
log.Fatal(err)
@@ -39,7 +39,8 @@ func Example_json() {
Items []CartItem `json:"items"`
}
_, err = db.Exec(`INSERT INTO orders (user_id, cart) VALUES (?, ?)`, 123, sqlite3.JSON(Cart{
// convert to JSONB on insertion
_, err = db.Exec(`INSERT INTO orders (user_id, cart) VALUES (?, jsonb(?))`, 123, sqlite3.JSON(Cart{
[]CartItem{
{ItemID: "111", Name: "T-shirt", Quantity: 1, Price: 250},
{ItemID: "222", Name: "Trousers", Quantity: 1, Price: 600},
@@ -60,6 +61,24 @@ func Example_json() {
}
fmt.Println("total:", total)
var cart Cart
err = db.QueryRow(`
SELECT json(cart) -- convert to JSON on retrieval
FROM orders
WHERE cart_id = last_insert_rowid()
`).Scan(sqlite3.JSON(&cart))
if err != nil {
log.Fatal(err)
}
for _, item := range cart.Items {
fmt.Printf("id: %s, name: %s, quantity: %d, price: %d\n",
item.ItemID, item.Name, item.Quantity, item.Price)
}
// Output:
// total: 850
// id: 111, name: T-shirt, quantity: 1, price: 250
// id: 222, name: Trousers, quantity: 1, price: 600
}

View File

@@ -16,12 +16,25 @@ func Savepoint(tx *sql.Tx) sqlite3.Savepoint {
return ctx.Savepoint
}
// A saveptCtx is never canceled, has no values, and has no deadline.
type saveptCtx struct{ sqlite3.Savepoint }
func (*saveptCtx) Deadline() (deadline time.Time, ok bool) { return }
func (*saveptCtx) Deadline() (deadline time.Time, ok bool) {
// notest
return
}
func (*saveptCtx) Done() <-chan struct{} { return nil }
func (*saveptCtx) Done() <-chan struct{} {
// notest
return nil
}
func (*saveptCtx) Err() error { return nil }
func (*saveptCtx) Err() error {
// notest
return nil
}
func (*saveptCtx) Value(key any) any { return nil }
func (*saveptCtx) Value(key any) any {
// notest
return nil
}

View File

@@ -6,11 +6,12 @@ import (
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
)
func ExampleSavepoint() {
db, err := driver.Open("file:/test.db?vfs=memdb", nil)
db, err := driver.Open("file:/svpt.db?vfs=memdb")
if err != nil {
log.Fatal(err)
}

View File

@@ -1,8 +1,6 @@
package driver
import (
"time"
)
import "time"
// Convert a string in [time.RFC3339Nano] format into a [time.Time]
// if it roundtrips back to the same string.

View File

@@ -27,12 +27,12 @@ func Fuzz_stringOrTime_1(f *testing.F) {
// Make sure times round-trip to the same string:
// https://pkg.go.dev/database/sql#Rows.Scan
if v.Format(time.RFC3339Nano) != str {
t.Fatalf("did not round-trip: %q", str)
t.Errorf("did not round-trip: %q", str)
}
} else {
date, err := time.Parse(time.RFC3339Nano, str)
if err == nil && date.Format(time.RFC3339Nano) == str {
t.Fatalf("would round-trip: %q", str)
t.Errorf("would round-trip: %q", str)
}
}
})

View File

@@ -12,3 +12,63 @@ func namedValues(args []driver.Value) []driver.NamedValue {
}
return named
}
func notWhitespace(sql string) bool {
const (
code = iota
slash
minus
ccomment
sqlcomment
endcomment
)
state := code
for _, b := range ([]byte)(sql) {
if b == 0 {
break
}
switch state {
case code:
switch b {
case '/':
state = slash
case '-':
state = minus
case ' ', ';', '\t', '\n', '\v', '\f', '\r':
continue
default:
return true
}
case slash:
if b != '*' {
return true
}
state = ccomment
case minus:
if b != '-' {
return true
}
state = sqlcomment
case ccomment:
if b == '*' {
state = endcomment
}
case sqlcomment:
if b == '\n' {
state = code
}
case endcomment:
switch b {
case '/':
state = code
case '*':
state = endcomment
default:
state = ccomment
}
}
}
return state == slash || state == minus
}

View File

@@ -1,9 +1,13 @@
package driver
import (
"context"
"database/sql/driver"
"reflect"
"slices"
"testing"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func Test_namedValues(t *testing.T) {
@@ -12,7 +16,71 @@ func Test_namedValues(t *testing.T) {
{Ordinal: 2, Value: false},
}
got := namedValues([]driver.Value{true, false})
if !reflect.DeepEqual(got, want) {
if !slices.Equal(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}
func Fuzz_notWhitespace(f *testing.F) {
f.Add("")
f.Add(" ")
f.Add(";")
f.Add("0")
f.Add("-")
f.Add("-0")
f.Add("--")
f.Add("--0")
f.Add("--\n")
f.Add("--0\n")
f.Add("/0")
f.Add("/*")
f.Add("/*/")
f.Add("/**")
f.Add("/*0")
f.Add("/**/")
f.Add("/***/")
f.Add("/**0/")
f.Add("\v")
f.Add(" \v")
f.Add("\xf0")
f.Add("\000")
db, err := Open(":memory:")
if err != nil {
f.Fatal(err)
}
defer db.Close()
f.Fuzz(func(t *testing.T, str string) {
if len(str) > 128 {
t.SkipNow()
}
c, err := db.Conn(context.Background())
if err != nil {
t.Fatal(err)
}
defer c.Close()
c.Raw(func(driverConn any) error {
conn := driverConn.(*conn).Conn
stmt, tail, err := conn.Prepare(str)
stmt.Close()
// It's hard to be bug for bug compatible with SQLite.
// We settle for somewhat less:
// - if SQLite reports whitespace, we must too
// - if we report whitespace, SQLite must not parse a statement
if notWhitespace(str) {
if stmt == nil && tail == "" && err == nil {
t.Errorf("was whitespace: %q", str)
}
} else {
if stmt != nil {
t.Errorf("was not whitespace: %q (%v)", str, err)
}
}
return nil
})
})
}

View File

@@ -1,6 +1,6 @@
# Embeddable Wasm build of SQLite
This folder includes an embeddable Wasm build of SQLite 3.46.0 for use with
This folder includes an embeddable Wasm build of SQLite 3.49.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:
@@ -9,6 +9,7 @@ The following optional features are compiled in:
- [JSON](https://sqlite.org/json1.html)
- [R*Tree](https://sqlite.org/rtree.html)
- [GeoPoly](https://sqlite.org/geopoly.html)
- [Spellfix1](https://sqlite.org/spellfix1.html)
- [soundex](https://sqlite.org/lang_corefunc.html#soundex)
- [stat4](https://sqlite.org/compile.html#enable_stat4)
- [base64](https://github.com/sqlite/sqlite/blob/master/ext/misc/base64.c)
@@ -17,14 +18,24 @@ The following optional features are compiled in:
- [regexp](https://github.com/sqlite/sqlite/blob/master/ext/misc/regexp.c)
- [series](https://github.com/sqlite/sqlite/blob/master/ext/misc/series.c)
- [uint](https://github.com/sqlite/sqlite/blob/master/ext/misc/uint.c)
- [uuid](https://github.com/sqlite/sqlite/blob/master/ext/misc/uuid.c)
- [time](../sqlite3/time.c)
See the [configuration options](../sqlite3/sqlite_cfg.h),
See the [configuration options](../sqlite3/sqlite_opt.h),
and [patches](../sqlite3) applied.
Built using [`wasi-sdk`](https://github.com/WebAssembly/wasi-sdk),
and [`binaryen`](https://github.com/WebAssembly/binaryen).
The build is easily reproducible, and verifiable, using
[Artifact Attestations](https://github.com/ncruces/go-sqlite3/attestations).
[Artifact Attestations](https://github.com/ncruces/go-sqlite3/attestations).
### Customizing the build
You can use your own custom build of SQLite.
Examples of custom builds of SQLite are:
- [`github.com/ncruces/go-sqlite3/embed/bcw2`](https://github.com/ncruces/go-sqlite3/tree/main/embed/bcw2)
built from a branch supporting [`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md)
and [Wal2](https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md).
- [`github.com/asg017/sqlite-vec-go-bindings/ncruces`](https://github.com/asg017/sqlite-vec-go-bindings)
which includes the [`sqlite-vec`](https://github.com/asg017/sqlite-vec) vector search extension.

2
embed/bcw2/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
build/
sqlite/

22
embed/bcw2/README.md Normal file
View File

@@ -0,0 +1,22 @@
# Embeddable Wasm build of SQLite
This folder includes an alternative embeddable Wasm build of SQLite,
which includes the experimental
[`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md) and
[Wal2](https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md) patches.
It also enables the optional
[`UPDATE … ORDER BY … LIMIT`](https://sqlite.org/lang_update.html#optional_limit_and_order_by_clauses) and
[`DELETE … ORDER BY … LIMIT`](https://sqlite.org/lang_delete.html#optional_limit_and_order_by_clauses) clauses,
and the [`WITHIN GROUP ORDER BY`](https://sqlite.org/compile.html#enable_ordered_set_aggregates) aggregate syntax.
> [!IMPORTANT]
> This package is experimental.
> It is built from the `bedrock` branch of SQLite,
> since that is _currently_ the most stable, maintained branch to include these features.
> [!CAUTION]
> The Wal2 journaling mode creates databases that other versions of SQLite cannot access.
The build is easily reproducible, and verifiable, using
[Artifact Attestations](https://github.com/ncruces/go-sqlite3/attestations).

BIN
embed/bcw2/bcw2.wasm Executable file

Binary file not shown.

59
embed/bcw2/bcw2_test.go Normal file
View File

@@ -0,0 +1,59 @@
package bcw2
import (
"path/filepath"
"testing"
"github.com/ncruces/go-sqlite3/driver"
"github.com/ncruces/go-sqlite3/ext/stats"
"github.com/ncruces/go-sqlite3/vfs"
)
func Test_bcw2(t *testing.T) {
if !vfs.SupportsSharedMemory {
t.Skip("skipping without shared memory")
}
tmp := filepath.ToSlash(filepath.Join(t.TempDir(), "test.db"))
db, err := driver.Open("file:"+tmp+"?_pragma=journal_mode(wal2)&_txlock=concurrent", stats.Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
_, err = tx.Exec(`CREATE TABLE test (col)`)
if err != nil {
t.Fatal(err)
}
_, err = tx.Exec(`DELETE FROM test LIMIT 1`)
if err != nil {
t.Fatal(err)
}
_, err = tx.Exec(`SELECT median() WITHIN GROUP (ORDER BY col) FROM test`)
if err != nil {
t.Fatal(err)
}
err = tx.Commit()
if err != nil {
t.Fatal(err)
}
var version string
err = db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
if err != nil {
t.Fatal(err)
}
if version != "3.50.0" {
t.Error(version)
}
}

66
embed/bcw2/build.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
cd -P -- "$(dirname -- "$0")"
ROOT=../../
BINARYEN="$ROOT/tools/binaryen/bin"
WASI_SDK="$ROOT/tools/wasi-sdk/bin"
trap 'rm -rf build/ sqlite/ bcw2.tmp' EXIT
mkdir -p build/ext/
cp "$ROOT"/sqlite3/*.[ch] build/
cp "$ROOT"/sqlite3/*.patch build/
# https://sqlite.org/src/info/c09656c62155a6e8
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=c09656c6 | tar xz
cd sqlite
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c "OPTS=-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT -DSQLITE_ENABLE_ORDERED_SET_AGGREGATES"
else
sh configure --enable-update-limit
OPTS=-DSQLITE_ENABLE_ORDERED_SET_AGGREGATES make sqlite3.c
fi
cd ~-
mv sqlite/sqlite3.c build/
mv sqlite/sqlite3.h build/
mv sqlite/sqlite3ext.h build/
mv sqlite/ext/misc/anycollseq.c build/ext/
mv sqlite/ext/misc/base64.c build/ext/
mv sqlite/ext/misc/decimal.c build/ext/
mv sqlite/ext/misc/ieee754.c build/ext/
mv sqlite/ext/misc/regexp.c build/ext/
mv sqlite/ext/misc/series.c build/ext/
mv sqlite/ext/misc/spellfix.c build/ext/
mv sqlite/ext/misc/uint.c build/ext/
cd build
cat *.patch | patch -p0 --no-backup-if-mismatch
cd ~-
"$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -g0 -O2 \
-Wall -Wextra -Wno-unused-parameter -Wno-unused-function \
-o bcw2.wasm "build/main.c" \
-I"build" \
-mexec-model=reactor \
-msimd128 -mmutable-globals -mmultivalue \
-mbulk-memory -mreference-types \
-mnontrapping-fptoint -msign-ext \
-fno-stack-protector -fno-stack-clash-protection \
-Wl,--stack-first \
-Wl,--import-undefined \
-Wl,--initial-memory=327680 \
-D_HAVE_SQLITE_CONFIG_H \
-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \
-DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \
$(awk '{print "-Wl,--export="$0}' ../exports.txt)
"$BINARYEN/wasm-ctor-eval" -g -c _initialize bcw2.wasm -o bcw2.tmp
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
bcw2.tmp -o bcw2.wasm \
--enable-simd --enable-mutable-globals --enable-multivalue \
--enable-bulk-memory --enable-reference-types \
--enable-nontrapping-float-to-int --enable-sign-ext

14
embed/bcw2/go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/ncruces/go-sqlite3/embed/bcw2
go 1.22.0
toolchain go1.24.0
require github.com/ncruces/go-sqlite3 v0.23.1
require (
github.com/ncruces/julianday v1.0.0 // indirect
github.com/ncruces/sort v0.1.5 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
golang.org/x/sys v0.30.0 // indirect
)

12
embed/bcw2/go.sum Normal file
View File

@@ -0,0 +1,12 @@
github.com/ncruces/go-sqlite3 v0.23.1 h1:zGAd76q+Tr18z/xKGatUlzBQdjR3J+rexfANUcjAgkY=
github.com/ncruces/go-sqlite3 v0.23.1/go.mod h1:Xg3FyAZl25HcBSFmcbymdfoTqD7jRnBUmv1jSrbIjdE=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/ncruces/sort v0.1.5 h1:fiFWXXAqKI8QckPf/6hu/bGFwcEPrirIOFaJqWujs4k=
github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=

24
embed/bcw2/init.go Normal file
View File

@@ -0,0 +1,24 @@
// Package bcw2 embeds SQLite into your application.
//
// Importing package bcw2 initializes the [sqlite3.Binary] variable
// with a build of SQLite that includes the [BEGIN CONCURRENT] and [Wal2] patches:
//
// import _ "github.com/ncruces/go-sqlite3/embed/bcw2"
//
// [BEGIN CONCURRENT]: https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md
// [Wal2]: https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md
package bcw2
import (
_ "embed"
"unsafe"
"github.com/ncruces/go-sqlite3"
)
//go:embed bcw2.wasm
var binary string
func init() {
sqlite3.Binary = unsafe.Slice(unsafe.StringData(binary), len(binary))
}

View File

@@ -4,26 +4,27 @@ set -euo pipefail
cd -P -- "$(dirname -- "$0")"
ROOT=../
BINARYEN="$ROOT/tools/binaryen-version_117/bin"
WASI_SDK="$ROOT/tools/wasi-sdk-22.0/bin"
BINARYEN="$ROOT/tools/binaryen/bin"
WASI_SDK="$ROOT/tools/wasi-sdk/bin"
"$WASI_SDK/clang" --target=wasm32-wasi -std=c17 -flto -g0 -O2 \
trap 'rm -f sqlite3.tmp' EXIT
"$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -g0 -O2 \
-Wall -Wextra -Wno-unused-parameter -Wno-unused-function \
-o sqlite3.wasm "$ROOT/sqlite3/main.c" \
-I"$ROOT/sqlite3" \
-mexec-model=reactor \
-msimd128 -mmutable-globals \
-msimd128 -mmutable-globals -mmultivalue \
-mbulk-memory -mreference-types \
-mnontrapping-fptoint -msign-ext \
-fno-stack-protector -fno-stack-clash-protection \
-Wl,--initial-memory=327680 \
-Wl,--stack-first \
-Wl,--import-undefined \
-Wl,--initial-memory=327680 \
-D_HAVE_SQLITE_CONFIG_H \
-DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \
$(awk '{print "-Wl,--export="$0}' exports.txt)
trap 'rm -f sqlite3.tmp' EXIT
"$BINARYEN/wasm-ctor-eval" -g -c _initialize sqlite3.wasm -o sqlite3.tmp
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
sqlite3.tmp -o sqlite3.wasm \

View File

@@ -1,7 +1,4 @@
aligned_alloc
free
malloc
malloc_destructor
sqlite3_anycollseq_init
sqlite3_autovacuum_pages_go
sqlite3_backup_finish
@@ -9,7 +6,7 @@ sqlite3_backup_init
sqlite3_backup_pagecount
sqlite3_backup_remaining
sqlite3_backup_step
sqlite3_bind_blob64
sqlite3_bind_blob_go
sqlite3_bind_double
sqlite3_bind_int64
sqlite3_bind_null
@@ -17,7 +14,7 @@ sqlite3_bind_parameter_count
sqlite3_bind_parameter_index
sqlite3_bind_parameter_name
sqlite3_bind_pointer_go
sqlite3_bind_text64
sqlite3_bind_text_go
sqlite3_bind_value
sqlite3_bind_zeroblob64
sqlite3_blob_bytes
@@ -54,33 +51,43 @@ sqlite3_create_collation_go
sqlite3_create_function_go
sqlite3_create_module_go
sqlite3_create_window_function_go
sqlite3_data_count
sqlite3_database_file_object
sqlite3_db_cacheflush
sqlite3_db_config
sqlite3_db_filename
sqlite3_db_name
sqlite3_db_readonly
sqlite3_db_release_memory
sqlite3_db_status
sqlite3_declare_vtab
sqlite3_errcode
sqlite3_errmsg
sqlite3_error_offset
sqlite3_errstr
sqlite3_exec
sqlite3_expanded_sql
sqlite3_file_control
sqlite3_filename_database
sqlite3_filename_journal
sqlite3_filename_wal
sqlite3_finalize
sqlite3_free
sqlite3_get_autocommit
sqlite3_get_auxdata
sqlite3_hard_heap_limit64
sqlite3_interrupt
sqlite3_invoke_busy_handler_go
sqlite3_last_insert_rowid
sqlite3_limit
sqlite3_malloc64
sqlite3_open_v2
sqlite3_overload_function
sqlite3_prepare_v3
sqlite3_progress_handler_go
sqlite3_realloc64
sqlite3_reset
sqlite3_result_blob64
sqlite3_result_blob_go
sqlite3_result_double
sqlite3_result_error
sqlite3_result_error_code
@@ -89,27 +96,30 @@ sqlite3_result_error_toobig
sqlite3_result_int64
sqlite3_result_null
sqlite3_result_pointer_go
sqlite3_result_text64
sqlite3_result_text_go
sqlite3_result_value
sqlite3_result_zeroblob64
sqlite3_rollback_hook_go
sqlite3_set_authorizer_go
sqlite3_set_auxdata_go
sqlite3_set_last_insert_rowid
sqlite3_soft_heap_limit64
sqlite3_step
sqlite3_stmt_busy
sqlite3_stmt_readonly
sqlite3_stmt_status
sqlite3_table_column_metadata
sqlite3_total_changes64
sqlite3_trace_go
sqlite3_txn_state
sqlite3_update_hook_go
sqlite3_uri_key
sqlite3_uri_parameter
sqlite3_value_blob
sqlite3_value_bytes
sqlite3_value_double
sqlite3_value_dup
sqlite3_value_free
sqlite3_value_frombind
sqlite3_value_int64
sqlite3_value_nochange
sqlite3_value_numeric_type

View File

@@ -8,13 +8,14 @@ package embed
import (
_ "embed"
"unsafe"
"github.com/ncruces/go-sqlite3"
)
//go:embed sqlite3.wasm
var binary []byte
var binary string
func init() {
sqlite3.Binary = binary
sqlite3.Binary = unsafe.Slice(unsafe.StringData(binary), len(binary))
}

25
embed/init_test.go Normal file
View File

@@ -0,0 +1,25 @@
package embed
import (
"testing"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Test_init(t *testing.T) {
db, err := driver.Open("file:/test.db?vfs=memdb")
if err != nil {
t.Fatal(err)
}
defer db.Close()
var version string
err = db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
if err != nil {
t.Fatal(err)
}
if version != "3.49.1" {
t.Error(version)
}
}

Binary file not shown.

View File

@@ -15,7 +15,7 @@ type Error struct {
str string
msg string
sql string
code uint64
code res_t
}
// Code returns the primary error code for this error.
@@ -106,6 +106,11 @@ func (e ErrorCode) Temporary() bool {
return e == BUSY
}
// ExtendedCode returns the extended error code for this error.
func (e ErrorCode) ExtendedCode() ExtendedErrorCode {
return ExtendedErrorCode(e)
}
// Error implements the error interface.
func (e ExtendedErrorCode) Error() string {
return util.ErrorCodeString(uint32(e))
@@ -136,27 +141,32 @@ func (e ExtendedErrorCode) Timeout() bool {
return e == BUSY_TIMEOUT
}
func errorCode(err error, def ErrorCode) (msg string, code uint32) {
// Code returns the primary error code for this error.
func (e ExtendedErrorCode) Code() ErrorCode {
return ErrorCode(e)
}
func errorCode(err error, def ErrorCode) (msg string, code res_t) {
switch code := err.(type) {
case nil:
return "", _OK
case ErrorCode:
return "", uint32(code)
return "", res_t(code)
case xErrorCode:
return "", uint32(code)
return "", res_t(code)
case *Error:
return code.msg, uint32(code.code)
return code.msg, res_t(code.code)
}
var ecode ErrorCode
var xcode xErrorCode
switch {
case errors.As(err, &xcode):
code = uint32(xcode)
code = res_t(xcode)
case errors.As(err, &ecode):
code = uint32(ecode)
code = res_t(ecode)
default:
code = uint32(def)
code = res_t(def)
}
return err.Error(), code
}

View File

@@ -2,6 +2,7 @@ package sqlite3
import (
"errors"
"fmt"
"strings"
"testing"
@@ -10,7 +11,7 @@ import (
func Test_assertErr(t *testing.T) {
err := util.AssertErr()
if s := err.Error(); !strings.HasPrefix(s, "sqlite3: assertion failed") || !strings.HasSuffix(s, "error_test.go:12)") {
if s := err.Error(); !strings.HasPrefix(s, "sqlite3: assertion failed") || !strings.HasSuffix(s, "error_test.go:13)") {
t.Errorf("got %q", s)
}
}
@@ -58,14 +59,14 @@ func TestError_Temporary(t *testing.T) {
tests := []struct {
name string
code uint64
code res_t
want bool
}{
{"ERROR", uint64(ERROR), false},
{"BUSY", uint64(BUSY), true},
{"BUSY_RECOVERY", uint64(BUSY_RECOVERY), true},
{"BUSY_SNAPSHOT", uint64(BUSY_SNAPSHOT), true},
{"BUSY_TIMEOUT", uint64(BUSY_TIMEOUT), true},
{"ERROR", res_t(ERROR), false},
{"BUSY", res_t(BUSY), true},
{"BUSY_RECOVERY", res_t(BUSY_RECOVERY), true},
{"BUSY_SNAPSHOT", res_t(BUSY_SNAPSHOT), true},
{"BUSY_TIMEOUT", res_t(BUSY_TIMEOUT), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -96,14 +97,14 @@ func TestError_Timeout(t *testing.T) {
tests := []struct {
name string
code uint64
code res_t
want bool
}{
{"ERROR", uint64(ERROR), false},
{"BUSY", uint64(BUSY), false},
{"BUSY_RECOVERY", uint64(BUSY_RECOVERY), false},
{"BUSY_SNAPSHOT", uint64(BUSY_SNAPSHOT), false},
{"BUSY_TIMEOUT", uint64(BUSY_TIMEOUT), true},
{"ERROR", res_t(ERROR), false},
{"BUSY", res_t(BUSY), false},
{"BUSY_RECOVERY", res_t(BUSY_RECOVERY), false},
{"BUSY_SNAPSHOT", res_t(BUSY_SNAPSHOT), false},
{"BUSY_TIMEOUT", res_t(BUSY_TIMEOUT), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -135,8 +136,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("sqlite3_errstr", uint64(i))
want += util.ReadString(db.mod, uint32(r), _MAX_NAME)
ptr := ptr_t(db.call("sqlite3_errstr", stk_t(i)))
want += util.ReadString(db.mod, ptr, _MAX_NAME)
got := ErrorCode(i).Error()
if got != want {
@@ -157,8 +158,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("sqlite3_errstr", uint64(i))
want += util.ReadString(db.mod, uint32(r), _MAX_NAME)
ptr := ptr_t(db.call("sqlite3_errstr", stk_t(i)))
want += util.ReadString(db.mod, ptr, _MAX_NAME)
got := ExtendedErrorCode(i).Error()
if got != want {
@@ -166,3 +167,32 @@ func Test_ExtendedErrorCode_Error(t *testing.T) {
}
}
}
func Test_errorCode(t *testing.T) {
tests := []struct {
arg error
wantMsg string
wantCode res_t
}{
{nil, "", _OK},
{ERROR, "", util.ERROR},
{IOERR, "", util.IOERR},
{IOERR_READ, "", util.IOERR_READ},
{&Error{code: util.ERROR}, "", util.ERROR},
{fmt.Errorf("%w", ERROR), ERROR.Error(), util.ERROR},
{fmt.Errorf("%w", IOERR), IOERR.Error(), util.IOERR},
{fmt.Errorf("%w", IOERR_READ), IOERR_READ.Error(), util.IOERR_READ},
{fmt.Errorf("error"), "error", util.ERROR},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
gotMsg, gotCode := errorCode(tt.arg, ERROR)
if gotMsg != tt.wantMsg {
t.Errorf("errorCode() gotMsg = %q, want %q", gotMsg, tt.wantMsg)
}
if gotCode != tt.wantCode {
t.Errorf("errorCode() gotCode = %d, want %d", gotCode, tt.wantCode)
}
})
}
}

48
ext/README.md Normal file
View File

@@ -0,0 +1,48 @@
# Go SQLite Extensions
This folder collects optional SQLite extensions
you can load into your database connections.
### Extensions
- [`github.com/ncruces/go-sqlite3/ext/array`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/array)
provides the [`array`](https://sqlite.org/carray.html) table-valued function.
- [`github.com/ncruces/go-sqlite3/ext/blobio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blobio)
simplifies [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html).
- [`github.com/ncruces/go-sqlite3/ext/bloom`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/bloom)
provides a [Bloom filter](https://github.com/nalgeon/sqlean/issues/27#issuecomment-1002267134) virtual table.
- [`github.com/ncruces/go-sqlite3/ext/closure`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/closure)
provides a transitive closure virtual table.
- [`github.com/ncruces/go-sqlite3/ext/csv`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/csv)
reads [comma-separated values](https://sqlite.org/csv.html).
- [`github.com/ncruces/go-sqlite3/ext/fileio`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/fileio)
reads, writes and lists files.
- [`github.com/ncruces/go-sqlite3/ext/hash`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/hash)
provides cryptographic hash functions.
- [`github.com/ncruces/go-sqlite3/ext/lines`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/lines)
reads data [line-by-line](https://github.com/asg017/sqlite-lines).
- [`github.com/ncruces/go-sqlite3/ext/pivot`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/pivot)
creates [pivot tables](https://github.com/jakethaw/pivot_vtab).
- [`github.com/ncruces/go-sqlite3/ext/regexp`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/regexp)
provides regular expression functions.
- [`github.com/ncruces/go-sqlite3/ext/serdes`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/serdes)
(de)serializes databases.
- [`github.com/ncruces/go-sqlite3/ext/statement`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/statement)
creates [parameterized views](https://github.com/0x09/sqlite-statement-vtab).
- [`github.com/ncruces/go-sqlite3/ext/stats`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
provides [statistics](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html) functions.
- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions.
- [`github.com/ncruces/go-sqlite3/ext/uuid`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/uuid)
generates [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier).
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
maps multidimensional data to one dimension.
### Pakages
These packages may also be useful to work with SQLite:
- [`github.com/ncruces/decimal`](https://pkg.go.dev/github.com/ncruces/decimal)
decimal arithmetic.
- [`github.com/ncruces/julianday`](https://pkg.go.dev/github.com/ncruces/julianday)
Julian day math.

View File

@@ -15,8 +15,8 @@ import (
// The argument must be bound to a Go slice or array of
// ints, floats, bools, strings or byte slices,
// using [sqlite3.BindPointer] or [sqlite3.Pointer].
func Register(db *sqlite3.Conn) {
sqlite3.CreateModule(db, "array", nil,
func Register(db *sqlite3.Conn) error {
return sqlite3.CreateModule(db, "array", nil,
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (array, error) {
err := db.DeclareVTab(`CREATE TABLE x(value, array HIDDEN)`)
return array{}, err
@@ -62,7 +62,7 @@ func (c *cursor) RowID() (int64, error) {
return int64(c.rowID), nil
}
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
if n != 0 {
return nil
}

View File

@@ -12,13 +12,11 @@ import (
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/array"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Example_driver() {
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
array.Register(c)
return nil
})
db, err := driver.Open("file:/test.db?vfs=memdb", array.Register)
if err != nil {
log.Fatal(err)
}
@@ -53,14 +51,14 @@ func Example_driver() {
}
func Example() {
sqlite3.AutoExtension(array.Register)
db, err := sqlite3.Open(":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
array.Register(db)
stmt, _, err := db.Prepare(`
SELECT name
FROM pragma_function_list
@@ -90,11 +88,9 @@ func Example() {
func Test_cursor_Column(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
array.Register(c)
return nil
})
db, err := driver.Open(tmp, array.Register)
if err != nil {
t.Fatal(err)
}
@@ -126,7 +122,7 @@ func Test_cursor_Column(t *testing.T) {
want = want[1:]
}
if err := rows.Err(); err != nil {
log.Fatal(err)
t.Fatal(err)
}
}
@@ -139,7 +135,10 @@ func Test_array_errors(t *testing.T) {
}
defer db.Close()
array.Register(db)
err = array.Register(db)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`SELECT * FROM array()`)
if err == nil {

View File

@@ -11,11 +11,11 @@ import (
// Register registers the SQL functions:
//
// readblob(schema, table, column, rowid, offset, n)
// readblob(schema, table, column, rowid, offset, n/writer)
//
// Reads n bytes of a blob, starting at offset.
//
// writeblob(schema, table, column, rowid, offset, data)
// writeblob(schema, table, column, rowid, offset, data/reader)
//
// Writes data into a blob, at the given offset.
//
@@ -27,63 +27,83 @@ import (
// using [sqlite3.BindPointer] or [sqlite3.Pointer].
// The optional args will be passed to the callback,
// along with the [sqlite3.Blob] handle.
// The [sqlite3.Blob] handle is only valid during
// the execution of the callback. Callers cannot
// read or write to the handle after the callback
// exits.
//
// https://sqlite.org/c3ref/blob.html
func Register(db *sqlite3.Conn) {
db.CreateFunction("readblob", 6, 0, readblob)
db.CreateFunction("writeblob", 6, 0, writeblob)
db.CreateFunction("openblob", -1, 0, openblob)
func Register(db *sqlite3.Conn) error {
return errors.Join(
db.CreateFunction("readblob", 6, 0, readblob),
db.CreateFunction("writeblob", 6, 0, writeblob),
db.CreateFunction("openblob", -1, 0, openblob))
}
// OpenCallback is the type for the openblob callback.
type OpenCallback func(*sqlite3.Blob, ...sqlite3.Value) error
func readblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
_ = arg[5] // bounds check
blob, err := getAuxBlob(ctx, arg, false)
if err != nil {
ctx.ResultError(err)
return
return // notest
}
_, err = blob.Seek(arg[4].Int64(), io.SeekStart)
if err != nil {
ctx.ResultError(err)
return
return // notest
}
n := arg[5].Int64()
if n <= 0 {
return
if p, ok := arg[5].Pointer().(io.Writer); ok {
var n int64
n, err = blob.WriteTo(p)
ctx.ResultInt64(n)
} else {
n := arg[5].Int64()
if n <= 0 {
return
}
buf := make([]byte, n)
_, err = io.ReadFull(blob, buf)
ctx.ResultBlob(buf)
}
buf := make([]byte, n)
_, err = io.ReadFull(blob, buf)
if err != nil {
ctx.ResultError(err)
return
return // notest
}
ctx.ResultBlob(buf)
setAuxBlob(ctx, blob, false)
}
func writeblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
_ = arg[5] // bounds check
blob, err := getAuxBlob(ctx, arg, true)
if err != nil {
ctx.ResultError(err)
return
return // notest
}
_, err = blob.Seek(arg[4].Int64(), io.SeekStart)
if err != nil {
ctx.ResultError(err)
return
return // notest
}
_, err = blob.Write(arg[5].RawBlob())
if p, ok := arg[5].Pointer().(io.Reader); ok {
var n int64
n, err = blob.ReadFrom(p)
ctx.ResultInt64(n)
} else {
_, err = blob.Write(arg[5].RawBlob())
}
if err != nil {
ctx.ResultError(err)
return
return // notest
}
setAuxBlob(ctx, blob, false)
@@ -98,14 +118,14 @@ func openblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
blob, err := getAuxBlob(ctx, arg, arg[4].Bool())
if err != nil {
ctx.ResultError(err)
return
return // notest
}
fn := arg[5].Pointer().(OpenCallback)
err = fn(blob, arg[6:]...)
if err != nil {
ctx.ResultError(err)
return
return // notest
}
setAuxBlob(ctx, blob, true)

View File

@@ -4,7 +4,8 @@ import (
"io"
"log"
"os"
"reflect"
"slices"
"strings"
"testing"
"github.com/ncruces/go-sqlite3"
@@ -18,10 +19,7 @@ import (
func Example() {
// Open the database, registering the extension.
db, err := driver.Open("file:/test.db?vfs=memdb", func(conn *sqlite3.Conn) error {
blobio.Register(conn)
return nil
})
db, err := driver.Open("file:/test.db?vfs=memdb", blobio.Register)
if err != nil {
log.Fatal(err)
@@ -36,23 +34,26 @@ func Example() {
const message = "Hello BLOB!"
// Create the BLOB.
_, err = db.Exec(`INSERT INTO test VALUES (?)`, sqlite3.ZeroBlob(len(message)))
r, err := db.Exec(`INSERT INTO test VALUES (?)`, sqlite3.ZeroBlob(len(message)))
if err != nil {
log.Fatal(err)
}
id, err := r.LastInsertId()
if err != nil {
log.Fatal(err)
}
// Write the BLOB.
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', last_insert_rowid(), 0, ?)`, message)
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', ?, 0, ?)`,
id, message)
if err != nil {
log.Fatal(err)
}
// Read the BLOB.
_, err = db.Exec(`SELECT openblob('main', 'test', 'col', rowid, false, ?) FROM test`,
sqlite3.Pointer[blobio.OpenCallback](func(blob *sqlite3.Blob, _ ...sqlite3.Value) error {
_, err = io.Copy(os.Stdout, blob)
return err
}))
_, err = db.Exec(`SELECT readblob('main', 'test', 'col', ?, 0, ?)`,
id, sqlite3.Pointer(os.Stdout))
if err != nil {
log.Fatal(err)
}
@@ -60,6 +61,12 @@ func Example() {
// Hello BLOB!
}
func TestMain(m *testing.M) {
sqlite3.AutoExtension(blobio.Register)
sqlite3.AutoExtension(array.Register)
m.Run()
}
func Test_readblob(t *testing.T) {
t.Parallel()
@@ -69,9 +76,6 @@ func Test_readblob(t *testing.T) {
}
defer db.Close()
blobio.Register(db)
array.Register(db)
err = db.Exec(`SELECT readblob()`)
if err == nil {
t.Fatal("want error")
@@ -79,44 +83,135 @@ func Test_readblob(t *testing.T) {
t.Log(err)
}
err = db.Exec(`SELECT readblob('main', 'test1', 'col', 1, 1, 1)`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`
CREATE TABLE test1 (col);
CREATE TABLE test2 (col);
INSERT INTO test1 VALUES (x'cafe');
INSERT INTO test1 VALUES (x'dead');
INSERT INTO test2 VALUES (x'babe');
`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT readblob('main', value, 'col', 1, 1, 1) FROM array(?)`)
err = db.Exec(`SELECT readblob('main', 'test1', 'col', 1, -1, 1)`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`SELECT readblob('main', 'test1', 'col', 1, 1, 0)`)
if err != nil {
t.Log(err)
}
tests := []struct {
name string
sql string
want1 string
want2 string
}{
{"rows", `SELECT readblob('main', 'test1', 'col', rowid, 1, 1) FROM test1`, "\xfe", "\xad"},
{"tables", `SELECT readblob('main', value, 'col', 1, 1, 1) FROM array(?)`, "\xfe", "\xbe"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, _, err := db.Prepare(tt.sql)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if stmt.BindCount() == 1 {
err = stmt.BindPointer(1, []string{"test1", "test2"})
if err != nil {
t.Fatal(err)
}
}
if stmt.Step() {
got := stmt.ColumnText(0)
if got != tt.want1 {
t.Errorf("got %q", got)
}
}
if stmt.Step() {
got := stmt.ColumnText(0)
if got != tt.want2 {
t.Errorf("got %q", got)
}
}
err = stmt.Err()
if err != nil {
t.Fatal(err)
}
})
}
}
func Test_writeblob(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
err = db.Exec(`SELECT writeblob()`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`SELECT writeblob('main', 'test', 'col', 1, 1, x'')`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`
CREATE TABLE test (col);
INSERT INTO test VALUES (x'cafe');
`)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`SELECT writeblob('main', 'test', 'col', 1, -1, x'')`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
stmt, _, err := db.Prepare(`SELECT writeblob('main', 'test', 'col', 1, 0, ?)`)
if err != nil {
t.Log(err)
}
defer stmt.Close()
err = stmt.BindPointer(1, []string{"test1", "test2"})
err = stmt.BindPointer(1, strings.NewReader("\xba\xbe"))
if err != nil {
t.Fatal(err)
t.Log(err)
}
if stmt.Step() {
got := stmt.ColumnText(0)
if got != "\xfe" {
t.Errorf("got %q", got)
}
}
if stmt.Step() {
got := stmt.ColumnText(0)
if got != "\xbe" {
t.Errorf("got %q", got)
}
}
err = stmt.Err()
err = stmt.Exec()
if err != nil {
t.Fatal(err)
t.Log(err)
}
}
@@ -129,9 +224,6 @@ func Test_openblob(t *testing.T) {
}
defer db.Close()
blobio.Register(db)
array.Register(db)
err = db.Exec(`SELECT openblob()`)
if err == nil {
t.Fatal("want error")
@@ -139,6 +231,13 @@ func Test_openblob(t *testing.T) {
t.Log(err)
}
err = db.Exec(`SELECT openblob('main', 'test1', 'col', 1, false, NULL)`)
if err == nil {
t.Fatal("want error")
} else {
t.Log(err)
}
err = db.Exec(`
CREATE TABLE test1 (col);
CREATE TABLE test2 (col);
@@ -179,7 +278,7 @@ func Test_openblob(t *testing.T) {
}
want := []string{"\xca\xfe", "\xba\xbe"}
if !reflect.DeepEqual(got, want) {
if !slices.Equal(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}

View File

@@ -7,21 +7,22 @@
package bloom
import (
"errors"
"fmt"
"io"
"math"
"strconv"
"github.com/dchest/siphash"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Register registers the bloom_filter virtual table:
//
// CREATE VIRTUAL TABLE foo USING bloom_filter(nElements, falseProb, kHashes)
func Register(db *sqlite3.Conn) {
sqlite3.CreateModule(db, "bloom_filter", create, connect)
func Register(db *sqlite3.Conn) error {
return sqlite3.CreateModule(db, "bloom_filter", create, connect)
}
type bloom struct {
@@ -34,7 +35,7 @@ type bloom struct {
}
func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom, err error) {
t := bloom{
b := bloom{
db: db,
schema: schema,
storage: table + "_storage",
@@ -47,41 +48,47 @@ func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom,
return nil, err
}
if nelem <= 0 {
return nil, errors.New("bloom: number of elements in filter must be positive")
return nil, util.ErrorString("bloom: number of elements in filter must be positive")
}
} else {
nelem = 100
}
if len(arg) > 1 {
t.prob, err = strconv.ParseFloat(arg[1], 64)
b.prob, err = strconv.ParseFloat(arg[1], 64)
if err != nil {
return nil, err
}
if t.prob <= 0 || t.prob >= 1 {
return nil, errors.New("bloom: probability must be in the range (0,1)")
if b.prob <= 0 || b.prob >= 1 {
return nil, util.ErrorString("bloom: probability must be in the range (0,1)")
}
} else {
t.prob = 0.01
b.prob = 0.01
}
if len(arg) > 2 {
t.hashes, err = strconv.Atoi(arg[2])
b.hashes, err = strconv.Atoi(arg[2])
if err != nil {
return nil, err
}
if t.hashes <= 0 {
return nil, errors.New("bloom: number of hash functions must be positive")
if b.hashes <= 0 {
return nil, util.ErrorString("bloom: number of hash functions must be positive")
}
} else {
t.hashes = max(1, numHashes(t.prob))
b.hashes = max(1, numHashes(b.prob))
}
t.bytes = numBytes(nelem, t.prob)
b.bytes = numBytes(nelem, b.prob)
err = db.DeclareVTab(
`CREATE TABLE x(present, word HIDDEN NOT NULL PRIMARY KEY) WITHOUT ROWID`)
if err != nil {
return nil, err
}
err = db.Exec(fmt.Sprintf(
`CREATE TABLE %s.%s (data BLOB, p REAL, n INTEGER, m INTEGER, k INTEGER)`,
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage)))
sqlite3.QuoteIdentifier(b.schema), sqlite3.QuoteIdentifier(b.storage)))
if err != nil {
return nil, err
}
@@ -92,23 +99,17 @@ func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom,
err = db.Exec(fmt.Sprintf(
`INSERT INTO %s.%s (rowid, data, p, n, m, k)
VALUES (1, zeroblob(%d), %f, %d, %d, %d)`,
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage),
t.bytes, t.prob, nelem, 8*t.bytes, t.hashes))
sqlite3.QuoteIdentifier(b.schema), sqlite3.QuoteIdentifier(b.storage),
b.bytes, b.prob, nelem, 8*b.bytes, b.hashes))
if err != nil {
b.Destroy()
return nil, err
}
err = db.DeclareVTab(
`CREATE TABLE x(present, word HIDDEN NOT NULL PRIMARY KEY) WITHOUT ROWID`)
if err != nil {
t.Destroy()
return nil, err
}
return &t, nil
return &b, nil
}
func connect(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom, err error) {
t := bloom{
b := bloom{
db: db,
schema: schema,
storage: table + "_storage",
@@ -122,23 +123,23 @@ func connect(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom
load, _, err := db.Prepare(fmt.Sprintf(
`SELECT m/8, p, k FROM %s.%s WHERE rowid = 1`,
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage)))
sqlite3.QuoteIdentifier(b.schema), sqlite3.QuoteIdentifier(b.storage)))
if err != nil {
return nil, err
}
defer load.Close()
if !load.Step() {
if err = load.Err(); err == nil {
err = sqlite3.CORRUPT_VTAB
if err := load.Err(); err != nil {
return nil, err
}
return nil, err
return nil, sqlite3.CORRUPT_VTAB
}
t.bytes = load.ColumnInt64(0)
t.prob = load.ColumnFloat(1)
t.hashes = load.ColumnInt(2)
return &t, nil
b.bytes = load.ColumnInt64(0)
b.prob = load.ColumnFloat(1)
b.hashes = load.ColumnInt(2)
return &b, nil
}
func (b *bloom) Destroy() error {
@@ -160,7 +161,9 @@ func (b *bloom) Rename(new string) error {
return err
}
func (t *bloom) ShadowTables() {}
func (t *bloom) ShadowTables() {
// notest // not meant to be called
}
func (t *bloom) Integrity(schema, table string, flags int) error {
load, _, err := t.db.Prepare(fmt.Sprintf(
@@ -171,7 +174,7 @@ func (t *bloom) Integrity(schema, table string, flags int) error {
}
defer load.Close()
err = errors.New("bloom: invalid parameters")
err = util.ErrorString("bloom: invalid parameters")
if !load.Step() {
return err
}
@@ -196,10 +199,10 @@ func (t *bloom) Integrity(schema, table string, flags int) error {
}
func (b *bloom) BestIndex(idx *sqlite3.IndexInfo) error {
for n, cst := range idx.Constraint {
for i, cst := range idx.Constraint {
if cst.Usable && cst.Column == 1 &&
cst.Op == sqlite3.INDEX_CONSTRAINT_EQ {
idx.ConstraintUsage[n].ArgvIndex = 1
idx.ConstraintUsage[i].ArgvIndex = 1
idx.OrderByConsumed = true
idx.EstimatedRows = 1
idx.EstimatedCost = float64(b.hashes)
@@ -213,11 +216,14 @@ func (b *bloom) BestIndex(idx *sqlite3.IndexInfo) error {
func (b *bloom) Update(arg ...sqlite3.Value) (rowid int64, err error) {
if arg[0].Type() != sqlite3.NULL {
if len(arg) == 1 {
return 0, errors.New("bloom: elements cannot be deleted")
return 0, util.ErrorString("bloom: elements cannot be deleted")
}
return 0, errors.New("bloom: elements cannot be updated")
return 0, util.ErrorString("bloom: elements cannot be updated")
}
if arg[2].NoChange() {
return 0, nil
}
blob := arg[2].RawBlob()
f, err := b.db.OpenBlob(b.schema, b.storage, "data", 1, true)
@@ -226,7 +232,7 @@ func (b *bloom) Update(arg ...sqlite3.Value) (rowid int64, err error) {
}
defer f.Close()
for n := 0; n < b.hashes; n++ {
for n := range b.hashes {
hash := calcHash(n, blob)
hash %= uint64(b.bytes * 8)
bitpos := byte(hash % 8)
@@ -262,15 +268,11 @@ func (b *bloom) Open() (sqlite3.VTabCursor, error) {
type cursor struct {
*bloom
eof bool
arg *sqlite3.Value
eof bool
}
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
if len(arg) != 1 {
return nil
}
c.eof = false
c.arg = &arg[0]
blob := arg[0].RawBlob()
@@ -302,7 +304,10 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
return nil
}
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
if ctx.VTabNoChange() {
return nil
}
switch n {
case 0:
ctx.ResultBool(true)
@@ -322,6 +327,7 @@ func (c *cursor) EOF() bool {
}
func (c *cursor) RowID() (int64, error) {
// notest // WITHOUT ROWID
return 0, nil
}

View File

@@ -12,6 +12,11 @@ import (
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func TestMain(m *testing.M) {
sqlite3.AutoExtension(bloom.Register)
m.Run()
}
func TestRegister(t *testing.T) {
t.Parallel()
@@ -21,10 +26,8 @@ func TestRegister(t *testing.T) {
}
defer db.Close()
bloom.Register(db)
err = db.Exec(`
CREATE VIRTUAL TABLE sports_cars USING bloom_filter(20);
CREATE VIRTUAL TABLE sports_cars USING bloom_filter();
INSERT INTO sports_cars VALUES ('ferrari'), ('lamborghini'), ('alfa romeo')
`)
if err != nil {
@@ -66,7 +69,22 @@ func TestRegister(t *testing.T) {
t.Fatal(err)
}
err = db.Exec(`DROP TABLE sports_cars`)
err = db.Exec(`DELETE FROM sports_cars WHERE word = 'lamborghini'`)
if err == nil {
t.Error("want error")
}
err = db.Exec(`UPDATE sports_cars SET word = 'ferrari' WHERE word = 'lamborghini'`)
if err == nil {
t.Error("want error")
}
err = db.Exec(`ALTER TABLE sports_cars RENAME TO fast_cars`)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`DROP TABLE fast_cars`)
if err != nil {
t.Fatal(err)
}
@@ -90,8 +108,6 @@ func Test_compatible(t *testing.T) {
}
defer db.Close()
bloom.Register(db)
query, _, err := db.Prepare(`SELECT COUNT(*) FROM plants(?)`)
if err != nil {
t.Fatal(err)
@@ -138,3 +154,42 @@ func Test_compatible(t *testing.T) {
t.Error(err)
}
}
func Test_errors(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
bloom.Register(db)
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter(0)`)
if err == nil {
t.Error("want error")
}
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter('a')`)
if err == nil {
t.Error("want error")
}
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter(20, 2)`)
if err == nil {
t.Error("want error")
}
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter(20, 'a')`)
if err == nil {
t.Error("want error")
}
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter(20, 0.9, 0)`)
if err == nil {
t.Error("want error")
}
err = db.Exec(`CREATE VIRTUAL TABLE sports_cars USING bloom_filter(20, 0.9, 'a')`)
if err == nil {
t.Error("want error")
}
}

Binary file not shown.

264
ext/closure/closure.go Normal file
View File

@@ -0,0 +1,264 @@
// Package closure provides a transitive closure virtual table.
//
// The transitive_closure virtual table finds the transitive closure of
// a parent/child relationship in a real table.
//
// https://sqlite.org/src/doc/tip/ext/misc/closure.c
package closure
import (
"fmt"
"math"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
const (
_COL_ID = 0
_COL_DEPTH = 1
_COL_ROOT = 2
_COL_TABLENAME = 3
_COL_IDCOLUMN = 4
_COL_PARENTCOLUMN = 5
)
// Register registers the transitive_closure virtual table:
//
// CREATE VIRTUAL TABLE temp.closure USING transitive_closure;
func Register(db *sqlite3.Conn) error {
return sqlite3.CreateModule(db, "transitive_closure", nil,
func(db *sqlite3.Conn, _, _, _ string, arg ...string) (*closure, error) {
var (
table string
column string
parent string
done = util.Set[string]{}
)
for _, arg := range arg {
key, val := sql3util.NamedArg(arg)
if done.Contains(key) {
return nil, fmt.Errorf("transitive_closure: more than one %q parameter", key)
}
switch key {
case "tablename":
table = sql3util.Unquote(val)
case "idcolumn":
column = sql3util.Unquote(val)
case "parentcolumn":
parent = sql3util.Unquote(val)
default:
return nil, fmt.Errorf("transitive_closure: unknown %q parameter", key)
}
done.Add(key)
}
err := db.DeclareVTab(`CREATE TABLE x(id,depth,root HIDDEN,tablename HIDDEN,idcolumn HIDDEN,parentcolumn HIDDEN)`)
if err != nil {
return nil, err
}
return &closure{
db: db,
table: table,
column: column,
parent: parent,
}, nil
})
}
type closure struct {
db *sqlite3.Conn
table string
column string
parent string
}
func (c *closure) Destroy() error { return nil }
func (c *closure) BestIndex(idx *sqlite3.IndexInfo) error {
plan := 0
posi := 1
cost := 1e7
for i, cst := range idx.Constraint {
switch {
case !cst.Usable:
continue
case plan&1 == 0 && cst.Column == _COL_ROOT:
switch cst.Op {
case sqlite3.INDEX_CONSTRAINT_EQ:
plan |= 1
cost /= 100
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
ArgvIndex: 1,
Omit: true,
}
}
case plan&0xf0 == 0 && cst.Column == _COL_DEPTH:
switch cst.Op {
case sqlite3.INDEX_CONSTRAINT_LT, sqlite3.INDEX_CONSTRAINT_LE, sqlite3.INDEX_CONSTRAINT_EQ:
plan |= posi << 4
cost /= 5
posi += 1
idx.ConstraintUsage[i].ArgvIndex = posi
if cst.Op == sqlite3.INDEX_CONSTRAINT_LT {
plan |= 2
}
}
case plan&0xf00 == 0 && cst.Column == _COL_TABLENAME:
switch cst.Op {
case sqlite3.INDEX_CONSTRAINT_EQ:
plan |= posi << 8
cost /= 5
posi += 1
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
ArgvIndex: posi,
Omit: true,
}
}
case plan&0xf000 == 0 && cst.Column == _COL_IDCOLUMN:
switch cst.Op {
case sqlite3.INDEX_CONSTRAINT_EQ:
plan |= posi << 12
posi += 1
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
ArgvIndex: posi,
Omit: true,
}
}
case plan&0xf0000 == 0 && cst.Column == _COL_PARENTCOLUMN:
switch cst.Op {
case sqlite3.INDEX_CONSTRAINT_EQ:
plan |= posi << 16
posi += 1
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
ArgvIndex: posi,
Omit: true,
}
}
}
}
if plan&1 == 0 ||
c.table == "" && plan&0xf00 == 0 ||
c.column == "" && plan&0xf000 == 0 ||
c.parent == "" && plan&0xf0000 == 0 {
return sqlite3.CONSTRAINT
}
idx.EstimatedCost = cost
idx.IdxNum = plan
return nil
}
func (c *closure) Open() (sqlite3.VTabCursor, error) {
return &cursor{closure: c}, nil
}
type cursor struct {
*closure
nodes []node
}
type node struct {
id int64
depth int
}
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
root := arg[0].Int64()
maxDepth := math.MaxInt
if idxNum&0xf0 != 0 {
maxDepth = arg[(idxNum>>4)&0xf].Int()
if idxNum&2 != 0 {
maxDepth -= 1
}
}
table := c.table
if idxNum&0xf00 != 0 {
table = arg[(idxNum>>8)&0xf].Text()
}
column := c.column
if idxNum&0xf000 != 0 {
column = arg[(idxNum>>12)&0xf].Text()
}
parent := c.parent
if idxNum&0xf0000 != 0 {
parent = arg[(idxNum>>16)&0xf].Text()
}
sql := fmt.Sprintf(
`SELECT %[1]s.%[2]s FROM %[1]s WHERE %[1]s.%[3]s=?`,
sqlite3.QuoteIdentifier(table),
sqlite3.QuoteIdentifier(column),
sqlite3.QuoteIdentifier(parent),
)
stmt, _, err := c.db.Prepare(sql)
if err != nil {
return err
}
defer stmt.Close()
c.nodes = []node{{root, 0}}
set := util.Set[int64]{}
set.Add(root)
for i := range c.nodes {
curr := c.nodes[i]
if curr.depth >= maxDepth {
continue
}
if err := stmt.BindInt64(1, curr.id); err != nil {
return err
}
for stmt.Step() {
if stmt.ColumnType(0) == sqlite3.INTEGER {
next := stmt.ColumnInt64(0)
if !set.Contains(next) {
set.Add(next)
c.nodes = append(c.nodes, node{next, curr.depth + 1})
}
}
}
if err := stmt.Reset(); err != nil {
return err
}
}
return nil
}
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
switch n {
case _COL_ID:
ctx.ResultInt64(c.nodes[0].id)
case _COL_DEPTH:
ctx.ResultInt(c.nodes[0].depth)
case _COL_TABLENAME:
ctx.ResultText(c.table)
case _COL_IDCOLUMN:
ctx.ResultText(c.column)
case _COL_PARENTCOLUMN:
ctx.ResultText(c.parent)
}
return nil
}
func (c *cursor) Next() error {
c.nodes = c.nodes[1:]
return nil
}
func (c *cursor) EOF() bool {
return len(c.nodes) == 0
}
func (c *cursor) RowID() (int64, error) {
return c.nodes[0].id, nil
}

184
ext/closure/closure_test.go Normal file
View File

@@ -0,0 +1,184 @@
package closure_test
import (
_ "embed"
"fmt"
"log"
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/closure"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func TestMain(m *testing.M) {
sqlite3.AutoExtension(closure.Register)
m.Run()
}
func Example() {
db, err := sqlite3.Open(":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
closure.Register(db)
err = db.Exec(`
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
parent_id INTEGER,
name TEXT
);
CREATE INDEX employees_parent_idx ON employees(parent_id);
INSERT INTO employees (id, parent_id, name) VALUES
(11, NULL, 'Diane'),
(12, 11, 'Bob'),
(21, 11, 'Emma'),
(22, 21, 'Grace'),
(23, 21, 'Henry'),
(24, 21, 'Irene'),
(25, 21, 'Frank'),
(31, 11, 'Cindy'),
(32, 31, 'Dave'),
(33, 31, 'Alice');
CREATE VIRTUAL TABLE hierarchy USING transitive_closure(
tablename = "employees",
idcolumn = "id",
parentcolumn = "parent_id"
);
`)
if err != nil {
log.Fatal(err)
}
stmt, _, err := db.Prepare(`
SELECT employees.id, name FROM employees, hierarchy
WHERE employees.id = hierarchy.id AND hierarchy.root = 31
`)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
for stmt.Step() {
fmt.Println(stmt.ColumnInt(0), stmt.ColumnText(1))
}
if err := stmt.Err(); err != nil {
log.Fatal(err)
}
err = stmt.Close()
if err != nil {
log.Fatal(err)
}
err = db.Close()
if err != nil {
log.Fatal(err)
}
// Output:
// 31 Cindy
// 32 Dave
// 33 Alice
}
func TestRegister(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
err = db.Exec(`
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
parent_id INTEGER,
name TEXT
);
CREATE INDEX employees_parent_idx ON employees(parent_id);
INSERT INTO employees (id, parent_id, name) VALUES
(11, NULL, 'Diane'),
(12, 11, 'Bob'),
(21, 11, 'Emma'),
(22, 21, 'Grace'),
(23, 21, 'Henry'),
(24, 21, 'Irene'),
(25, 21, 'Frank'),
(31, 11, 'Cindy'),
(32, 31, 'Dave'),
(33, 31, 'Alice');
CREATE VIRTUAL TABLE temp.closure USING transitive_closure;
`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`
SELECT employees.id, name FROM employees, closure
WHERE employees.id = closure.id
AND closure.root = 31
AND closure.depth < 1
AND closure.tablename='employees'
AND closure.idcolumn='id'
AND closure.parentcolumn='parent_id'
`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if !stmt.Step() {
t.Error("want row")
}
if stmt.Step() {
t.Error("don't want row")
}
if err := stmt.Err(); err != nil {
t.Fatal(err)
}
err = stmt.Close()
if err != nil {
t.Fatal(err)
}
err = db.Close()
if err != nil {
t.Fatal(err)
}
}
func Test_errors(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
err = db.Exec(`CREATE VIRTUAL TABLE hierarchy USING transitive_closure(table='employees')`)
if err == nil {
t.Error("want error")
}
err = db.Exec(`CREATE VIRTUAL TABLE hierarchy USING transitive_closure(tablename='employees', tablename="employees")`)
if err == nil {
t.Error("want error")
}
err = db.Exec("CREATE VIRTUAL TABLE hierarchy USING transitive_closure(tablename=`employees`)")
if err != nil {
t.Error(err)
}
err = db.Exec(`SELECT * FROM hierarchy`)
if err == nil {
t.Error("want error")
}
}

View File

@@ -4,8 +4,7 @@ import (
"fmt"
"strconv"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/vtabutil"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
func uintArg(key, val string) (int, error) {
@@ -20,7 +19,7 @@ func boolArg(key, val string) (bool, error) {
if val == "" {
return true, nil
}
b, ok := util.ParseBool(val)
b, ok := sql3util.ParseBool(val)
if ok {
return b, nil
}
@@ -28,7 +27,7 @@ func boolArg(key, val string) (bool, error) {
}
func runeArg(key, val string) (rune, error) {
r, _, tail, err := strconv.UnquoteChar(vtabutil.Unquote(val), 0)
r, _, tail, err := strconv.UnquoteChar(sql3util.Unquote(val), 0)
if tail != "" || err != nil {
return 0, fmt.Errorf("csv: invalid %q parameter: %s", key, val)
}

View File

@@ -3,7 +3,7 @@ package csv
import (
"testing"
"github.com/ncruces/go-sqlite3/util/vtabutil"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
func Test_uintArg(t *testing.T) {
@@ -24,7 +24,7 @@ func Test_uintArg(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.arg, func(t *testing.T) {
key, val := vtabutil.NamedArg(tt.arg)
key, val := sql3util.NamedArg(tt.arg)
if key != tt.key {
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
}
@@ -62,7 +62,7 @@ func Test_boolArg(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.arg, func(t *testing.T) {
key, val := vtabutil.NamedArg(tt.arg)
key, val := sql3util.NamedArg(tt.arg)
if key != tt.key {
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
}
@@ -96,7 +96,7 @@ func Test_runeArg(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.arg, func(t *testing.T) {
key, val := vtabutil.NamedArg(tt.arg)
key, val := sql3util.NamedArg(tt.arg)
if key != tt.key {
t.Errorf("NamedArg() %v, want err %v", key, tt.key)
}

View File

@@ -9,7 +9,6 @@ package csv
import (
"bufio"
"encoding/csv"
"errors"
"fmt"
"io"
"io/fs"
@@ -17,19 +16,20 @@ import (
"strings"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/osutil"
"github.com/ncruces/go-sqlite3/util/vtabutil"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
// Register registers the CSV virtual table.
// If a filename is specified, [os.Open] is used to open the file.
func Register(db *sqlite3.Conn) {
RegisterFS(db, osutil.FS{})
func Register(db *sqlite3.Conn) error {
return RegisterFS(db, osutil.FS{})
}
// RegisterFS registers the CSV virtual table.
// If a filename is specified, fsys is used to open the file.
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
var (
filename string
@@ -40,21 +40,21 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
comma rune = ','
comment rune
done = map[string]struct{}{}
done = util.Set[string]{}
)
for _, arg := range arg {
key, val := vtabutil.NamedArg(arg)
if _, ok := done[key]; ok {
key, val := sql3util.NamedArg(arg)
if done.Contains(key) {
return nil, fmt.Errorf("csv: more than one %q parameter", key)
}
switch key {
case "filename":
filename = vtabutil.Unquote(val)
filename = sql3util.Unquote(val)
case "data":
data = vtabutil.Unquote(val)
data = sql3util.Unquote(val)
case "schema":
schema = vtabutil.Unquote(val)
schema = sql3util.Unquote(val)
case "header":
header, err = boolArg(key, val)
case "columns":
@@ -69,14 +69,14 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
if err != nil {
return nil, err
}
done[key] = struct{}{}
done.Add(key)
}
if (filename == "") == (data == "") {
return nil, errors.New(`csv: must specify either "filename" or "data" but not both`)
return nil, util.ErrorString(`csv: must specify either "filename" or "data" but not both`)
}
table := &table{
t := &table{
fsys: fsys,
name: filename,
data: data,
@@ -88,7 +88,7 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
if schema == "" {
var row []string
if header || columns < 0 {
csv, c, err := table.newReader()
csv, c, err := t.newReader()
defer c.Close()
if err != nil {
return nil, err
@@ -100,25 +100,23 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
}
schema = getSchema(header, columns, row)
} else {
defer func() {
if err == nil {
table.typs, err = getColumnAffinities(schema)
}
}()
t.typs, err = getColumnAffinities(schema)
if err != nil {
return nil, err
}
}
err = db.DeclareVTab(schema)
if err == nil {
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
}
if err != nil {
return nil, err
}
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
if err != nil {
return nil, err
}
return table, nil
return t, nil
}
sqlite3.CreateModule(db, "csv", declare, declare)
return sqlite3.CreateModule(db, "csv", declare, declare)
}
type table struct {
@@ -216,7 +214,10 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
return err
}
if c.table.header {
c.Next() // skip header
err = c.Next() // skip header
if err != nil {
return err
}
}
c.rowID = 0
return c.Next()
@@ -239,7 +240,7 @@ func (c *cursor) RowID() (int64, error) {
return c.rowID, nil
}
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
func (c *cursor) Column(ctx sqlite3.Context, col int) error {
if col < len(c.row) {
typ := text
if col < len(c.table.typs) {

View File

@@ -18,7 +18,10 @@ func Example() {
}
defer db.Close()
csv.Register(db)
err = csv.Register(db)
if err != nil {
log.Fatal(err)
}
err = db.Exec(`
CREATE VIRTUAL TABLE eurofxref USING csv(
@@ -51,6 +54,11 @@ func Example() {
// On Twosday, 1€ = $1.1342
}
func TestMain(m *testing.M) {
sqlite3.AutoExtension(csv.Register)
m.Run()
}
func TestRegister(t *testing.T) {
t.Parallel()
@@ -60,8 +68,6 @@ func TestRegister(t *testing.T) {
}
defer db.Close()
csv.Register(db)
const data = `
# Comment
"Rob" "Pike" rob
@@ -124,8 +130,6 @@ func TestAffinity(t *testing.T) {
}
defer db.Close()
csv.Register(db)
const data = "01\n0.10\ne"
err = db.Exec(`
CREATE VIRTUAL TABLE temp.nums USING csv(
@@ -168,8 +172,6 @@ func TestRegister_errors(t *testing.T) {
}
defer db.Close()
csv.Register(db)
err = db.Exec(`CREATE VIRTUAL TABLE temp.users USING csv()`)
if err == nil {
t.Fatal("want error")

View File

@@ -1,10 +1,9 @@
package csv
import (
_ "embed"
"strings"
"github.com/ncruces/go-sqlite3/util/vtabutil"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
type affinity byte
@@ -18,16 +17,15 @@ const (
)
func getColumnAffinities(schema string) ([]affinity, error) {
tab, err := vtabutil.Parse(schema)
tab, err := sql3util.ParseTable(schema)
if err != nil {
return nil, err
}
defer tab.Close()
types := make([]affinity, tab.NumColumns())
for i := range types {
col := tab.Column(i)
types[i] = getAffinity(col.Type())
columns := tab.Columns
types := make([]affinity, len(columns))
for i, col := range columns {
types[i] = getAffinity(col.Type)
}
return types, nil
}

View File

@@ -1,9 +1,6 @@
package csv
import (
_ "embed"
"testing"
)
import "testing"
func Test_getAffinity(t *testing.T) {
tests := []struct {

View File

@@ -1,3 +1,5 @@
//go:build !go1.23
package fileio
import (

View File

@@ -14,24 +14,28 @@ import (
// Register registers SQL functions readfile, writefile, lsmode,
// and the table-valued function fsdir.
func Register(db *sqlite3.Conn) {
RegisterFS(db, nil)
func Register(db *sqlite3.Conn) error {
return RegisterFS(db, nil)
}
// Register registers SQL functions readfile, lsmode,
// and the table-valued function fsdir;
// fsys will be used to read files and list directories.
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
db.CreateFunction("lsmode", 1, sqlite3.DETERMINISTIC, lsmode)
db.CreateFunction("readfile", 1, sqlite3.DIRECTONLY, readfile(fsys))
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
var err error
if fsys == nil {
db.CreateFunction("writefile", -1, sqlite3.DIRECTONLY, writefile)
err = db.CreateFunction("writefile", -1, sqlite3.DIRECTONLY, writefile)
}
sqlite3.CreateModule(db, "fsdir", nil, func(db *sqlite3.Conn, _, _, _ string, _ ...string) (fsdir, error) {
err := db.DeclareVTab(`CREATE TABLE x(name,mode,mtime TIMESTAMP,data,path HIDDEN,dir HIDDEN)`)
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
return fsdir{fsys}, err
})
return errors.Join(err,
db.CreateFunction("readfile", 1, sqlite3.DIRECTONLY, readfile(fsys)),
db.CreateFunction("lsmode", 1, sqlite3.DETERMINISTIC, lsmode),
sqlite3.CreateModule(db, "fsdir", nil, func(db *sqlite3.Conn, _, _, _ string, _ ...string) (fsdir, error) {
err := db.DeclareVTab(`CREATE TABLE x(name,mode,mtime TIMESTAMP,data,path HIDDEN,dir HIDDEN)`)
if err == nil {
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
}
return fsdir{fsys}, err
}))
}
func lsmode(ctx sqlite3.Context, arg ...sqlite3.Value) {
@@ -53,7 +57,7 @@ func readfile(fsys fs.FS) func(ctx sqlite3.Context, arg ...sqlite3.Value) {
case err == nil:
ctx.ResultBlob(data)
case !errors.Is(err, fs.ErrNotExist):
ctx.ResultError(fmt.Errorf("readfile: %w", err))
ctx.ResultError(fmt.Errorf("readfile: %w", err)) // notest
}
}
}

View File

@@ -12,15 +12,14 @@ import (
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/fileio"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Test_lsmode(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
fileio.Register(c)
return nil
})
db, err := driver.Open(tmp, fileio.Register)
if err != nil {
t.Fatal(err)
}
@@ -54,7 +53,9 @@ func Test_readfile(t *testing.T) {
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
t.Run("", func(t *testing.T) {
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
tmp := memdb.TestDB(t)
db, err := driver.Open(tmp, func(c *sqlite3.Conn) error {
fileio.RegisterFS(c, fsys)
return nil
})

View File

@@ -10,13 +10,22 @@ import (
"github.com/ncruces/go-sqlite3"
)
const (
_COL_NAME = 0
_COL_MODE = 1
_COL_TIME = 2
_COL_DATA = 3
_COL_ROOT = 4
_COL_BASE = 5
)
type fsdir struct{ fsys fs.FS }
func (d fsdir) BestIndex(idx *sqlite3.IndexInfo) error {
var root, base bool
for i, cst := range idx.Constraint {
switch cst.Column {
case 4: // root
case _COL_ROOT:
if !cst.Usable || cst.Op != sqlite3.INDEX_CONSTRAINT_EQ {
return sqlite3.CONSTRAINT
}
@@ -25,7 +34,7 @@ func (d fsdir) BestIndex(idx *sqlite3.IndexInfo) error {
ArgvIndex: 1,
}
root = true
case 5: // base
case _COL_BASE:
if !cst.Usable || cst.Op != sqlite3.INDEX_CONSTRAINT_EQ {
return sqlite3.CONSTRAINT
}
@@ -54,7 +63,7 @@ func (d fsdir) Open() (sqlite3.VTabCursor, error) {
type cursor struct {
fsdir
base string
resume func(struct{}) (entry, bool)
resume resume
cancel func()
curr entry
eof bool
@@ -92,25 +101,14 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
c.base = base
}
c.resume, c.cancel = coroNew(func(_ struct{}, yield func(entry) struct{}) entry {
walkDir := func(path string, d fs.DirEntry, err error) error {
yield(entry{d, err, path})
return nil
}
if c.fsys != nil {
fs.WalkDir(c.fsys, root, walkDir)
} else {
filepath.WalkDir(root, walkDir)
}
return entry{}
})
c.resume, c.cancel = pull(c, root)
c.eof = false
c.rowID = 0
return c.Next()
}
func (c *cursor) Next() error {
curr, ok := c.resume(struct{}{})
curr, ok := next(c)
c.curr = curr
c.eof = !ok
c.rowID++
@@ -125,27 +123,27 @@ func (c *cursor) RowID() (int64, error) {
return c.rowID, nil
}
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
switch n {
case 0: // name
case _COL_NAME:
name := strings.TrimPrefix(c.curr.path, c.base)
ctx.ResultText(name)
case 1: // mode
case _COL_MODE:
i, err := c.curr.Info()
if err != nil {
return err
}
ctx.ResultInt64(int64(i.Mode()))
case 2: // mtime
case _COL_TIME:
i, err := c.curr.Info()
if err != nil {
return err
}
ctx.ResultTime(i.ModTime(), sqlite3.TimeFormatUnixFrac)
case 3: // data
case _COL_DATA:
switch typ := c.curr.Type(); {
case typ.IsRegular():
var data []byte

29
ext/fileio/fsdir_coro.go Normal file
View File

@@ -0,0 +1,29 @@
//go:build !go1.23
package fileio
import (
"io/fs"
"path/filepath"
)
type resume = func(struct{}) (entry, bool)
func next(c *cursor) (entry, bool) {
return c.resume(struct{}{})
}
func pull(c *cursor, root string) (resume, func()) {
return coroNew(func(_ struct{}, yield func(entry) struct{}) entry {
walkDir := func(path string, d fs.DirEntry, err error) error {
yield(entry{d, err, path})
return nil
}
if c.fsys != nil {
fs.WalkDir(c.fsys, root, walkDir)
} else {
filepath.WalkDir(root, walkDir)
}
return entry{}
})
}

31
ext/fileio/fsdir_iter.go Normal file
View File

@@ -0,0 +1,31 @@
//go:build go1.23
package fileio
import (
"io/fs"
"iter"
"path/filepath"
)
type resume = func() (entry, bool)
func next(c *cursor) (entry, bool) {
return c.resume()
}
func pull(c *cursor, root string) (resume, func()) {
return iter.Pull(func(yield func(entry) bool) {
walkDir := func(path string, d fs.DirEntry, err error) error {
if yield(entry{d, err, path}) {
return nil
}
return fs.SkipAll
}
if c.fsys != nil {
fs.WalkDir(c.fsys, root, walkDir)
} else {
filepath.WalkDir(root, walkDir)
}
})
}

View File

@@ -13,6 +13,7 @@ import (
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/fileio"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Test_fsdir(t *testing.T) {
@@ -20,7 +21,9 @@ func Test_fsdir(t *testing.T) {
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
t.Run("", func(t *testing.T) {
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
tmp := memdb.TestDB(t)
db, err := driver.Open(tmp, func(c *sqlite3.Conn) error {
fileio.RegisterFS(c, fsys)
return nil
})
@@ -68,7 +71,10 @@ func Test_fsdir_errors(t *testing.T) {
}
defer db.Close()
fileio.Register(db)
err = fileio.Register(db)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`SELECT name FROM fsdir()`)
if err == nil {

View File

@@ -29,7 +29,7 @@ func writefile(ctx sqlite3.Context, arg ...sqlite3.Value) {
n, err := createFileAndDir(file, mode, arg[1])
if err != nil {
if len(arg) > 2 {
ctx.ResultError(fmt.Errorf("writefile: %w", err))
ctx.ResultError(fmt.Errorf("writefile: %w", err)) // notest
}
return
}
@@ -39,7 +39,7 @@ func writefile(ctx sqlite3.Context, arg ...sqlite3.Value) {
err := os.Chmod(file, mode.Perm())
if err != nil {
ctx.ResultError(fmt.Errorf("writefile: %w", err))
return
return // notest
}
}
@@ -48,7 +48,7 @@ func writefile(ctx sqlite3.Context, arg ...sqlite3.Value) {
err := os.Chtimes(file, time.Time{}, mtime)
if err != nil {
ctx.ResultError(fmt.Errorf("writefile: %w", err))
return
return // notest
}
}
}

View File

@@ -7,19 +7,17 @@ import (
"testing"
"time"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Test_writefile(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
Register(c)
return nil
})
db, err := driver.Open(tmp, Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -21,47 +21,60 @@ package hash
import (
"crypto"
"errors"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Register registers cryptographic hash functions for a database connection.
func Register(db *sqlite3.Conn) {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
func Register(db *sqlite3.Conn) error {
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
var errs util.ErrorJoiner
if crypto.MD4.Available() {
db.CreateFunction("md4", 1, flags, md4Func)
errs.Join(
db.CreateFunction("md4", 1, flags, md4Func))
}
if crypto.MD5.Available() {
db.CreateFunction("md5", 1, flags, md5Func)
errs.Join(
db.CreateFunction("md5", 1, flags, md5Func))
}
if crypto.SHA1.Available() {
db.CreateFunction("sha1", 1, flags, sha1Func)
errs.Join(
db.CreateFunction("sha1", 1, flags, sha1Func))
}
if crypto.SHA3_512.Available() {
db.CreateFunction("sha3", 1, flags, sha3Func)
db.CreateFunction("sha3", 2, flags, sha3Func)
errs.Join(
db.CreateFunction("sha3", 1, flags, sha3Func),
db.CreateFunction("sha3", 2, flags, sha3Func))
}
if crypto.SHA256.Available() {
db.CreateFunction("sha224", 1, flags, sha224Func)
db.CreateFunction("sha256", 1, flags, sha256Func)
db.CreateFunction("sha256", 2, flags, sha256Func)
errs.Join(
db.CreateFunction("sha224", 1, flags, sha224Func),
db.CreateFunction("sha256", 1, flags, sha256Func),
db.CreateFunction("sha256", 2, flags, sha256Func))
}
if crypto.SHA512.Available() {
db.CreateFunction("sha384", 1, flags, sha384Func)
db.CreateFunction("sha512", 1, flags, sha512Func)
db.CreateFunction("sha512", 2, flags, sha512Func)
errs.Join(
db.CreateFunction("sha384", 1, flags, sha384Func),
db.CreateFunction("sha512", 1, flags, sha512Func),
db.CreateFunction("sha512", 2, flags, sha512Func))
}
if crypto.BLAKE2s_256.Available() {
db.CreateFunction("blake2s", 1, flags, blake2sFunc)
errs.Join(
db.CreateFunction("blake2s", 1, flags, blake2sFunc))
}
if crypto.BLAKE2b_512.Available() {
db.CreateFunction("blake2b", 1, flags, blake2bFunc)
db.CreateFunction("blake2b", 2, flags, blake2bFunc)
errs.Join(
db.CreateFunction("blake2b", 1, flags, blake2bFunc),
db.CreateFunction("blake2b", 2, flags, blake2bFunc))
}
if crypto.RIPEMD160.Available() {
db.CreateFunction("ripemd160", 1, flags, ripemd160Func)
errs.Join(
db.CreateFunction("ripemd160", 1, flags, ripemd160Func))
}
return errors.Join(errs...)
}
func md4Func(ctx sqlite3.Context, arg ...sqlite3.Value) {

View File

@@ -7,19 +7,21 @@ import (
_ "crypto/sha512"
"testing"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
_ "golang.org/x/crypto/blake2b"
_ "golang.org/x/crypto/blake2s"
_ "golang.org/x/crypto/md4"
_ "golang.org/x/crypto/ripemd160"
_ "golang.org/x/crypto/sha3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func TestRegister(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
tests := []struct {
name string
@@ -53,10 +55,7 @@ func TestRegister(t *testing.T) {
{"blake2b('', 256)", "0E5751C026E543B2E8AB2EB06099DAA1D1E5DF47778F7787FAAB45CDF12FE3A8"},
}
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
Register(c)
return nil
})
db, err := driver.Open(tmp, Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -13,6 +13,7 @@ package lines
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/fs"
@@ -25,46 +26,61 @@ import (
// The lines function reads from a database blob or text.
// The lines_read function reads from a file or an [io.Reader].
// If a filename is specified, [os.Open] is used to open the file.
func Register(db *sqlite3.Conn) {
RegisterFS(db, osutil.FS{})
func Register(db *sqlite3.Conn) error {
return RegisterFS(db, osutil.FS{})
}
// RegisterFS registers the lines and lines_read table-valued functions.
// The lines function reads from a database blob or text.
// The lines_read function reads from a file or an [io.Reader].
// If a filename is specified, fsys is used to open the file.
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
sqlite3.CreateModule(db, "lines", nil,
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
db.VTabConfig(sqlite3.VTAB_INNOCUOUS)
return lines{}, err
})
sqlite3.CreateModule(db, "lines_read", nil,
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
return lines{fsys}, err
})
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
return errors.Join(
sqlite3.CreateModule(db, "lines", nil,
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN, delim HIDDEN)`)
if err == nil {
err = db.VTabConfig(sqlite3.VTAB_INNOCUOUS)
}
return lines{}, err
}),
sqlite3.CreateModule(db, "lines_read", nil,
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN, delim HIDDEN)`)
if err == nil {
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
}
return lines{fsys}, err
}))
}
type lines struct {
fsys fs.FS
}
func (l lines) BestIndex(idx *sqlite3.IndexInfo) error {
func (l lines) BestIndex(idx *sqlite3.IndexInfo) (err error) {
err = sqlite3.CONSTRAINT
for i, cst := range idx.Constraint {
if cst.Column == 1 && cst.Op == sqlite3.INDEX_CONSTRAINT_EQ && cst.Usable {
if !cst.Usable || cst.Op != sqlite3.INDEX_CONSTRAINT_EQ {
continue
}
switch cst.Column {
case 1:
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
Omit: true,
ArgvIndex: 1,
}
idx.EstimatedCost = 1e6
idx.EstimatedRows = 100
return nil
err = nil
case 2:
idx.ConstraintUsage[i] = sqlite3.IndexConstraintUsage{
Omit: true,
ArgvIndex: 2,
}
}
}
return sqlite3.CONSTRAINT
return err
}
func (l lines) Open() (sqlite3.VTabCursor, error) {
@@ -79,6 +95,7 @@ type cursor struct {
line []byte
rowID int64
eof bool
delim byte
}
func (c *cursor) EOF() bool {
@@ -89,7 +106,7 @@ func (c *cursor) RowID() (int64, error) {
return c.rowID, nil
}
func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
if n == 0 {
ctx.ResultRawText(c.line)
}
@@ -134,6 +151,15 @@ func (c *reader) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
return fmt.Errorf("lines: unsupported argument:%.0w %v", sqlite3.MISMATCH, typ)
}
c.delim = '\n'
if len(arg) > 1 {
b := arg[1].RawText()
if len(b) != 1 {
return fmt.Errorf("lines: delimiter must be a single byte%.0w", sqlite3.MISMATCH)
}
c.delim = b[0]
}
c.reader = bufio.NewReader(r)
c.closer, _ = r.(io.Closer)
c.rowID = 0
@@ -144,7 +170,12 @@ func (c *reader) Next() (err error) {
c.line = c.line[:0]
for more := true; more; {
var line []byte
line, more, err = c.reader.ReadLine()
if c.delim == '\n' {
line, more, err = c.reader.ReadLine()
} else {
line, err = c.reader.ReadSlice(c.delim)
more = err == bufio.ErrBufferFull
}
c.line = append(c.line, line...)
}
if err == io.EOF {
@@ -171,18 +202,27 @@ func (c *buffer) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
return fmt.Errorf("lines: unsupported argument:%.0w %v", sqlite3.MISMATCH, typ)
}
c.delim = '\n'
if len(arg) > 1 {
b := arg[1].RawText()
if len(b) != 1 {
return fmt.Errorf("lines: delimiter must be a single byte%.0w", sqlite3.MISMATCH)
}
c.delim = b[0]
}
c.rowID = 0
return c.Next()
}
func (c *buffer) Next() error {
i := bytes.IndexByte(c.data, '\n')
i := bytes.IndexByte(c.data, c.delim)
j := i + 1
switch {
case i < 0:
i = len(c.data)
j = i
case i > 0 && c.data[i-1] == '\r':
case i > 0 && c.delim == '\n' && c.data[i-1] == '\r':
i--
}
c.eof = len(c.data) == 0

View File

@@ -15,13 +15,11 @@ import (
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/lines"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Example() {
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
lines.Register(c)
return nil
})
db, err := driver.Open("file:/test.db?vfs=memdb", lines.Register)
if err != nil {
log.Fatal(err)
}
@@ -69,11 +67,9 @@ func Example() {
func Test_lines(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
lines.Register(c)
return nil
})
db, err := driver.Open(tmp, lines.Register)
if err != nil {
log.Fatal(err)
}
@@ -102,11 +98,9 @@ func Test_lines(t *testing.T) {
func Test_lines_error(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
lines.Register(c)
return nil
})
db, err := driver.Open(tmp, lines.Register)
if err != nil {
log.Fatal(err)
}
@@ -129,11 +123,9 @@ func Test_lines_error(t *testing.T) {
func Test_lines_read(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
lines.Register(c)
return nil
})
db, err := driver.Open(tmp, lines.Register)
if err != nil {
log.Fatal(err)
}
@@ -163,17 +155,15 @@ func Test_lines_read(t *testing.T) {
func Test_lines_test(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(":memory:", func(c *sqlite3.Conn) error {
lines.Register(c)
return nil
})
db, err := driver.Open(tmp, lines.Register)
if err != nil {
log.Fatal(err)
}
defer db.Close()
rows, err := db.Query(`SELECT rowid, line FROM lines_read(?)`, "lines_test.go")
rows, err := db.Query(`SELECT rowid, line FROM lines_read(?, '}')`, "lines_test.go")
if errors.Is(err, os.ErrNotExist) {
t.Skip(err)
}

View File

@@ -9,11 +9,12 @@ import (
"strings"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Register registers the pivot virtual table.
func Register(db *sqlite3.Conn) {
sqlite3.CreateModule(db, "pivot", declare, declare)
func Register(db *sqlite3.Conn) error {
return sqlite3.CreateModule(db, "pivot", declare, declare)
}
type table struct {
@@ -24,15 +25,15 @@ type table struct {
cols []*sqlite3.Value
}
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (ret *table, err error) {
if len(arg) != 3 {
return nil, fmt.Errorf("pivot: wrong number of arguments")
}
table := &table{db: db}
t := &table{db: db}
defer func() {
if err != nil {
table.Close()
if ret == nil {
t.Close()
}
}()
@@ -41,17 +42,17 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
create.WriteString("CREATE TABLE x(")
// Row key query.
table.scan = "SELECT * FROM\n" + arg[0]
stmt, _, err := db.Prepare(table.scan)
t.scan = "SELECT * FROM\n" + arg[0]
stmt, _, err := db.Prepare(t.scan)
if err != nil {
return nil, err
}
defer stmt.Close()
table.keys = make([]string, stmt.ColumnCount())
for i := range table.keys {
t.keys = make([]string, stmt.ColumnCount())
for i := range t.keys {
name := sqlite3.QuoteIdentifier(stmt.ColumnName(i))
table.keys[i] = name
t.keys[i] = name
create.WriteString(sep)
create.WriteString(name)
sep = ","
@@ -65,28 +66,28 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
}
if stmt.ColumnCount() != 2 {
return nil, errors.New("pivot: column definition query expects 2 result columns")
return nil, util.ErrorString("pivot: column definition query expects 2 result columns")
}
for stmt.Step() {
name := sqlite3.QuoteIdentifier(stmt.ColumnText(1))
table.cols = append(table.cols, stmt.ColumnValue(0).Dup())
t.cols = append(t.cols, stmt.ColumnValue(0).Dup())
create.WriteString(",")
create.WriteString(name)
}
stmt.Close()
// Pivot cell query.
table.cell = "SELECT * FROM\n" + arg[2]
stmt, _, err = db.Prepare(table.cell)
t.cell = "SELECT * FROM\n" + arg[2]
stmt, _, err = db.Prepare(t.cell)
if err != nil {
return nil, err
}
if stmt.ColumnCount() != 1 {
return nil, errors.New("pivot: cell query expects 1 result columns")
return nil, util.ErrorString("pivot: cell query expects 1 result columns")
}
if stmt.BindCount() != len(table.keys)+1 {
return nil, fmt.Errorf("pivot: cell query expects %d bound parameters", len(table.keys)+1)
if stmt.BindCount() != len(t.keys)+1 {
return nil, fmt.Errorf("pivot: cell query expects %d bound parameters", len(t.keys)+1)
}
create.WriteByte(')')
@@ -94,14 +95,15 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
if err != nil {
return nil, err
}
return table, nil
return t, nil
}
func (t *table) Close() error {
for i := range t.cols {
t.cols[i].Close()
var errs []error
for _, c := range t.cols {
errs = append(errs, c.Close())
}
return nil
return errors.Join(errs...)
}
func (t *table) BestIndex(idx *sqlite3.IndexInfo) error {
@@ -205,7 +207,7 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
func (c *cursor) Next() error {
if c.scan.Step() {
count := c.scan.ColumnCount()
for i := 0; i < count; i++ {
for i := range count {
err := c.cell.BindValue(i+1, c.scan.ColumnValue(i))
if err != nil {
return err
@@ -224,7 +226,7 @@ func (c *cursor) RowID() (int64, error) {
return c.rowID, nil
}
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
func (c *cursor) Column(ctx sqlite3.Context, col int) error {
count := c.scan.ColumnCount()
if col < count {
ctx.ResultValue(c.scan.ColumnValue(col))

View File

@@ -14,14 +14,14 @@ import (
// https://antonz.org/sqlite-pivot-table/
func Example() {
sqlite3.AutoExtension(pivot.Register)
db, err := sqlite3.Open(":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
pivot.Register(db)
err = db.Exec(`
CREATE TABLE sales(product TEXT, year INT, income DECIMAL);
INSERT INTO sales(product, year, income) VALUES
@@ -83,6 +83,11 @@ func Example() {
// gamma 80 75 78 80
}
func TestMain(m *testing.M) {
sqlite3.AutoExtension(pivot.Register)
m.Run()
}
func TestRegister(t *testing.T) {
t.Parallel()
@@ -92,8 +97,6 @@ func TestRegister(t *testing.T) {
}
defer db.Close()
pivot.Register(db)
err = db.Exec(`
CREATE TABLE r AS
SELECT 1 id UNION SELECT 2 UNION SELECT 3;
@@ -142,6 +145,11 @@ func TestRegister(t *testing.T) {
t.Errorf("got %d, want 3", got)
}
}
err = db.Exec(`ALTER TABLE v_x RENAME TO v_y`)
if err != nil {
t.Fatal(err)
}
}
func TestRegister_errors(t *testing.T) {
@@ -153,8 +161,6 @@ func TestRegister_errors(t *testing.T) {
}
defer db.Close()
pivot.Register(db)
err = db.Exec(`CREATE VIRTUAL TABLE pivot USING pivot()`)
if err == nil {
t.Fatal("want error")

292
ext/regexp/regexp.go Normal file
View File

@@ -0,0 +1,292 @@
// Package regexp provides additional regular expression functions.
//
// It provides the following Unicode aware functions:
// - regexp_like(text, pattern),
// - regexp_count(text, pattern [, start]),
// - regexp_instr(text, pattern [, start [, N [, endoption [, subexpr ]]]]),
// - regexp_substr(text, pattern [, start [, N [, subexpr ]]]),
// - regexp_replace(text, pattern, replacement [, start [, N ]]),
// - and a REGEXP operator.
//
// The implementation uses Go [regexp/syntax] for regular expressions.
//
// https://github.com/nalgeon/sqlean/blob/main/docs/regexp.md
package regexp
import (
"errors"
"regexp"
"regexp/syntax"
"strings"
"unicode/utf8"
"github.com/ncruces/go-sqlite3"
)
// Register registers Unicode aware functions for a database connection.
func Register(db *sqlite3.Conn) error {
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
return errors.Join(
db.CreateFunction("regexp", 2, flags, regex),
db.CreateFunction("regexp_like", 2, flags, regexLike),
db.CreateFunction("regexp_count", 2, flags, regexCount),
db.CreateFunction("regexp_count", 3, flags, regexCount),
db.CreateFunction("regexp_instr", 2, flags, regexInstr),
db.CreateFunction("regexp_instr", 3, flags, regexInstr),
db.CreateFunction("regexp_instr", 4, flags, regexInstr),
db.CreateFunction("regexp_instr", 5, flags, regexInstr),
db.CreateFunction("regexp_instr", 6, flags, regexInstr),
db.CreateFunction("regexp_substr", 2, flags, regexSubstr),
db.CreateFunction("regexp_substr", 3, flags, regexSubstr),
db.CreateFunction("regexp_substr", 4, flags, regexSubstr),
db.CreateFunction("regexp_substr", 5, flags, regexSubstr),
db.CreateFunction("regexp_replace", 3, flags, regexReplace),
db.CreateFunction("regexp_replace", 4, flags, regexReplace),
db.CreateFunction("regexp_replace", 5, flags, regexReplace))
}
// GlobPrefix returns a GLOB for a regular expression
// appropriate to take advantage of the [LIKE optimization]
// in a query such as:
//
// SELECT column WHERE column GLOB :glob_prefix AND column REGEXP :regexp
//
// [LIKE optimization]: https://sqlite.org/optoverview.html#the_like_optimization
func GlobPrefix(expr string) string {
re, err := syntax.Parse(expr, syntax.Perl)
if err != nil {
return "" // no match possible
}
prog, err := syntax.Compile(re.Simplify())
if err != nil {
return "" // notest
}
i := &prog.Inst[prog.Start]
var empty syntax.EmptyOp
loop1:
for {
switch i.Op {
case syntax.InstFail:
return "" // notest
case syntax.InstCapture, syntax.InstNop:
// skip
case syntax.InstEmptyWidth:
empty |= syntax.EmptyOp(i.Arg)
default:
break loop1
}
i = &prog.Inst[i.Out]
}
if empty&syntax.EmptyBeginText == 0 {
return "*" // not anchored
}
var glob strings.Builder
loop2:
for {
switch i.Op {
case syntax.InstFail:
return "" // notest
case syntax.InstCapture, syntax.InstEmptyWidth, syntax.InstNop:
// skip
case syntax.InstRune, syntax.InstRune1:
if len(i.Rune) != 1 || syntax.Flags(i.Arg)&syntax.FoldCase != 0 {
break loop2
}
switch r := i.Rune[0]; r {
case '*', '?', '[', utf8.RuneError:
break loop2
default:
glob.WriteRune(r)
}
default:
break loop2
}
i = &prog.Inst[i.Out]
}
glob.WriteByte('*')
return glob.String()
}
func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
re, ok := ctx.GetAuxData(i).(*regexp.Regexp)
if !ok {
r, err := regexp.Compile(expr)
if err != nil {
return nil, err
}
re = r
ctx.SetAuxData(0, r)
}
return re, nil
}
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
_ = arg[1] // bounds check
re, err := load(ctx, 0, arg[0].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
text := arg[1].RawText()
ctx.ResultBool(re.Match(text))
}
func regexLike(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 1, arg[1].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
text := arg[0].RawText()
ctx.ResultBool(re.Match(text))
}
func regexCount(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 1, arg[1].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
text := arg[0].RawText()
if len(arg) > 2 {
pos := arg[2].Int()
text = text[skip(text, pos):]
}
ctx.ResultInt(len(re.FindAll(text, -1)))
}
func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 1, arg[1].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
text := arg[0].RawText()
var pos, n, subexpr int
if len(arg) > 2 {
pos = arg[2].Int()
}
if len(arg) > 3 {
n = arg[3].Int()
}
if len(arg) > 4 {
subexpr = arg[4].Int()
}
loc := regexFind(re, text, pos, n, subexpr)
if loc != nil {
ctx.ResultRawText(text[loc[0]:loc[1]])
}
}
func regexInstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 1, arg[1].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
text := arg[0].RawText()
var pos, n, end, subexpr int
if len(arg) > 2 {
pos = arg[2].Int()
}
if len(arg) > 3 {
n = arg[3].Int()
}
if len(arg) > 4 && arg[4].Bool() {
end = 1
}
if len(arg) > 5 {
subexpr = arg[5].Int()
}
loc := regexFind(re, text, pos, n, subexpr)
if loc != nil {
ctx.ResultInt(loc[end] + 1)
}
}
func regexReplace(ctx sqlite3.Context, arg ...sqlite3.Value) {
_ = arg[2] // bounds check
re, err := load(ctx, 1, arg[1].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
text := arg[0].RawText()
repl := arg[2].RawText()
var pos, n int
if len(arg) > 3 {
pos = arg[3].Int()
}
if len(arg) > 4 {
n = arg[4].Int()
}
res := text
pos = skip(text, pos)
if n > 0 {
all := re.FindAllSubmatchIndex(text[pos:], n)
if n <= len(all) {
loc := all[n-1]
res = text[:pos+loc[0]]
res = re.Expand(res, repl, text[pos:], loc)
res = append(res, text[pos+loc[1]:]...)
}
} else {
res = append(text[:pos], re.ReplaceAll(text[pos:], repl)...)
}
ctx.ResultRawText(res)
}
func regexFind(re *regexp.Regexp, text []byte, pos, n, subexpr int) (loc []int) {
pos = skip(text, pos)
text = text[pos:]
if n <= 1 {
if subexpr == 0 {
loc = re.FindIndex(text)
} else {
loc = re.FindSubmatchIndex(text)
}
} else {
if subexpr == 0 {
all := re.FindAllIndex(text, n)
if n <= len(all) {
loc = all[n-1]
}
} else {
all := re.FindAllSubmatchIndex(text, n)
if n <= len(all) {
loc = all[n-1]
}
}
}
if 2+2*subexpr <= len(loc) {
loc = loc[2*subexpr : 2+2*subexpr]
loc[0] += pos
loc[1] += pos
return loc
}
return nil
}
func skip(text []byte, start int) int {
for pos := range string(text) {
if start--; start <= 0 {
return pos
}
}
return len(text)
}

163
ext/regexp/regexp_test.go Normal file
View File

@@ -0,0 +1,163 @@
package regexp
import (
"database/sql"
"regexp"
"strings"
"testing"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func TestRegister(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
tests := []struct {
test string
want string
}{
{`'Hello' REGEXP 'elo'`, "0"},
{`'Hello' REGEXP 'ell'`, "1"},
{`'Hello' REGEXP 'el.'`, "1"},
{`regexp_like('Hello', 'elo')`, "0"},
{`regexp_like('Hello', 'ell')`, "1"},
{`regexp_like('Hello', 'el.')`, "1"},
{`regexp_count('Hello', 'l')`, "2"},
{`regexp_instr('Hello', 'el.')`, "2"},
{`regexp_instr('Hello', '.', 6)`, ""},
{`regexp_substr('Hello', 'el.')`, "ell"},
{`regexp_replace('Hello', 'llo', 'll')`, "Hell"},
// https://www.postgresql.org/docs/current/functions-matching.html
{`regexp_count('ABCABCAXYaxy', 'A.')`, "3"},
{`regexp_count('ABCABCAXYaxy', '(?i)A.', 1)`, "4"},
{`regexp_instr('number of your street, town zip, FR', '[^,]+', 1, 2)`, "23"},
{`regexp_instr('ABCDEFGHI', '(?i)(c..)(...)', 1, 1, 0, 2)`, "6"},
{`regexp_substr('number of your street, town zip, FR', '[^,]+', 1, 2)`, " town zip"},
{`regexp_substr('ABCDEFGHI', '(?i)(c..)(...)', 1, 1, 2)`, "FGH"},
{`regexp_replace('foobarbaz', 'b..', 'X', 1, 1)`, "fooXbaz"},
{`regexp_replace('foobarbaz', 'b..', 'X')`, "fooXX"},
{`regexp_replace('foobarbaz', 'b(..)', 'X${1}Y')`, "fooXarYXazY"},
{`regexp_replace('A PostgreSQL function', '(?i)a|e|i|o|u', 'X', 1, 0)`, "X PXstgrXSQL fXnctXXn"},
{`regexp_replace('A PostgreSQL function', '(?i)a|e|i|o|u', 'X', 1, 3)`, "A PostgrXSQL function"},
// https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/REGEXP_COUNT.html
{`regexp_count('123123123123123', '(12)3', 1)`, "5"},
{`regexp_count('123123123123', '123', 3)`, "3"},
{`regexp_instr('500 Oracle Parkway, Redwood Shores, CA', '[^ ]+', 1, 6)`, "37"},
{`regexp_instr('500 Oracle Parkway, Redwood Shores, CA', '(?i)[s|r|p][[:alpha:]]{6}', 3, 2, 1)`, "28"},
{`regexp_instr('1234567890', '(123)(4(56)(78))', 1, 1, 0, 1)`, "1"},
{`regexp_instr('1234567890', '(123)(4(56)(78))', 1, 1, 0, 2)`, "4"},
{`regexp_instr('1234567890', '(123)(4(56)(78))', 1, 1, 0, 4)`, "7"},
{`regexp_substr('500 Oracle Parkway, Redwood Shores, CA', ',[^,]+,')`, ", Redwood Shores,"},
{`regexp_substr('http://www.example.com/products', 'http://([[:alnum:]]+\.?){3,4}/?')`, "http://www.example.com/"},
{`regexp_substr('1234567890', '(123)(4(56)(78))', 1, 1, 1)`, "123"},
{`regexp_substr('1234567890', '(123)(4(56)(78))', 1, 1, 4)`, "78"},
{`regexp_substr('123123123123', '1(.)3', 3, 2, 1)`, "2"},
{`regexp_replace('500 Oracle Parkway, Redwood Shores, CA', '( ){2,}', ' ')`, "500 Oracle Parkway, Redwood Shores, CA"},
}
for _, tt := range tests {
var got sql.NullString
err := db.QueryRow(`SELECT ` + tt.test).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got.String != tt.want {
t.Errorf("got %q, want %q", got.String, tt.want)
}
}
}
func TestRegister_errors(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
tests := []string{
`'' REGEXP ?`,
`regexp_like('', ?)`,
`regexp_count('', ?)`,
`regexp_instr('', ?)`,
`regexp_substr('', ?)`,
`regexp_replace('', ?, '')`,
}
for _, tt := range tests {
err := db.QueryRow(`SELECT `+tt, `\`).Scan(nil)
if err == nil {
t.Fatal("want error")
}
}
}
func TestGlobPrefix(t *testing.T) {
tests := []struct {
re string
want string
}{
{`[`, ""},
{``, "*"},
{`^`, "*"},
{`a`, "*"},
{`ab`, "*"},
{`^a`, "a*"},
{`^a*`, "*"},
{`^a+`, "a*"},
{`^ab*`, "a*"},
{`^ab+`, "ab*"},
{`^a\?b`, "a*"},
{`^[a-z]`, "*"},
}
for _, tt := range tests {
t.Run(tt.re, func(t *testing.T) {
if got := GlobPrefix(tt.re); got != tt.want {
t.Errorf("GlobPrefix(%v) = %v, want %v", tt.re, got, tt.want)
}
})
}
}
func FuzzGlobPrefix(f *testing.F) {
f.Add(``, ``)
f.Add(`[`, ``)
f.Add(`^`, ``)
f.Add(`a`, `a`)
f.Add(`ab`, `b`)
f.Add(`^a`, `a`)
f.Add(`^a*`, `ab`)
f.Add(`^a+`, `ab`)
f.Add(`^ab*`, `ab`)
f.Add(`^ab+`, `ab`)
f.Add(`^a\?b`, `ab`)
f.Add(`^[a-z]`, `ab`)
f.Fuzz(func(t *testing.T, lit, str string) {
re, err := regexp.Compile(lit)
if err != nil {
t.SkipNow()
}
if re.MatchString(str) {
prefix, ok := strings.CutSuffix(GlobPrefix(lit), "*")
if !ok {
t.Fatalf("missing * after %q for %q with %q", prefix, lit, str)
}
if !strings.HasPrefix(str, prefix) {
t.Fatalf("missing prefix %q for %q with %q", prefix, lit, str)
}
}
})
}

140
ext/serdes/serdes.go Normal file
View File

@@ -0,0 +1,140 @@
// Package serdes provides functions to (de)serialize databases.
package serdes
import (
"io"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/vfs"
)
const vfsName = "github.com/ncruces/go-sqlite3/ext/serdes.sliceVFS"
func init() {
vfs.Register(vfsName, sliceVFS{})
}
var fileToOpen = make(chan *sliceFile, 1)
// Serialize backs up a database into a byte slice.
//
// https://sqlite.org/c3ref/serialize.html
func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) {
var file sliceFile
fileToOpen <- &file
err := db.Backup(schema, "file:serdes.db?vfs="+vfsName)
return file.data, err
}
// Deserialize restores a database from a byte slice,
// DESTROYING any contents previously stored in schema.
//
// To non-destructively open a database from a byte slice,
// consider alternatives like the ["reader"] or ["memdb"] VFSes.
//
// This differs from the similarly named SQLite API
// in that it DOES NOT disconnect from schema
// to reopen as an in-memory database.
//
// https://sqlite.org/c3ref/deserialize.html
//
// ["memdb"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb
// ["reader"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs
func Deserialize(db *sqlite3.Conn, schema string, data []byte) error {
fileToOpen <- &sliceFile{data}
return db.Restore(schema, "file:serdes.db?vfs="+vfsName)
}
type sliceVFS struct{}
func (sliceVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
if flags&vfs.OPEN_MAIN_DB == 0 || name != "serdes.db" {
return nil, flags, sqlite3.CANTOPEN
}
select {
case file := <-fileToOpen:
return file, flags | vfs.OPEN_MEMORY, nil
default:
return nil, flags, sqlite3.MISUSE
}
}
func (sliceVFS) Delete(name string, dirSync bool) error {
// notest // OPEN_MEMORY
return sqlite3.IOERR_DELETE
}
func (sliceVFS) Access(name string, flag vfs.AccessFlag) (bool, error) {
return name == "serdes.db", nil
}
func (sliceVFS) FullPathname(name string) (string, error) {
return name, nil
}
type sliceFile struct{ data []byte }
func (f *sliceFile) ReadAt(b []byte, off int64) (n int, err error) {
if d := f.data; off < int64(len(d)) {
n = copy(b, d[off:])
}
if n == 0 {
err = io.EOF
}
return
}
func (f *sliceFile) WriteAt(b []byte, off int64) (n int, err error) {
if d := f.data; off > int64(len(d)) {
f.data = append(d, make([]byte, off-int64(len(d)))...)
}
d := append(f.data[:off], b...)
if len(d) > len(f.data) {
f.data = d
}
return len(b), nil
}
func (f *sliceFile) Size() (int64, error) {
return int64(len(f.data)), nil
}
func (f *sliceFile) Truncate(size int64) error {
if d := f.data; size < int64(len(d)) {
f.data = d[:size]
}
return nil
}
func (f *sliceFile) SizeHint(size int64) error {
if d := f.data; size > int64(len(d)) {
f.data = append(d, make([]byte, size-int64(len(d)))...)
}
return nil
}
func (*sliceFile) Close() error { return nil }
func (*sliceFile) Sync(flag vfs.SyncFlag) error { return nil }
func (*sliceFile) Lock(lock vfs.LockLevel) error { return nil }
func (*sliceFile) Unlock(lock vfs.LockLevel) error { return nil }
func (*sliceFile) CheckReservedLock() (bool, error) {
// notest // OPEN_MEMORY
return false, nil
}
func (*sliceFile) SectorSize() int {
// notest // IOCAP_POWERSAFE_OVERWRITE
return 0
}
func (*sliceFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
return vfs.IOCAP_ATOMIC |
vfs.IOCAP_SAFE_APPEND |
vfs.IOCAP_SEQUENTIAL |
vfs.IOCAP_POWERSAFE_OVERWRITE |
vfs.IOCAP_SUBPAGE_READ
}

87
ext/serdes/serdes_test.go Normal file
View File

@@ -0,0 +1,87 @@
package serdes_test
import (
"errors"
"io"
"net/http"
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/serdes"
)
func TestDeserialize(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
input, err := httpGet()
if err != nil {
t.Fatal(err)
}
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
err = serdes.Deserialize(db, "temp", input)
if err != nil {
t.Fatal(err)
}
output, err := serdes.Serialize(db, "temp")
if err != nil {
t.Fatal(err)
}
if len(input) != len(output) {
t.Fatal("lengths are different")
}
for i := range input {
// These may be different.
switch {
case 24 <= i && i < 28:
// File change counter.
continue
case 40 <= i && i < 44:
// Schema cookie.
continue
case 92 <= i && i < 100:
// SQLite version that wrote the file.
continue
}
if input[i] != output[i] {
t.Errorf("difference at %d: %d %d", i, input[i], output[i])
}
}
}
func httpGet() ([]byte, error) {
res, err := http.Get("https://raw.githubusercontent.com/jpwhite3/northwind-SQLite3/refs/heads/main/dist/northwind.db")
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func TestOpen_errors(t *testing.T) {
_, err := sqlite3.Open("file:test.db?vfs=github.com/ncruces/go-sqlite3/ext/serdes.sliceVFS")
if err == nil {
t.Error("want error")
}
if !errors.Is(err, sqlite3.CANTOPEN) {
t.Errorf("got %v, want sqlite3.CANTOPEN", err)
}
_, err = sqlite3.Open("file:serdes.db?vfs=github.com/ncruces/go-sqlite3/ext/serdes.sliceVFS")
if err == nil {
t.Error("want error")
}
if !errors.Is(err, sqlite3.MISUSE) {
t.Errorf("got %v, want sqlite3.MISUSE", err)
}
}

View File

@@ -14,11 +14,12 @@ import (
"unsafe"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Register registers the statement virtual table.
func Register(db *sqlite3.Conn) {
sqlite3.CreateModule(db, "statement", declare, declare)
func Register(db *sqlite3.Conn) error {
return sqlite3.CreateModule(db, "statement", declare, declare)
}
type table struct {
@@ -29,12 +30,12 @@ type table struct {
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (*table, error) {
if len(arg) != 1 {
return nil, errors.New("statement: wrong number of arguments")
return nil, util.ErrorString("statement: wrong number of arguments")
}
sql := "SELECT * FROM\n" + arg[0]
stmt, _, err := db.Prepare(sql)
stmt, _, err := db.PrepareFlags(sql, sqlite3.PREPARE_PERSISTENT)
if err != nil {
return nil, err
}
@@ -43,7 +44,7 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (*table, error) {
var str strings.Builder
str.WriteString("CREATE TABLE x(")
outputs := stmt.ColumnCount()
for i := 0; i < outputs; i++ {
for i := range outputs {
name := sqlite3.QuoteIdentifier(stmt.ColumnName(i))
str.WriteString(sep)
str.WriteString(name)
@@ -123,12 +124,11 @@ func (t *table) BestIndex(idx *sqlite3.IndexInfo) error {
return nil
}
func (t *table) Open() (sqlite3.VTabCursor, error) {
func (t *table) Open() (_ sqlite3.VTabCursor, err error) {
stmt := t.stmt
if !t.inuse {
t.inuse = true
} else {
var err error
stmt, _, err = t.stmt.Conn().Prepare(t.sql)
if err != nil {
return nil, err
@@ -151,8 +151,9 @@ type cursor struct {
func (c *cursor) Close() error {
if c.stmt == c.table.stmt {
c.table.inuse = false
c.stmt.ClearBindings()
return c.stmt.Reset()
return errors.Join(
c.stmt.Reset(),
c.stmt.ClearBindings())
}
return c.stmt.Close()
}
@@ -160,8 +161,10 @@ func (c *cursor) Close() error {
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 {
err := errors.Join(
c.stmt.Reset(),
c.stmt.ClearBindings())
if err != nil {
return err
}
@@ -202,7 +205,7 @@ func (c *cursor) RowID() (int64, error) {
return c.rowID, nil
}
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
func (c *cursor) Column(ctx sqlite3.Context, col int) error {
switch outputs := c.stmt.ColumnCount(); {
case col < outputs:
ctx.ResultValue(c.stmt.ColumnValue(col))

View File

@@ -12,14 +12,14 @@ import (
)
func Example() {
sqlite3.AutoExtension(statement.Register)
db, err := sqlite3.Open(":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
statement.Register(db)
err = db.Exec(`
CREATE VIRTUAL TABLE split_date USING statement((
SELECT
@@ -48,6 +48,11 @@ func Example() {
// Twosday was 2022-2-22
}
func TestMain(m *testing.M) {
sqlite3.AutoExtension(statement.Register)
m.Run()
}
func TestRegister(t *testing.T) {
t.Parallel()
@@ -57,8 +62,6 @@ func TestRegister(t *testing.T) {
}
defer db.Close()
statement.Register(db)
err = db.Exec(`
CREATE VIRTUAL TABLE arguments USING statement((SELECT ? AS a, ? AS b, ? AS c))
`)
@@ -107,8 +110,6 @@ func TestRegister_errors(t *testing.T) {
}
defer db.Close()
statement.Register(db)
err = db.Exec(`CREATE VIRTUAL TABLE split_date USING statement()`)
if err == nil {
t.Fatal("want error")

View File

@@ -26,21 +26,21 @@ func (b *boolean) Value(ctx sqlite3.Context) {
}
func (b *boolean) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
if arg[0].Type() == sqlite3.NULL {
return
}
if arg[0].Bool() {
a := arg[0]
if a.Bool() {
b.count++
}
b.total++
if a.Type() != sqlite3.NULL {
b.total++
}
}
func (b *boolean) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
if arg[0].Type() == sqlite3.NULL {
return
}
if arg[0].Bool() {
a := arg[0]
if a.Bool() {
b.count--
}
b.total--
if a.Type() != sqlite3.NULL {
b.total--
}
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/stats"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
@@ -18,8 +17,6 @@ func TestRegister_boolean(t *testing.T) {
}
defer db.Close()
stats.Register(db)
err = db.Exec(`CREATE TABLE data (x)`)
if err != nil {
t.Fatal(err)

19
ext/stats/kahan.go Normal file
View File

@@ -0,0 +1,19 @@
package stats
// https://en.wikipedia.org/wiki/Kahan_summation_algorithm
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
}

112
ext/stats/mode.go Normal file
View File

@@ -0,0 +1,112 @@
package stats
import (
"unsafe"
"github.com/ncruces/go-sqlite3"
)
func newMode() sqlite3.AggregateFunction {
return &mode{}
}
type mode struct {
ints counter[int64]
reals counter[float64]
texts counter[string]
blobs counter[string]
}
func (m mode) Value(ctx sqlite3.Context) {
var (
max = 0
typ = sqlite3.NULL
i64 int64
f64 float64
str string
)
for k, v := range m.ints {
if v > max || v == max && k < i64 {
typ = sqlite3.INTEGER
max = v
i64 = k
}
}
f64 = float64(i64)
for k, v := range m.reals {
if v > max || v == max && k < f64 {
typ = sqlite3.FLOAT
max = v
f64 = k
}
}
for k, v := range m.texts {
if v > max || v == max && typ == sqlite3.TEXT && k < str {
typ = sqlite3.TEXT
max = v
str = k
}
}
for k, v := range m.blobs {
if v > max || v == max && typ == sqlite3.BLOB && k < str {
typ = sqlite3.BLOB
max = v
str = k
}
}
switch typ {
case sqlite3.INTEGER:
ctx.ResultInt64(i64)
case sqlite3.FLOAT:
ctx.ResultFloat(f64)
case sqlite3.TEXT:
ctx.ResultText(str)
case sqlite3.BLOB:
ctx.ResultBlob(unsafe.Slice(unsafe.StringData(str), len(str)))
}
}
func (b *mode) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
switch arg[0].Type() {
case sqlite3.INTEGER:
b.ints.add(arg[0].Int64())
case sqlite3.FLOAT:
b.reals.add(arg[0].Float())
case sqlite3.TEXT:
b.texts.add(arg[0].Text())
case sqlite3.BLOB:
b.blobs.add(string(arg[0].RawBlob()))
}
}
func (b *mode) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
switch arg[0].Type() {
case sqlite3.INTEGER:
b.ints.del(arg[0].Int64())
case sqlite3.FLOAT:
b.reals.del(arg[0].Float())
case sqlite3.TEXT:
b.texts.del(arg[0].Text())
case sqlite3.BLOB:
b.blobs.del(string(arg[0].RawBlob()))
}
}
type counter[T comparable] map[T]int
func (c *counter[T]) add(k T) {
if (*c) == nil {
(*c) = make(counter[T])
}
(*c)[k]++
}
func (c counter[T]) del(k T) {
switch n := c[k]; n {
default:
c[k] = n - 1
case 1:
delete(c, k)
case 0:
}
}

85
ext/stats/mode_test.go Normal file
View File

@@ -0,0 +1,85 @@
package stats_test
import (
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func TestRegister_mode(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
stmt, _, err := db.Prepare(`SELECT mode(column1) FROM (VALUES (NULL), (1), (NULL), (2), (NULL), (3), (3))`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnInt(0); got != 3 {
t.Errorf("got %v, want 3", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES (1), (1), (2), (2), (3))`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnInt(0); got != 1 {
t.Errorf("got %v, want 1", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES (0.5), (1), (2.5), (2), (2.5))`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 2.5 {
t.Errorf("got %v, want 2.5", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES ('red'), ('green'), ('blue'), ('red'))`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnText(0); got != "red" {
t.Errorf("got %q, want red", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES (X'cafebabe'), ('green'), ('blue'), (X'cafebabe'))`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnText(0); got != "\xca\xfe\xba\xbe" {
t.Errorf("got %q, want cafebabe", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`
SELECT mode(column1) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)
FROM (VALUES (1), (1), (2.5), ('blue'), (X'cafebabe'), (1), (1))
`)
if err != nil {
t.Fatal(err)
}
for stmt.Step() {
}
stmt.Close()
}

101
ext/stats/moments.go Normal file
View File

@@ -0,0 +1,101 @@
package stats
import "math"
// FisherPearson skewness and kurtosis using
// Terriberry's algorithm with Kahan summation:
// https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Higher-order_statistics
type moments struct {
m1, m2, m3, m4 kahan
n int64
}
func (m moments) mean() float64 {
return m.m1.hi
}
func (m moments) var_pop() float64 {
return m.m2.hi / float64(m.n)
}
func (m moments) var_samp() float64 {
return m.m2.hi / float64(m.n-1) // Bessel's correction
}
func (m moments) stddev_pop() float64 {
return math.Sqrt(m.var_pop())
}
func (m moments) stddev_samp() float64 {
return math.Sqrt(m.var_samp())
}
func (m moments) skewness_pop() float64 {
m2 := m.m2.hi
if div := m2 * m2 * m2; div != 0 {
return m.m3.hi * math.Sqrt(float64(m.n)/div)
}
return math.NaN()
}
func (m moments) skewness_samp() float64 {
n := m.n
// https://mathworks.com/help/stats/skewness.html#f1132178
return m.skewness_pop() * math.Sqrt(float64(n*(n-1))) / float64(n-2)
}
func (m moments) kurtosis_pop() float64 {
return m.raw_kurtosis_pop() - 3
}
func (m moments) raw_kurtosis_pop() float64 {
m2 := m.m2.hi
if div := m2 * m2; div != 0 {
return m.m4.hi * float64(m.n) / div
}
return math.NaN()
}
func (m moments) kurtosis_samp() float64 {
n := m.n
k := math.FMA(m.raw_kurtosis_pop(), float64(n+1), float64(3-3*n))
return k * float64(n-1) / float64((n-2)*(n-3))
}
func (m moments) raw_kurtosis_samp() float64 {
n := m.n
// https://mathworks.com/help/stats/kurtosis.html#f4975293
k := math.FMA(m.raw_kurtosis_pop(), float64(n+1), float64(3-3*n))
return math.FMA(k, float64(n-1)/float64((n-2)*(n-3)), 3)
}
func (m *moments) enqueue(x float64) {
n := m.n + 1
m.n = n
d1 := x - m.m1.hi - m.m1.lo
dn := d1 / float64(n)
d2 := dn * dn
t1 := d1 * dn * float64(n-1)
m.m4.add(t1*d2*float64(n*n-3*n+3) + 6*d2*m.m2.hi - 4*dn*m.m3.hi)
m.m3.add(t1*dn*float64(n-2) - 3*dn*m.m2.hi)
m.m2.add(t1)
m.m1.add(dn)
}
func (m *moments) dequeue(x float64) {
n := m.n - 1
if n <= 0 {
*m = moments{}
return
}
m.n = n
d1 := x - m.m1.hi - m.m1.lo
dn := d1 / float64(n)
d2 := dn * dn
t1 := d1 * dn * float64(n+1)
m.m4.sub(t1*d2*float64(n*n+3*n+3) - 6*d2*m.m2.hi - 4*dn*m.m3.hi)
m.m3.sub(t1*dn*float64(n+2) - 3*dn*m.m2.hi)
m.m2.sub(t1)
m.m1.sub(dn)
}

87
ext/stats/moments_test.go Normal file
View File

@@ -0,0 +1,87 @@
package stats
import (
"math"
"testing"
)
func Test_moments(t *testing.T) {
t.Parallel()
var s1 moments
s1.enqueue(1)
s1.dequeue(1)
if !math.IsNaN(s1.skewness_pop()) {
t.Errorf("want NaN")
}
if !math.IsNaN(s1.raw_kurtosis_pop()) {
t.Errorf("want NaN")
}
s1.enqueue(+0.5377)
s1.enqueue(+1.8339)
s1.enqueue(-2.2588)
s1.enqueue(+0.8622)
s1.enqueue(+0.3188)
s1.enqueue(-1.3077)
s1.enqueue(-0.4336)
s1.enqueue(+0.3426)
s1.enqueue(+3.5784)
s1.enqueue(+2.7694)
if got := s1.skewness_pop(); float32(got) != 0.106098293 {
t.Errorf("got %v, want 0.1061", got)
}
if got := s1.skewness_samp(); float32(got) != 0.1258171 {
t.Errorf("got %v, want 0.1258", got)
}
if got := s1.raw_kurtosis_pop(); float32(got) != 2.3121266 {
t.Errorf("got %v, want 2.3121", got)
}
if got := s1.raw_kurtosis_samp(); float32(got) != 2.7482237 {
t.Errorf("got %v, want 2.7483", got)
}
var s2 welford
s2.enqueue(+0.5377)
s2.enqueue(+1.8339)
s2.enqueue(-2.2588)
s2.enqueue(+0.8622)
s2.enqueue(+0.3188)
s2.enqueue(-1.3077)
s2.enqueue(-0.4336)
s2.enqueue(+0.3426)
s2.enqueue(+3.5784)
s2.enqueue(+2.7694)
if got, want := s1.mean(), s2.mean(); got != want {
t.Errorf("got %v, want %v", got, want)
}
if got, want := s1.stddev_pop(), s2.stddev_pop(); got != want {
t.Errorf("got %v, want %v", got, want)
}
if got, want := s1.stddev_samp(), s2.stddev_samp(); got != want {
t.Errorf("got %v, want %v", got, want)
}
s1.enqueue(math.Pi)
s1.enqueue(math.Sqrt2)
s1.enqueue(math.E)
s1.dequeue(math.Pi)
s1.dequeue(math.E)
s1.dequeue(math.Sqrt2)
if got := s1.skewness_pop(); float32(got) != 0.106098293 {
t.Errorf("got %v, want 0.1061", got)
}
if got := s1.skewness_samp(); float32(got) != 0.1258171 {
t.Errorf("got %v, want 0.1258", got)
}
if got := s1.raw_kurtosis_pop(); float32(got) != 2.3121266 {
t.Errorf("got %v, want 2.3121", got)
}
if got := s1.raw_kurtosis_samp(); float32(got) != 2.7482237 {
t.Errorf("got %v, want 2.7483", got)
}
}

View File

@@ -11,8 +11,12 @@ import (
"github.com/ncruces/sort/quick"
)
// Compatible with:
// https://sqlite.org/src/file/ext/misc/percentile.c
const (
median = iota
percentile_100
percentile_cont
percentile_disc
)
@@ -28,17 +32,25 @@ type percentile struct {
}
func (q *percentile) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
if a := arg[0]; a.NumericType() != sqlite3.NULL {
q.nums = append(q.nums, a.Float())
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
q.nums = append(q.nums, f)
}
if q.kind != median {
q.arg1 = arg[1].Blob(q.arg1[:0])
if q.kind != median && q.arg1 == nil {
q.arg1 = append(q.arg1, arg[1].RawText()...)
}
}
func (q *percentile) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
// Implementing inverse allows certain queries that don't really need it to succeed.
ctx.ResultError(util.ErrorString("percentile: may not be used as a window function"))
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
i := slices.Index(q.nums, f)
l := len(q.nums) - 1
q.nums[i] = q.nums[l]
q.nums = q.nums[:l]
}
}
func (q *percentile) Value(ctx sqlite3.Context) {
@@ -52,39 +64,42 @@ func (q *percentile) Value(ctx sqlite3.Context) {
floats []float64
)
if q.kind == median {
float, err = getPercentile(q.nums, 0.5, false)
float, err = q.at(0.5)
ctx.ResultFloat(float)
} else if err = json.Unmarshal(q.arg1, &float); err == nil {
float, err = getPercentile(q.nums, float, q.kind == percentile_disc)
float, err = q.at(float)
ctx.ResultFloat(float)
} else if err = json.Unmarshal(q.arg1, &floats); err == nil {
err = getPercentiles(q.nums, floats, q.kind == percentile_disc)
err = q.atMore(floats)
ctx.ResultJSON(floats)
}
if err != nil {
ctx.ResultError(fmt.Errorf("percentile: %w", err))
ctx.ResultError(fmt.Errorf("percentile: %w", err)) // notest
}
}
func getPercentile(nums []float64, pos float64, disc bool) (float64, error) {
func (q *percentile) at(pos float64) (float64, error) {
if q.kind == percentile_100 {
pos = pos / 100
}
if pos < 0 || pos > 1 {
return 0, util.ErrorString("invalid pos")
}
i, f := math.Modf(pos * float64(len(nums)-1))
m0 := quick.Select(nums, int(i))
i, f := math.Modf(pos * float64(len(q.nums)-1))
m0 := quick.Select(q.nums, int(i))
if f == 0 || disc {
if f == 0 || q.kind == percentile_disc {
return m0, nil
}
m1 := slices.Min(nums[int(i)+1:])
return math.FMA(f, m1, -math.FMA(f, m0, -m0)), nil
m1 := slices.Min(q.nums[int(i)+1:])
return util.Lerp(m0, m1, f), nil
}
func getPercentiles(nums []float64, pos []float64, disc bool) error {
func (q *percentile) atMore(pos []float64) error {
for i := range pos {
v, err := getPercentile(nums, pos[i], disc)
v, err := q.at(pos[i])
if err != nil {
return err
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/stats"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
@@ -19,8 +18,6 @@ func TestRegister_percentile(t *testing.T) {
}
defer db.Close()
stats.Register(db)
err = db.Exec(`CREATE TABLE data (x)`)
if err != nil {
t.Fatal(err)
@@ -34,6 +31,7 @@ func TestRegister_percentile(t *testing.T) {
stmt, _, err := db.Prepare(`
SELECT
median(x),
percentile(x, 50),
percentile_disc(x, 0.5),
percentile_cont(x, '[0.25, 0.5, 0.75]')
FROM data`)
@@ -44,11 +42,14 @@ func TestRegister_percentile(t *testing.T) {
if got := stmt.ColumnFloat(0); got != 10 {
t.Errorf("got %v, want 10", got)
}
if got := stmt.ColumnFloat(1); got != 7 {
if got := stmt.ColumnFloat(1); got != 10 {
t.Errorf("got %v, want 10", got)
}
if got := stmt.ColumnFloat(2); got != 7 {
t.Errorf("got %v, want 7", got)
}
var got []float64
if err := stmt.ColumnJSON(2, &got); err != nil {
if err := stmt.ColumnJSON(3, &got); err != nil {
t.Error(err)
}
if !slices.Equal(got, []float64{6.25, 10, 13.75}) {
@@ -57,9 +58,44 @@ func TestRegister_percentile(t *testing.T) {
}
stmt.Close()
stmt, _, err = db.Prepare(`
SELECT
median(x) OVER (ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)
FROM data`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 5.5 {
t.Errorf("got %v, want 5.5", got)
}
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 7 {
t.Errorf("got %v, want 7", got)
}
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 10 {
t.Errorf("got %v, want 10", got)
}
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 14.5 {
t.Errorf("got %v, want 14.5", got)
}
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 16 {
t.Errorf("got %v, want 16", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`
SELECT
median(x),
percentile(x, 50),
percentile_disc(x, 0.5),
percentile_cont(x, '[0.25, 0.5, 0.75]')
FROM data
@@ -74,8 +110,11 @@ func TestRegister_percentile(t *testing.T) {
if got := stmt.ColumnFloat(1); got != 4 {
t.Errorf("got %v, want 4", got)
}
if got := stmt.ColumnFloat(2); got != 4 {
t.Errorf("got %v, want 4", got)
}
var got []float64
if err := stmt.ColumnJSON(2, &got); err != nil {
if err := stmt.ColumnJSON(3, &got); err != nil {
t.Error(err)
}
if !slices.Equal(got, []float64{4, 4, 4}) {
@@ -87,6 +126,7 @@ func TestRegister_percentile(t *testing.T) {
stmt, _, err = db.Prepare(`
SELECT
median(x),
percentile(x, 50),
percentile_disc(x, 0.5),
percentile_cont(x, '[0.25, 0.5, 0.75]')
FROM data
@@ -104,6 +144,9 @@ func TestRegister_percentile(t *testing.T) {
if got := stmt.ColumnType(2); got != sqlite3.NULL {
t.Error("want NULL")
}
if got := stmt.ColumnType(3); got != sqlite3.NULL {
t.Error("want NULL")
}
}
stmt.Close()

View File

@@ -1,13 +1,17 @@
// 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
// - stddev_pop: population standard deviation
// - stddev_samp: sample standard deviation
// - skewness_pop: Pearson population skewness
// - skewness_samp: Pearson sample skewness
// - kurtosis_pop: Fisher population excess kurtosis
// - kurtosis_samp: Fisher sample excess kurtosis
// - covar_pop: population covariance
// - covar_samp: sample covariance
// - corr: correlation coefficient
// - corr: Pearson correlation coefficient
// - regr_r2: correlation coefficient squared
// - regr_avgx: average of the independent variable
// - regr_avgy: average of the dependent variable
@@ -17,10 +21,12 @@
// - 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
// - percentile_disc: discrete percentile
// - percentile_cont: continuous percentile
// - median: median value
// - regr_json: all regr stats as a JSON object
// - percentile_disc: discrete quantile
// - percentile_cont: continuous quantile
// - percentile: continuous percentile
// - median: middle value
// - mode: most frequent value
// - every: boolean and
// - some: boolean or
//
@@ -44,33 +50,45 @@
// [ANSI SQL Aggregate Functions]: https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
package stats
import "github.com/ncruces/go-sqlite3"
import (
"errors"
"github.com/ncruces/go-sqlite3"
)
// Register registers statistics functions.
func Register(db *sqlite3.Conn) {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
db.CreateWindowFunction("var_pop", 1, flags, newVariance(var_pop))
db.CreateWindowFunction("var_samp", 1, flags, newVariance(var_samp))
db.CreateWindowFunction("stddev_pop", 1, flags, newVariance(stddev_pop))
db.CreateWindowFunction("stddev_samp", 1, flags, newVariance(stddev_samp))
db.CreateWindowFunction("covar_pop", 2, flags, newCovariance(var_pop))
db.CreateWindowFunction("covar_samp", 2, flags, newCovariance(var_samp))
db.CreateWindowFunction("corr", 2, flags, newCovariance(corr))
db.CreateWindowFunction("regr_r2", 2, flags, newCovariance(regr_r2))
db.CreateWindowFunction("regr_sxx", 2, flags, newCovariance(regr_sxx))
db.CreateWindowFunction("regr_syy", 2, flags, newCovariance(regr_syy))
db.CreateWindowFunction("regr_sxy", 2, flags, newCovariance(regr_sxy))
db.CreateWindowFunction("regr_avgx", 2, flags, newCovariance(regr_avgx))
db.CreateWindowFunction("regr_avgy", 2, flags, newCovariance(regr_avgy))
db.CreateWindowFunction("regr_slope", 2, flags, newCovariance(regr_slope))
db.CreateWindowFunction("regr_intercept", 2, flags, newCovariance(regr_intercept))
db.CreateWindowFunction("regr_count", 2, flags, newCovariance(regr_count))
db.CreateWindowFunction("regr_json", 2, flags, newCovariance(regr_json))
db.CreateWindowFunction("median", 1, flags, newPercentile(median))
db.CreateWindowFunction("percentile_cont", 2, flags, newPercentile(percentile_cont))
db.CreateWindowFunction("percentile_disc", 2, flags, newPercentile(percentile_disc))
db.CreateWindowFunction("every", 1, flags, newBoolean(every))
db.CreateWindowFunction("some", 1, flags, newBoolean(some))
func Register(db *sqlite3.Conn) error {
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
const order = sqlite3.SELFORDER1 | flags
return errors.Join(
db.CreateWindowFunction("var_pop", 1, flags, newVariance(var_pop)),
db.CreateWindowFunction("var_samp", 1, flags, newVariance(var_samp)),
db.CreateWindowFunction("stddev_pop", 1, flags, newVariance(stddev_pop)),
db.CreateWindowFunction("stddev_samp", 1, flags, newVariance(stddev_samp)),
db.CreateWindowFunction("skewness_pop", 1, flags, newMoments(skewness_pop)),
db.CreateWindowFunction("skewness_samp", 1, flags, newMoments(skewness_samp)),
db.CreateWindowFunction("kurtosis_pop", 1, flags, newMoments(kurtosis_pop)),
db.CreateWindowFunction("kurtosis_samp", 1, flags, newMoments(kurtosis_samp)),
db.CreateWindowFunction("covar_pop", 2, flags, newCovariance(var_pop)),
db.CreateWindowFunction("covar_samp", 2, flags, newCovariance(var_samp)),
db.CreateWindowFunction("corr", 2, flags, newCovariance(corr)),
db.CreateWindowFunction("regr_r2", 2, flags, newCovariance(regr_r2)),
db.CreateWindowFunction("regr_sxx", 2, flags, newCovariance(regr_sxx)),
db.CreateWindowFunction("regr_syy", 2, flags, newCovariance(regr_syy)),
db.CreateWindowFunction("regr_sxy", 2, flags, newCovariance(regr_sxy)),
db.CreateWindowFunction("regr_avgx", 2, flags, newCovariance(regr_avgx)),
db.CreateWindowFunction("regr_avgy", 2, flags, newCovariance(regr_avgy)),
db.CreateWindowFunction("regr_slope", 2, flags, newCovariance(regr_slope)),
db.CreateWindowFunction("regr_intercept", 2, flags, newCovariance(regr_intercept)),
db.CreateWindowFunction("regr_count", 2, flags, newCovariance(regr_count)),
db.CreateWindowFunction("regr_json", 2, flags, newCovariance(regr_json)),
db.CreateWindowFunction("median", 1, order, newPercentile(median)),
db.CreateWindowFunction("percentile", 2, order, newPercentile(percentile_100)),
db.CreateWindowFunction("percentile_cont", 2, order, newPercentile(percentile_cont)),
db.CreateWindowFunction("percentile_disc", 2, order, newPercentile(percentile_disc)),
db.CreateWindowFunction("every", 1, flags, newBoolean(every)),
db.CreateWindowFunction("some", 1, flags, newBoolean(some)),
db.CreateWindowFunction("mode", 1, order, newMode))
}
const (
@@ -78,6 +96,10 @@ const (
var_samp
stddev_pop
stddev_samp
skewness_pop
skewness_samp
kurtosis_pop
kurtosis_samp
corr
regr_r2
regr_sxx
@@ -91,6 +113,23 @@ const (
regr_json
)
func special(kind int, n int64) (null, zero bool) {
switch kind {
case var_pop, stddev_pop, regr_sxx, regr_syy, regr_sxy:
return n <= 0, n == 1
case regr_avgx, regr_avgy:
return n <= 0, false
case kurtosis_samp:
return n <= 3, false
case skewness_samp:
return n <= 2, false
case skewness_pop:
return n <= 1, n == 2
default:
return n <= 1, false
}
}
func newVariance(kind int) func() sqlite3.AggregateFunction {
return func() sqlite3.AggregateFunction { return &variance{kind: kind} }
}
@@ -101,6 +140,14 @@ type variance struct {
}
func (fn *variance) Value(ctx sqlite3.Context) {
switch null, zero := special(fn.kind, fn.n); {
case zero:
ctx.ResultFloat(0)
return
case null:
return
}
var r float64
switch fn.kind {
case var_pop:
@@ -116,14 +163,18 @@ func (fn *variance) Value(ctx sqlite3.Context) {
}
func (fn *variance) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
if a := arg[0]; a.NumericType() != sqlite3.NULL {
fn.enqueue(a.Float())
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
fn.enqueue(f)
}
}
func (fn *variance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
if a := arg[0]; a.NumericType() != sqlite3.NULL {
fn.dequeue(a.Float())
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
fn.dequeue(f)
}
}
@@ -137,6 +188,18 @@ type covariance struct {
}
func (fn *covariance) Value(ctx sqlite3.Context) {
if fn.kind == regr_count {
ctx.ResultInt64(fn.regr_count())
return
}
switch null, zero := special(fn.kind, fn.n); {
case zero:
ctx.ResultFloat(0)
return
case null:
return
}
var r float64
switch fn.kind {
case var_pop:
@@ -161,26 +224,80 @@ func (fn *covariance) Value(ctx sqlite3.Context) {
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())
var buf [128]byte
ctx.ResultRawText(fn.regr_json(buf[:0]))
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())
b, a := arg[1], arg[0] // avoid a bounds check
fa := a.Float()
fb := b.Float()
if true &&
(fa != 0.0 || a.NumericType() != sqlite3.NULL) &&
(fb != 0.0 || b.NumericType() != sqlite3.NULL) {
fn.enqueue(fa, fb)
}
}
func (fn *covariance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
a, b := arg[0], arg[1]
if a.NumericType() != sqlite3.NULL && b.NumericType() != sqlite3.NULL {
fn.dequeue(a.Float(), b.Float())
b, a := arg[1], arg[0] // avoid a bounds check
fa := a.Float()
fb := b.Float()
if true &&
(fa != 0.0 || a.NumericType() != sqlite3.NULL) &&
(fb != 0.0 || b.NumericType() != sqlite3.NULL) {
fn.dequeue(fa, fb)
}
}
func newMoments(kind int) func() sqlite3.AggregateFunction {
return func() sqlite3.AggregateFunction { return &momentfn{kind: kind} }
}
type momentfn struct {
kind int
moments
}
func (fn *momentfn) Value(ctx sqlite3.Context) {
switch null, zero := special(fn.kind, fn.n); {
case zero:
ctx.ResultFloat(0)
return
case null:
return
}
var r float64
switch fn.kind {
case skewness_pop:
r = fn.skewness_pop()
case skewness_samp:
r = fn.skewness_samp()
case kurtosis_pop:
r = fn.kurtosis_pop()
case kurtosis_samp:
r = fn.kurtosis_samp()
}
ctx.ResultFloat(r)
}
func (fn *momentfn) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
fn.enqueue(f)
}
}
func (fn *momentfn) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
fn.dequeue(f)
}
}

View File

@@ -10,6 +10,11 @@ import (
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func TestMain(m *testing.M) {
sqlite3.AutoExtension(stats.Register)
m.Run()
}
func TestRegister_variance(t *testing.T) {
t.Parallel()
@@ -19,23 +24,34 @@ func TestRegister_variance(t *testing.T) {
}
defer db.Close()
stats.Register(db)
err = db.Exec(`CREATE TABLE data (x)`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT stddev_pop(x) FROM data`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != sqlite3.NULL {
t.Errorf("got %v, want NULL", got)
}
}
stmt.Close()
err = db.Exec(`INSERT INTO data (x) VALUES (4), (7.0), ('13'), (NULL), (16)`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`
stmt, _, err = db.Prepare(`
SELECT
sum(x), avg(x),
var_samp(x), var_pop(x),
stddev_samp(x), stddev_pop(x)
stddev_samp(x), stddev_pop(x),
skewness_samp(x), skewness_pop(x),
kurtosis_samp(x), kurtosis_pop(x)
FROM data`)
if err != nil {
t.Fatal(err)
@@ -59,10 +75,27 @@ func TestRegister_variance(t *testing.T) {
if got := stmt.ColumnFloat(5); got != math.Sqrt(22.5) {
t.Errorf("got %v, want √22.5", got)
}
if got := stmt.ColumnFloat(6); got != 0 {
t.Errorf("got %v, want zero", got)
}
if got := stmt.ColumnFloat(7); got != 0 {
t.Errorf("got %v, want zero", got)
}
if got := stmt.ColumnFloat(8); float32(got) != -3.3 {
t.Errorf("got %v, want -3.3", got)
}
if got := stmt.ColumnFloat(9); got != -1.64 {
t.Errorf("got %v, want -1.64", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`SELECT var_samp(x) OVER (ROWS 1 PRECEDING) FROM data`)
stmt, _, err = db.Prepare(`
SELECT
var_samp(x) OVER (ROWS 1 PRECEDING),
var_pop(x) OVER (ROWS 1 PRECEDING),
skewness_pop(x) OVER (ROWS 1 PRECEDING)
FROM data`)
if err != nil {
t.Fatal(err)
}
@@ -88,19 +121,31 @@ func TestRegister_covariance(t *testing.T) {
}
defer db.Close()
stats.Register(db)
err = db.Exec(`CREATE TABLE data (y, x)`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT regr_count(y, x), regr_json(y, x) FROM data`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnInt(0); got != 0 {
t.Errorf("got %v, want 0", got)
}
if got := stmt.ColumnType(1); got != sqlite3.NULL {
t.Errorf("got %v, want NULL", got)
}
}
stmt.Close()
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
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),
@@ -156,7 +201,12 @@ func TestRegister_covariance(t *testing.T) {
}
stmt.Close()
stmt, _, err = db.Prepare(`SELECT covar_samp(y, x) OVER (ROWS 1 PRECEDING) FROM data`)
stmt, _, err = db.Prepare(`
SELECT
covar_samp(y, x) OVER (ROWS 1 PRECEDING),
covar_pop(y, x) OVER (ROWS 1 PRECEDING),
regr_avgx(y, x) OVER (ROWS 1 PRECEDING)
FROM data`)
if err != nil {
t.Fatal(err)
}
@@ -170,6 +220,9 @@ func TestRegister_covariance(t *testing.T) {
t.Errorf("got %v, want %v", got, want[i])
}
}
if stmt.Err() != nil {
t.Fatal(stmt.Err())
}
stmt.Close()
}
@@ -217,8 +270,6 @@ func Benchmark_variance(b *testing.B) {
}
defer db.Close()
stats.Register(db)
stmt, _, err := db.Prepare(`SELECT var_pop(value) FROM generate_series(0, ?)`)
if err != nil {
b.Fatal(err)

View File

@@ -3,22 +3,20 @@ package stats
import (
"math"
"strconv"
"strings"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Welford's algorithm with Kahan summation:
// The effect of truncation in statistical computation [van Reeken, AJ 1970]
// 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 {
func (w welford) mean() float64 {
return w.m1.hi
}
@@ -39,17 +37,23 @@ func (w welford) stddev_samp() float64 {
}
func (w *welford) enqueue(x float64) {
w.n++
n := w.n + 1
w.n = n
d1 := x - w.m1.hi - w.m1.lo
w.m1.add(d1 / float64(w.n))
w.m1.add(d1 / float64(n))
d2 := x - w.m1.hi - w.m1.lo
w.m2.add(d1 * d2)
}
func (w *welford) dequeue(x float64) {
w.n--
n := w.n - 1
if n <= 0 {
*w = welford{}
return
}
w.n = n
d1 := x - w.m1.hi - w.m1.lo
w.m1.sub(d1 / float64(w.n))
w.m1.sub(d1 / float64(n))
d2 := x - w.m1.hi - w.m1.lo
w.m2.sub(d1 * d2)
}
@@ -112,38 +116,35 @@ 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) regr_json(dst []byte) []byte {
dst = append(dst, `{"count":`...)
dst = strconv.AppendInt(dst, w.regr_count(), 10)
dst = append(dst, `,"avgy":`...)
dst = util.AppendNumber(dst, w.regr_avgy())
dst = append(dst, `,"avgx":`...)
dst = util.AppendNumber(dst, w.regr_avgx())
dst = append(dst, `,"syy":`...)
dst = util.AppendNumber(dst, w.regr_syy())
dst = append(dst, `,"sxx":`...)
dst = util.AppendNumber(dst, w.regr_sxx())
dst = append(dst, `,"sxy":`...)
dst = util.AppendNumber(dst, w.regr_sxy())
dst = append(dst, `,"slope":`...)
dst = util.AppendNumber(dst, w.regr_slope())
dst = append(dst, `,"intercept":`...)
dst = util.AppendNumber(dst, w.regr_intercept())
dst = append(dst, `,"r2":`...)
dst = util.AppendNumber(dst, w.regr_r2())
return append(dst, '}')
}
func (w *welford2) enqueue(y, x float64) {
w.n++
n := w.n + 1
w.n = 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))
w.m1y.add(d1y / float64(n))
w.m1x.add(d1x / float64(n))
d2y := y - w.m1y.hi - w.m1y.lo
d2x := x - w.m1x.hi - w.m1x.lo
w.m2y.add(d1y * d2y)
@@ -152,30 +153,19 @@ func (w *welford2) enqueue(y, x float64) {
}
func (w *welford2) dequeue(y, x float64) {
w.n--
n := w.n - 1
if n <= 0 {
*w = welford2{}
return
}
w.n = 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))
w.m1y.sub(d1y / float64(n))
w.m1x.sub(d1x / float64(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
}

View File

@@ -9,12 +9,14 @@ func Test_welford(t *testing.T) {
t.Parallel()
var s1, s2 welford
s1.enqueue(1)
s1.dequeue(1)
s1.enqueue(4)
s1.enqueue(7)
s1.enqueue(13)
s1.enqueue(16)
if got := s1.average(); got != 10 {
if got := s1.mean(); got != 10 {
t.Errorf("got %v, want 10", got)
}
if got := s1.var_samp(); got != 30 {
@@ -43,6 +45,8 @@ func Test_covar(t *testing.T) {
t.Parallel()
var c1, c2 welford2
c1.enqueue(1, 1)
c1.dequeue(1, 1)
c1.enqueue(3, 70)
c1.enqueue(5, 80)

View File

@@ -11,6 +11,12 @@
// - the REGEXP operator uses Go [regexp/syntax];
// - collation sequences use [collate].
//
// It also provides (approximately) from PostgreSQL:
// - casefold(),
// - initcap(),
// - normalize(),
// - unaccent().
//
// Expect subtle differences (e.g.) in the handling of Turkish case folding.
//
// [ICU extension]: https://sqlite.org/src/dir/ext/icu
@@ -18,41 +24,63 @@ package unicode
import (
"bytes"
"errors"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
"golang.org/x/text/cases"
"golang.org/x/text/collate"
"golang.org/x/text/language"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Set RegisterLike to false to not register a Unicode aware LIKE operator.
// Overriding the built-in LIKE operator disables the [LIKE optimization].
//
// [LIKE optimization]: https://sqlite.org/optoverview.html#the_like_optimization
var RegisterLike = true
// Register registers Unicode aware functions for a database connection.
func Register(db *sqlite3.Conn) {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
func Register(db *sqlite3.Conn) error {
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
var lkfn sqlite3.ScalarFunction
if RegisterLike {
lkfn = like
}
return errors.Join(
db.CreateFunction("like", 2, flags, lkfn),
db.CreateFunction("like", 3, flags, lkfn),
db.CreateFunction("upper", 1, flags, upper),
db.CreateFunction("upper", 2, flags, upper),
db.CreateFunction("lower", 1, flags, lower),
db.CreateFunction("lower", 2, flags, lower),
db.CreateFunction("regexp", 2, flags, regex),
db.CreateFunction("initcap", 1, flags, initcap),
db.CreateFunction("initcap", 2, flags, initcap),
db.CreateFunction("casefold", 1, flags, casefold),
db.CreateFunction("unaccent", 1, flags, unaccent),
db.CreateFunction("normalize", 1, flags, normalize),
db.CreateFunction("normalize", 2, flags, normalize),
db.CreateFunction("icu_load_collation", 2, sqlite3.DIRECTONLY,
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
name := arg[1].Text()
if name == "" {
return
}
db.CreateFunction("like", 2, flags, like)
db.CreateFunction("like", 3, flags, like)
db.CreateFunction("upper", 1, flags, upper)
db.CreateFunction("upper", 2, flags, upper)
db.CreateFunction("lower", 1, flags, lower)
db.CreateFunction("lower", 2, flags, lower)
db.CreateFunction("regexp", 2, flags, regex)
db.CreateFunction("icu_load_collation", 2, sqlite3.DIRECTONLY,
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
name := arg[1].Text()
if name == "" {
return
}
err := RegisterCollation(db, arg[0].Text(), name)
if err != nil {
ctx.ResultError(err)
return
}
})
err := RegisterCollation(ctx.Conn(), arg[0].Text(), name)
if err != nil {
ctx.ResultError(err)
return // notest
}
}))
}
// RegisterCollation registers a Unicode collation sequence for a database connection.
@@ -64,6 +92,15 @@ func RegisterCollation(db *sqlite3.Conn, locale, name string) error {
return db.CreateCollation(name, collate.New(tag).Compare)
}
// RegisterCollationsNeeded registers Unicode collation sequences on demand for a database connection.
func RegisterCollationsNeeded(db *sqlite3.Conn) error {
return db.CollationNeeded(func(db *sqlite3.Conn, name string) {
if tag, err := language.Parse(name); err == nil {
db.CreateCollation(name, collate.New(tag).Compare)
}
})
}
func upper(ctx sqlite3.Context, arg ...sqlite3.Value) {
if len(arg) == 1 {
ctx.ResultRawText(bytes.ToUpper(arg[0].RawText()))
@@ -74,7 +111,7 @@ func upper(ctx sqlite3.Context, arg ...sqlite3.Value) {
t, err := language.Parse(arg[1].Text())
if err != nil {
ctx.ResultError(err)
return
return // notest
}
c := cases.Upper(t)
ctx.SetAuxData(1, c)
@@ -93,7 +130,7 @@ func lower(ctx sqlite3.Context, arg ...sqlite3.Value) {
t, err := language.Parse(arg[1].Text())
if err != nil {
ctx.ResultError(err)
return
return // notest
}
c := cases.Lower(t)
ctx.SetAuxData(1, c)
@@ -102,16 +139,74 @@ func lower(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
}
func initcap(ctx sqlite3.Context, arg ...sqlite3.Value) {
if len(arg) == 1 {
ctx.ResultRawText(bytes.Title(arg[0].RawText()))
return
}
cs, ok := ctx.GetAuxData(1).(cases.Caser)
if !ok {
t, err := language.Parse(arg[1].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
c := cases.Title(t)
ctx.SetAuxData(1, c)
cs = c
}
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
}
func casefold(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultRawText(cases.Fold().Bytes(arg[0].RawText()))
}
func unaccent(ctx sqlite3.Context, arg ...sqlite3.Value) {
unaccent := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
res, _, err := transform.Bytes(unaccent, arg[0].RawText())
if err != nil {
ctx.ResultError(err) // notest
} else {
ctx.ResultRawText(res)
}
}
func normalize(ctx sqlite3.Context, arg ...sqlite3.Value) {
form := norm.NFC
if len(arg) > 1 {
switch strings.ToUpper(arg[1].Text()) {
case "NFC":
//
case "NFD":
form = norm.NFD
case "NFKC":
form = norm.NFKC
case "NFKD":
form = norm.NFKD
default:
ctx.ResultError(util.ErrorString("unicode: invalid form"))
return
}
}
res, _, err := transform.Bytes(form, arg[0].RawText())
if err != nil {
ctx.ResultError(err) // notest
} else {
ctx.ResultRawText(res)
}
}
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, ok := ctx.GetAuxData(0).(*regexp.Regexp)
if !ok {
r, err := regexp.Compile(arg[0].Text())
if err != nil {
ctx.ResultError(err)
return
return // notest
}
re = r
ctx.SetAuxData(0, re)
ctx.SetAuxData(0, r)
}
ctx.ResultBool(re.Match(arg[1].RawText()))
}
@@ -127,6 +222,7 @@ func like(ctx sqlite3.Context, arg ...sqlite3.Value) {
return
}
}
_ = arg[1] // bounds check
type likeData struct {
*regexp.Regexp

View File

@@ -2,7 +2,7 @@ package unicode
import (
"errors"
"reflect"
"slices"
"testing"
"github.com/ncruces/go-sqlite3"
@@ -47,6 +47,15 @@ func TestRegister(t *testing.T) {
{`upper('istanbul', 'tr-TR')`, "İSTANBUL"},
{`lower('Dünyanın İlk Borsası', 'tr-TR')`, "dünyanın ilk borsası"},
{`upper('Dünyanın İlk Borsası', 'tr-TR')`, "DÜNYANIN İLK BORSASI"},
{`initcap('Kad je hladno Marko nosi džemper')`, "Kad Je Hladno Marko Nosi Džemper"},
{`initcap('Kad je hladno Marko nosi džemper', 'hr-HR')`, "Kad Je Hladno Marko Nosi Džemper"},
{`normalize(X'61cc88')`, "ä"},
{`normalize(X'61cc88', 'NFC' )`, "ä"},
{`normalize(X'61cc88', 'NFKC')`, "ä"},
{`normalize('ä', 'NFD' )`, "\x61\xcc\x88"},
{`normalize('ä', 'NFKD')`, "\x61\xcc\x88"},
{`casefold('Maße')`, "masse"},
{`unaccent('Hôtel')`, "Hotel"},
{`'Hello' REGEXP 'ell'`, "1"},
{`'Hello' REGEXP 'el.'`, "1"},
{`'Hello' LIKE 'hel_'`, "0"},
@@ -92,7 +101,7 @@ func TestRegister_collation(t *testing.T) {
t.Fatal(err)
}
err = db.Exec(`SELECT icu_load_collation('fr_FR', 'french')`)
err = db.Exec(`SELECT icu_load_collation('fr-FR', 'french')`)
if err != nil {
t.Fatal(err)
}
@@ -112,7 +121,58 @@ func TestRegister_collation(t *testing.T) {
t.Fatal(err)
}
if !reflect.DeepEqual(got, want) {
if !slices.Equal(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 TestRegisterCollationsNeeded(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
RegisterCollationsNeeded(db)
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`INSERT INTO words (word) VALUES ('côte'), ('cote'), ('coter'), ('coté'), ('cotée'), ('côté')`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT word FROM words ORDER BY word COLLATE fr_FR`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
got, want := []string{}, []string{"cote", "coté", "côte", "côté", "cotée", "coter"}
for stmt.Step() {
got = append(got, stmt.ColumnText(0))
}
if err := stmt.Err(); err != nil {
t.Fatal(err)
}
if !slices.Equal(got, want) {
t.Error("not equal")
}
@@ -154,6 +214,14 @@ func TestRegister_error(t *testing.T) {
t.Errorf("got %v, want sqlite3.ERROR", err)
}
err = db.Exec(`SELECT normalize('', 'NF')`)
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")

199
ext/uuid/uuid.go Normal file
View File

@@ -0,0 +1,199 @@
// Package uuid provides functions to generate RFC 4122 UUIDs.
//
// https://sqlite.org/src/file/ext/misc/uuid.c
package uuid
import (
"bytes"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Register registers the SQL functions:
//
// uuid([version], [domain/namespace], [id/data])
//
// Generates a UUID as a string.
//
// uuid_str(u)
//
// Converts a UUID into a well-formed UUID string.
//
// uuid_blob(u)
//
// Converts a UUID into a 16-byte blob.
func Register(db *sqlite3.Conn) error {
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
return errors.Join(
db.CreateFunction("uuid", 0, sqlite3.INNOCUOUS, generate),
db.CreateFunction("uuid", 1, sqlite3.INNOCUOUS, generate),
db.CreateFunction("uuid", 2, sqlite3.INNOCUOUS, generate),
db.CreateFunction("uuid", 3, sqlite3.INNOCUOUS, generate),
db.CreateFunction("uuid_str", 1, flags, toString),
db.CreateFunction("uuid_blob", 1, flags, toBlob),
db.CreateFunction("uuid_extract_version", 1, flags, version),
db.CreateFunction("uuid_extract_timestamp", 1, flags, timestamp))
}
func generate(ctx sqlite3.Context, arg ...sqlite3.Value) {
var (
ver int
err error
u uuid.UUID
)
if len(arg) > 0 {
ver = arg[0].Int()
} else {
ver = 4
}
switch ver {
case 1:
u, err = uuid.NewUUID()
case 4:
u, err = uuid.NewRandom()
case 6:
u, err = uuid.NewV6()
case 7:
u, err = uuid.NewV7()
case 2:
var domain uuid.Domain
if len(arg) > 1 {
domain = uuid.Domain(arg[1].Int64())
if domain == 0 {
if txt := arg[1].RawText(); len(txt) > 0 {
switch txt[0] | 0x20 { // to lower
case 'g': // group
domain = uuid.Group
case 'o': // org
domain = uuid.Org
}
}
}
}
switch {
case len(arg) > 2:
u, err = uuid.NewDCESecurity(domain, uint32(arg[2].Int64()))
case domain == uuid.Person:
u, err = uuid.NewDCEPerson()
case domain == uuid.Group:
u, err = uuid.NewDCEGroup()
default:
err = util.ErrorString("missing id")
}
case 3, 5:
if len(arg) < 2 {
err = util.ErrorString("missing data")
break
}
ns, err := fromValue(arg[1])
if err != nil {
space := arg[1].RawText()
switch {
case bytes.EqualFold(space, []byte("url")):
ns = uuid.NameSpaceURL
case bytes.EqualFold(space, []byte("oid")):
ns = uuid.NameSpaceOID
case bytes.EqualFold(space, []byte("dns")):
ns = uuid.NameSpaceDNS
case bytes.EqualFold(space, []byte("fqdn")):
ns = uuid.NameSpaceDNS
case bytes.EqualFold(space, []byte("x500")):
ns = uuid.NameSpaceX500
default:
ctx.ResultError(err)
return // notest
}
}
if ver == 3 {
u = uuid.NewMD5(ns, arg[2].RawBlob())
} else {
u = uuid.NewSHA1(ns, arg[2].RawBlob())
}
default:
err = fmt.Errorf("invalid version: %d", ver)
}
if err != nil {
ctx.ResultError(fmt.Errorf("uuid: %w", err)) // notest
} else {
ctx.ResultText(u.String())
}
}
func fromValue(arg sqlite3.Value) (u uuid.UUID, err error) {
switch t := arg.Type(); t {
case sqlite3.TEXT:
u, err = uuid.ParseBytes(arg.RawText())
if err != nil {
err = fmt.Errorf("uuid: %w", err)
}
case sqlite3.BLOB:
blob := arg.RawBlob()
if len := len(blob); len != 16 {
err = fmt.Errorf("uuid: invalid BLOB length: %d", len)
} else {
copy(u[:], blob)
}
default:
err = fmt.Errorf("uuid: invalid type: %v", t)
}
return u, err
}
func toBlob(ctx sqlite3.Context, arg ...sqlite3.Value) {
u, err := fromValue(arg[0])
if err != nil {
ctx.ResultError(err) // notest
} else {
ctx.ResultBlob(u[:])
}
}
func toString(ctx sqlite3.Context, arg ...sqlite3.Value) {
u, err := fromValue(arg[0])
if err != nil {
ctx.ResultError(err) // notest
} else {
ctx.ResultText(u.String())
}
}
func version(ctx sqlite3.Context, arg ...sqlite3.Value) {
u, err := fromValue(arg[0])
if err != nil {
ctx.ResultError(err)
return // notest
}
if u.Variant() == uuid.RFC4122 {
ctx.ResultInt64(int64(u.Version()))
}
}
func timestamp(ctx sqlite3.Context, arg ...sqlite3.Value) {
u, err := fromValue(arg[0])
if err != nil {
ctx.ResultError(err)
return // notest
}
if u.Variant() == uuid.RFC4122 {
switch u.Version() {
case 1, 2, 6, 7:
ctx.ResultTime(
time.Unix(u.Time().UnixTime()),
sqlite3.TimeFormatDefault)
}
}
}

211
ext/uuid/uuid_test.go Normal file
View File

@@ -0,0 +1,211 @@
package uuid
import (
"testing"
"time"
"github.com/google/uuid"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Test_generate(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var u uuid.UUID
// Version 4, SQLite compatible
err = db.QueryRow(`SELECT uuid()`).Scan(&u)
if err != nil {
t.Fatal(err)
}
if got := u.Version(); got != 4 {
t.Errorf("got %d, want 4", got)
}
// Invalid version
err = db.QueryRow(`SELECT uuid(8)`).Scan(&u)
if err == nil {
t.Error("want error")
}
// Custom version, no arguments
for _, want := range []uuid.Version{1, 2, 4, 6, 7} {
err = db.QueryRow(`SELECT uuid(?)`, want).Scan(&u)
if err != nil {
t.Fatal(err)
}
if got := u.Version(); got != want {
t.Errorf("got %d, want %d", got, want)
}
}
// Version 2, custom arguments
err = db.QueryRow(`SELECT uuid(2, 4)`).Scan(&u)
if err == nil {
t.Error("want error")
}
err = db.QueryRow(`SELECT uuid(2, 'group')`).Scan(&u)
if err != nil {
t.Fatal(err)
}
if got := u.Version(); got != 2 {
t.Errorf("got %d, want 2", got)
}
if got := u.Domain(); got != uuid.Group {
t.Errorf("got %d, want 1", got)
}
dce := []struct {
out uuid.Domain
in any
id uint32
}{
{uuid.Person, "user", 42},
{uuid.Group, "group", 42},
{uuid.Org, "org", 42},
{uuid.Person, 0, 42},
{uuid.Group, 1, 42},
{uuid.Org, 2, 42},
{3, 3, 42},
}
for _, tt := range dce {
err = db.QueryRow(`SELECT uuid(2, ?, ?)`, tt.in, tt.id).Scan(&u)
if err != nil {
t.Fatal(err)
}
if got := u.Version(); got != 2 {
t.Errorf("got %d, want 2", got)
}
if got := u.Domain(); got != tt.out {
t.Errorf("got %d, want %d", got, tt.out)
}
if got := u.ID(); got != tt.id {
t.Errorf("got %d, want %d", got, tt.id)
}
}
// Versions 3 and 5
err = db.QueryRow(`SELECT uuid(3)`).Scan(&u)
if err == nil {
t.Error("want error")
}
err = db.QueryRow(`SELECT uuid(3, 0, '')`).Scan(&u)
if err == nil {
t.Error("want error")
}
var tstamp time.Time
var version uuid.Version
err = db.QueryRow(`
SELECT
column1,
uuid_extract_version(column1),
uuid_extract_timestamp(column1)
FROM (VALUES (uuid(7)))
`).Scan(&u, &version, &tstamp)
if err != nil {
t.Fatal(err)
}
if got := u.Version(); got != version {
t.Errorf("got %d, want %d", got, version)
}
if got := time.Unix(u.Time().UnixTime()); !got.Equal(tstamp) {
t.Errorf("got %v, want %v", got, tstamp)
}
tests := []struct {
ver uuid.Version
ns any
data string
u uuid.UUID
}{
{3, "oid", "2.999", uuid.MustParse("31cb1efa-18c4-3d19-89ba-df6a74ddbd1d")},
{3, "dns", "www.example.com", uuid.MustParse("5df41881-3aed-3515-88a7-2f4a814cf09e")},
{3, "fqdn", "www.example.com", uuid.MustParse("5df41881-3aed-3515-88a7-2f4a814cf09e")},
{3, "url", "https://www.example.com/", uuid.MustParse("7fed185f-0864-319f-875b-a3d5458e30ac")},
{3, "x500", "CN=Test User 1, O=Example Organization, ST=California, C=US", uuid.MustParse("addf5e97-9287-3834-abfd-7edcbe7db56f")},
{3, "url", "https://www.php.net", uuid.MustParse("3f703955-aaba-3e70-a3cb-baff6aa3b28f")},
{5, "url", "https://www.php.net", uuid.MustParse("a8f6ae40-d8a7-58f0-be05-a22f94eca9ec")},
}
for _, tt := range tests {
err = db.QueryRow(`SELECT uuid(?, ?, ?)`, tt.ver, tt.ns, tt.data).Scan(&u)
if err != nil {
t.Fatal(err)
}
if u != tt.u {
t.Errorf("got %v, want %v", u, tt.u)
}
}
}
func Test_convert(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var u uuid.UUID
tests := []string{
"'6ba7b8119dad11d180b400c04fd430c8'",
"'6ba7b811-9dad-11d1-80b4-00c04fd430c8'",
"'{6ba7b811-9dad-11d1-80b4-00c04fd430c8}'",
"X'6ba7b8119dad11d180b400c04fd430c8'",
}
for _, tt := range tests {
err = db.QueryRow(`SELECT uuid_str(` + tt + `)`).Scan(&u)
if err != nil {
t.Fatal(err)
}
if u != uuid.NameSpaceURL {
t.Errorf("got %v, want %v", u, uuid.NameSpaceURL)
}
}
for _, tt := range tests {
err = db.QueryRow(`SELECT uuid_blob(` + tt + `)`).Scan(&u)
if err != nil {
t.Fatal(err)
}
if u != uuid.NameSpaceURL {
t.Errorf("got %v, want %v", u, uuid.NameSpaceURL)
}
}
err = db.QueryRow(`SELECT uuid_str(X'cafe')`).Scan(&u)
if err == nil {
t.Fatal("want error")
}
err = db.QueryRow(`SELECT uuid_blob(X'cafe')`).Scan(&u)
if err == nil {
t.Fatal("want error")
}
err = db.QueryRow(`SELECT uuid_extract_version(X'cafe')`).Scan(&u)
if err == nil {
t.Fatal("want error")
}
err = db.QueryRow(`SELECT uuid_extract_timestamp(X'cafe')`).Scan(&u)
if err == nil {
t.Fatal("want error")
}
}

View File

@@ -4,30 +4,33 @@
package zorder
import (
"errors"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Register registers the zorder and unzorder SQL functions.
func Register(db *sqlite3.Conn) {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
db.CreateFunction("zorder", -1, flags, zorder)
db.CreateFunction("unzorder", 3, flags, unzorder)
func Register(db *sqlite3.Conn) error {
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
return errors.Join(
db.CreateFunction("zorder", -1, flags, zorder),
db.CreateFunction("unzorder", 3, flags, unzorder))
}
func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
var x [63]int64
for i := range arg {
x[i] = arg[i].Int64()
}
if len(arg) > len(x) {
ctx.ResultError(util.ErrorString("zorder: too many parameters"))
return
}
for i := range arg {
x[i] = arg[i].Int64()
}
var z int64
if len(arg) > 0 {
for i := 0; i < 63; i++ {
for i := range x {
j := i % len(arg)
z |= (x[j] & 1) << i
x[j] >>= 1
@@ -44,9 +47,9 @@ func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
func unzorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
z := arg[0].Int64()
n := arg[1].Int64()
i := arg[2].Int64()
n := arg[1].Int64()
z := arg[0].Int64()
var k int
var x int64

Some files were not shown because too many files have changed in this diff Show More