Compare commits

...

315 Commits

Author SHA1 Message Date
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
Nuno Cruces
3484bda553 Attestations. 2024-06-21 15:08:33 +01:00
Nuno Cruces
cf0d56271d Integrity. 2024-06-21 13:59:19 +01:00
Nuno Cruces
a465458255 CSV comments. 2024-06-20 11:02:23 +01:00
Nuno Cruces
65af8065cd Fixes. 2024-06-20 00:16:07 +01:00
Nuno Cruces
5c1c0f03a5 Tweaks. 2024-06-19 14:43:44 +01:00
Nuno Cruces
2d168136f1 Cache bind count. 2024-06-19 13:54:58 +01:00
Nuno Cruces
eb8e716253 Fix CI. 2024-06-19 00:43:12 +01:00
Nuno Cruces
3479e8935a Bloom filter virtual table (#103) 2024-06-18 23:42:20 +01:00
Nuno Cruces
58e91052bb CSV type affinity (#102)
Use sqlite-createtable-parser compiled to Wasm to parse the CREATE TABLE statement.
2024-06-17 23:44:37 +01:00
Nuno Cruces
3719692349 Fix potential BSD locking race. (#98) 2024-06-12 20:41:51 +01:00
Nuno Cruces
f7ac77027c wazero v1.7.2. 2024-06-11 23:50:32 +01:00
Nuno Cruces
ef065b6baa More benchmarks. 2024-06-11 10:52:07 +01:00
Nuno Cruces
e7f8311e2e Fix readonly shared memory (see #94). 2024-06-10 00:24:15 +01:00
Nuno Cruces
35a3bfe2f9 Doc fixes. 2024-06-07 12:10:03 +01:00
Nuno Cruces
7386a52b93 Updated dependencies. 2024-06-07 11:06:15 +01:00
Nuno Cruces
34d0289534 Rename to percentile. 2024-06-06 19:55:32 +01:00
Nuno Cruces
dbf764aaf4 Boolean aggregates. 2024-06-06 19:53:22 +01:00
Nuno Cruces
8fd878afd6 Internal API tweaks. 2024-06-06 12:27:27 +01:00
Nuno Cruces
9b769d94d0 BSD WAL fixes. 2024-06-05 23:12:40 +01:00
Nuno Cruces
79c83cdce5 Windows sleep. 2024-06-05 23:12:39 +01:00
Nuno Cruces
e9ed4c103d BSD WAL support. (#90)
Uses in-memory locks.
Also supports illumos.
2024-06-05 00:43:49 +01:00
Nuno Cruces
d78a53a789 Multiple quantiles. 2024-06-02 13:37:29 +01:00
Nuno Cruces
19bc6e3fac Rename. 2024-06-02 12:34:57 +01:00
Nuno Cruces
3955c226cb Rename. 2024-06-02 10:33:20 +01:00
Nuno Cruces
8a3d454935 More tests. 2024-06-02 10:33:06 +01:00
Nuno Cruces
fa7516ce30 Quantiles. 2024-05-31 17:36:16 +01:00
dependabot[bot]
dbf93b2171 Bump lukechampine.com/adiantum from 1.1.0 to 1.1.1 (#89)
Bumps [lukechampine.com/adiantum](https://github.com/lukechampine/adiantum) from 1.1.0 to 1.1.1.
- [Commits](https://github.com/lukechampine/adiantum/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: lukechampine.com/adiantum
  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-05-24 23:55:10 +01:00
Nuno Cruces
f29a999ea7 Updated dependencies. 2024-05-24 12:06:37 +01:00
Nuno Cruces
00d52a873f SQLite 3.46.0. 2024-05-24 11:39:27 +01:00
Nuno Cruces
94fb25e84c Enable sqlite_stat4. 2024-05-24 10:30:14 +01:00
Nuno Cruces
b1f2ff55a0 Remove unnecessary conversions. 2024-05-24 10:30:14 +01:00
Nuno Cruces
53eef1510f Fixed capacity virtual memory. 2024-05-20 14:34:47 +01:00
Nuno Cruces
d23bdcd225 Commiting memory not needed. 2024-05-20 09:41:35 +01:00
Nuno Cruces
321d359663 Ensure benchmarks run. 2024-05-20 01:20:11 +01:00
Nuno Cruces
8f88b687d4 Fix flaky test. 2024-05-20 01:10:13 +01:00
Nuno Cruces
d1075f7dad Fix #87. 2024-05-20 01:04:53 +01:00
Nuno Cruces
ed932ee93b Interrupt busy handlers. 2024-05-19 16:30:09 +01:00
Nuno Cruces
3d30a561f0 Custom allocator. 2024-05-17 17:30:43 +01:00
Nuno Cruces
bdaf77a657 Custom Windows allocator. 2024-05-17 16:57:25 +01:00
Nuno Cruces
323bd6e47e Tweak options early. 2024-05-16 16:24:45 +01:00
Nuno Cruces
5f1c372a65 wazero v1.7.2. 2024-05-13 11:44:34 +01:00
Nuno Cruces
3950be71c1 HPolyC example. 2024-05-12 01:35:40 +01:00
Nuno Cruces
f3dc9bdafc Updated adiantum. 2024-05-10 17:00:35 +01:00
Nuno Cruces
e0720fdb92 Gorm v1.25.10. 2024-05-09 13:24:36 +01:00
Nuno Cruces
5fdcdff7e0 Solaris is flaky. 2024-05-07 17:21:14 +01:00
Nuno Cruces
4d23fc3cee Fix file format. 2024-05-07 16:34:51 +01:00
Nuno Cruces
34882e7c8d Fix z/OS build. 2024-05-07 01:54:13 +01:00
Nuno Cruces
57686a2cf3 Dependencies. 2024-05-06 20:39:37 +01:00
Nuno Cruces
190ca0f0cc NPOT sectors. 2024-05-06 11:57:48 +01:00
Nuno Cruces
1a223fa69f Update README.md 2024-05-06 00:41:56 +01:00
Nuno Cruces
12111a619a Cache LFS. 2024-05-05 23:24:39 +01:00
Nuno Cruces
1c58744f87 Test Solaris. 2024-05-04 15:25:03 +01:00
Nuno Cruces
f0ce3e58eb Docs. 2024-05-04 11:44:54 +01:00
Nuno Cruces
5d5c302ff4 Support for z/OS.
Support is behind sqlite3_flock build tag, and tested through s390x Linux. See #86.
2024-05-04 09:48:50 +01:00
Nuno Cruces
10c494031c EWOULDBLOCK. 2024-05-03 18:09:24 +01:00
Nuno Cruces
d84152dd8d Fix TestTimeFormat. 2024-05-03 18:09:11 +01:00
Nuno Cruces
19209b372c Raise Argon2id iterations. 2024-05-03 14:08:38 +01:00
Nuno Cruces
1e03c6c1fb Add initialize. 2024-05-03 12:39:51 +01:00
Nuno Cruces
bb279cb426 Fixes. 2024-05-02 23:42:31 +01:00
Nuno Cruces
7b646100cb Test endianness. 2024-05-02 23:24:24 +01:00
Nuno Cruces
e0a209908b Enable more tests. 2024-05-02 23:22:59 +01:00
Nuno Cruces
67d859a5b4 Support custom pepper. 2024-05-02 12:09:39 +01:00
Nuno Cruces
57daee7f59 Update README.md 2024-05-01 12:20:33 +01:00
Nuno Cruces
f976ab0dee Additional check. 2024-04-30 20:56:42 +01:00
Nuno Cruces
beba988824 Multiple fixes. 2024-04-30 01:30:39 +01:00
Nuno Cruces
992676d7ec Improved WAL API. 2024-04-27 20:55:14 +01:00
Nuno Cruces
82d8a2d796 Documentation. 2024-04-27 16:31:32 +01:00
Nuno Cruces
811e6e63be Adiantum pragmas. 2024-04-27 12:19:46 +01:00
Nuno Cruces
3c21784aee Simplify URI parameters. 2024-04-27 10:44:00 +01:00
Nuno Cruces
019246d1be Simplify mmap. 2024-04-26 16:45:32 +01:00
Nuno Cruces
fa259bdc94 Simplify _pragma. 2024-04-26 00:07:04 +01:00
Nuno Cruces
8e327a9783 VFS pragma. 2024-04-25 13:30:47 +01:00
Nuno Cruces
09a0ce04ce Test more. (#84)
Also, fix the progress callback and disable a slow example.
2024-04-24 15:49:45 +01:00
Nuno Cruces
fdb2ed0376 Fix illumos. (#83) 2024-04-24 01:07:17 +01:00
Nuno Cruces
3fb0eeec51 Filename API (#82)
Also remove VFSParams.
2024-04-23 11:43:14 +01:00
Nuno Cruces
7f6446ad31 Remove cache (side-channel for shared keys). 2024-04-23 02:25:26 +01:00
Nuno Cruces
77cdf1841f Documentation nits. 2024-04-22 15:28:19 +01:00
kim
189fbc98ac change driver name to SQLite{}, remove global variable 2024-04-22 14:02:45 +01:00
kim
d4027b0133 export the database/sql driver type and global instance 2024-04-22 14:02:45 +01:00
Nuno Cruces
62b79d2ac3 Shared memory API. 2024-04-21 12:33:38 +01:00
Nuno Cruces
07241d064a Adiantum encrypting VFS improvements. (#80)
Encrypt temporary files.
2024-04-21 01:56:38 +01:00
Nuno Cruces
2c30bc996a Don't panic. 2024-04-18 10:12:17 +01:00
Nuno Cruces
9d2194b4ea Update README.md 2024-04-18 02:13:59 +01:00
Nuno Cruces
b3a1cb3dd6 Update README.md 2024-04-18 01:42:25 +01:00
Nuno Cruces
ec1ed22149 Adiantum encrypting VFS. (#77) 2024-04-18 01:39:47 +01:00
Nuno Cruces
e86789b285 Test config. 2024-04-16 17:35:45 +01:00
Nuno Cruces
a1fcafa780 Formatting. 2024-04-16 14:02:23 +01:00
Nuno Cruces
d3f5745790 Updated dependencies. 2024-04-16 02:54:11 +01:00
Nuno Cruces
ec609ea131 F2FS. 2024-04-16 02:52:37 +01:00
Nuno Cruces
7bab8bb949 wazero 1.7.1. 2024-04-16 01:59:29 +01:00
Nuno Cruces
ce97e820d5 wasi-sdk-22. 2024-04-16 01:58:49 +01:00
Nuno Cruces
7d8249efa5 SQLite 3.45.3. 2024-04-16 01:58:48 +01:00
Nuno Cruces
d2362b0311 Use coroutines. 2024-04-16 01:05:24 +01:00
Nuno Cruces
17f1681477 Remove test. 2024-04-15 13:55:12 +01:00
Nuno Cruces
cc0b011e8d Readonly WAL. 2024-04-13 17:11:13 +01:00
dependabot[bot]
46086916d4 Bump cross-platform-actions/action from 0.23.0 to 0.24.0 (#76)
Bumps [cross-platform-actions/action](https://github.com/cross-platform-actions/action) from 0.23.0 to 0.24.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.23.0...v0.24.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-13 00:45:41 +01:00
Nuno Cruces
4322c71a09 Fix race. 2024-04-12 16:28:53 +01:00
Nuno Cruces
da9077cbea Fix repeat runs. 2024-04-12 15:54:48 +01:00
Nuno Cruces
1c3ad12434 WAL and vacuum hooks. 2024-04-12 15:02:01 +01:00
Nuno Cruces
7260962aba Update README.md 2024-04-11 15:18:49 +01:00
Nuno Cruces
e503be641a Refactors. 2024-04-11 12:00:17 +01:00
Nuno Cruces
11c03a16f9 Implement shared memory WAL. (#71)
- enabled by default on 64-bit macOS and Linux (`amd64`/`arm64`)
- depends on merged but unreleased wazero
- may cause small performance regression
- users may need WithMemoryLimitPages if not enough address space available
- needs docs
2024-04-10 13:15:36 +01:00
dependabot[bot]
f1c376cb49 Bump golang.org/x/sync from 0.6.0 to 0.7.0 (#72)
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.6.0 to 0.7.0.
- [Commits](https://github.com/golang/sync/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  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-04-04 23:52:16 +01:00
dependabot[bot]
91fd1457aa Bump golang.org/x/crypto from 0.21.0 to 0.22.0 (#74)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.22.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.22.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-04-04 23:50:02 +01:00
Nuno Cruces
63938d5705 Simplify tests. 2024-04-04 01:25:52 +01:00
Nuno Cruces
10daa594f5 Gorm v1.25.8. 2024-03-28 00:10:07 +00:00
Nuno Cruces
2c2b6835b4 Tweaks, docs. 2024-03-27 07:54:15 +00:00
Nuno Cruces
af7fc3dcb7 Remove deprecations. 2024-03-27 07:54:08 +00:00
Nuno Cruces
0f9ce387b9 Documentation. 2024-03-22 00:21:00 +00:00
Nuno Cruces
b7d22e8fbf Fdatasync. 2024-03-21 15:04:59 +00:00
Nuno Cruces
617982f947 F2FS atomic writes. (#66)
https://sqlite.org/cgi/src/technote/714f6cbbf7
2024-03-21 13:59:47 +00:00
Nuno Cruces
36583542e1 Updated dependencies. 2024-03-17 16:34:02 +00:00
Nuno Cruces
fd3a3a3499 Locking improvements. 2024-03-17 16:17:48 +00:00
dependabot[bot]
ec96c77715 Bump github.com/tetratelabs/wazero from 1.7.0-pre.1 to 1.7.0 (#65)
Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.7.0-pre.1 to 1.7.0.
- [Release notes](https://github.com/tetratelabs/wazero/releases)
- [Commits](https://github.com/tetratelabs/wazero/compare/v1.7.0-pre.1...v1.7.0)

---
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-03-15 23:23:02 +00:00
Nuno Cruces
c61f7b90f6 Locking improvements. 2024-03-15 16:14:49 +00:00
Nuno Cruces
7bd31c3443 Use strchrnul. 2024-03-15 14:15:12 +00:00
Nuno Cruces
f2f698b78a Remove clear. 2024-03-15 14:13:00 +00:00
Nuno Cruces
846b95d2d4 Persistent WAL. 2024-03-14 14:31:16 +00:00
Nuno Cruces
b9453aefb6 SQLite 3.45.2.
Also, remove FTS3/4.
2024-03-12 14:24:11 +00:00
Nuno Cruces
28b6fedef0 wazero v1.7.0-pre.1. (#60)
This enables the wazevo next gen compiler.
2024-03-09 10:11:50 +00:00
Nuno Cruces
fed9ce6e1c Backport date from 3.46. 2024-03-07 03:04:28 -08:00
dependabot[bot]
0ec08c2e74 Bump golang.org/x/crypto from 0.20.0 to 0.21.0 (#63)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.20.0 to 0.21.0.
- [Commits](https://github.com/golang/crypto/compare/v0.20.0...v0.21.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-03-04 23:10:29 +00:00
Nuno Cruces
4439cd302c binaryen-version_117. 2024-02-28 12:48:05 +00:00
dependabot[bot]
705eab456a Bump golang.org/x/crypto from 0.19.0 to 0.20.0 (#62)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/crypto/compare/v0.19.0...v0.20.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-02-26 23:04:54 +00:00
Nuno Cruces
d1d5e355c4 CI testing. 2024-02-24 00:22:40 +00:00
Nuno Cruces
d3da8cc4f3 BSD. 2024-02-23 23:56:54 +00:00
Nuno Cruces
6def6f735c Warn about Git LFS. 2024-02-20 10:08:42 +00:00
Nuno Cruces
e02c5b5db0 BSD. 2024-02-19 00:19:36 +00:00
Nuno Cruces
52d42e4b21 Naming. 2024-02-10 10:09:53 +00:00
Nuno Cruces
396e6537b4 GORM v1.25.7. (#59) 2024-02-10 10:03:12 +00:00
Nuno Cruces
78cb9abefd Lebesgue/Morton order. 2024-02-08 00:39:39 +00:00
dependabot[bot]
b76cb33e62 Bump golang.org/x/crypto from 0.18.0 to 0.19.0 (#57)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/crypto/compare/v0.18.0...v0.19.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-02-07 23:21:55 +00:00
Nuno Cruces
c7eea620a3 Tweaks. 2024-02-05 12:47:55 +00:00
Nuno Cruces
76e2733fef Updated dependencies. 2024-02-03 02:12:35 +00:00
282 changed files with 12863 additions and 2750 deletions

23
.github/actions/lfs/action.yml vendored Normal file
View File

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

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

@@ -0,0 +1,16 @@
name: VM Actions matrix
description: VM Actions matrix template
inputs:
run:
description: The CI command to run
required: true
runs:
using: composite
steps:
- uses: ${VMACTIONS}
with:
usesh: true
copyback: false
run: ${{inputs.run}}

View File

@@ -1,10 +0,0 @@
#!/usr/bin/env bash
echo 'set -euo pipefail' > test.sh
for p in $(go list ./...); do
dir=".${p#github.com/ncruces/go-sqlite3}"
name="$(basename "$p").test"
(cd ${dir}; GOOS=freebsd go test -c)
[ -f "${dir}/${name}" ] && echo "(cd ${dir}; ./${name} -test.v)" >> test.sh
done

View File

@@ -1,31 +0,0 @@
name: BSD
on:
workflow_dispatch:
jobs:
test:
runs-on: macos-12
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up
uses: actions/setup-go@v5
with:
go-version: stable
- name: Build
run: .github/workflows/bsd.sh
- name: Test
uses: cross-platform-actions/action@v0.22.0
with:
operating_system: freebsd
version: '13.2'
memory: 8G
shell: bash
sync_files: runner-to-vm
run: source test.sh

15
.github/workflows/build-test.sh vendored Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
echo 'set -eu' > test.sh
for p in $(go list ./...); do
dir=".${p#github.com/ncruces/go-sqlite3}"
name="$(basename "$p").test"
(cd ${dir}; go test -c ${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,56 +0,0 @@
name: CPUs
on:
workflow_dispatch:
jobs:
test-386:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up
uses: actions/setup-go@v5
with:
go-version: stable
- name: Test
run: GOARCH=386 go test -v -short ./...
test-arm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up
uses: actions/setup-go@v5
with:
go-version: stable
- name: Install QEMU
uses: docker/setup-qemu-action@v3
- name: Test
run: GOARCH=arm64 go test -v -short ./...
test-m1:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up
uses: actions/setup-go@v5
with:
go-version: stable
- name: Test
run: go test -v ./...

View File

@@ -16,8 +16,10 @@ 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-dotlk ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_dotlk .
echo darwin-flock ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_flock .
echo darwin-nosys ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_nosys .
echo linux-nosys ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_nosys .
echo windows-nosys ; GOOS=windows GOARCH=amd64 go build -tags sqlite3_nosys .
echo freebsd-nosys ; GOOS=freebsd GOARCH=amd64 go build -tags sqlite3_nosys .
echo darwin-dotlk ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_dotlk .
echo windows-dotlk ; GOOS=windows GOARCH=amd64 go build -tags sqlite3_dotlk .
echo freebsd-dotlk ; GOOS=freebsd GOARCH=amd64 go build -tags sqlite3_dotlk .
echo solaris-dotlk ; GOOS=solaris GOARCH=amd64 go build -tags sqlite3_dotlk .

View File

@@ -9,11 +9,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up
uses: actions/setup-go@v5
with:
go-version: stable
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Build
run: .github/workflows/cross.sh
run: .github/workflows/cross.sh

View File

@@ -1,63 +0,0 @@
name: Go
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:
jobs:
test:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up
uses: actions/setup-go@v5
with:
go-version: stable
- name: Format
run: gofmt -s -w . && git diff --exit-code
if: matrix.os != 'windows-latest'
- name: Tidy
run: go mod tidy && git diff --exit-code
- name: Download
run: go mod download
- name: Verify
run: go mod verify
- name: Vet
run: go vet ./...
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
- name: Test no locks
run: go test -v -tags sqlite3_nosys ./tests -run TestDB_nolock
- name: Test BSD locks
run: go test -v -tags sqlite3_flock ./...
if: matrix.os == 'macos-latest'
- name: Coverage report
uses: ncruces/go-coverage-report@v0
with:
chart: true
amend: true
if: |
github.event_name == 'push' &&
matrix.os == 'ubuntu-latest'

View File

@@ -2,22 +2,33 @@
set -euo pipefail
if [[ "$OSTYPE" == "linux"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/wasi-sdk-21.0-linux.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-linux.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-linux.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_120/binaryen-version_120-x86_64-linux.tar.gz"
elif [[ "$OSTYPE" == "darwin"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/wasi-sdk-21.0-macos.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-macos.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-arm64-macos.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_120/binaryen-version_120-arm64-macos.tar.gz"
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/wasi-sdk-21.0.m-mingw.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-windows.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-windows.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_120/binaryen-version_120-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 &
mkdir -p tools/
[ -d "tools/wasi-sdk" ] || curl -#L "$WASI_SDK" | tar xzC tools &
[ -d "tools/binaryen" ] || curl -#L "$BINARYEN" | tar xzC tools &
wait
sqlite3/download.sh # Download SQLite
embed/build.sh # Build WASM
git diff --exit-code # Check diffs
[ -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/sql3util/parse/download.sh
util/sql3util/parse/build.sh
# Check diffs
git diff --exit-code

View File

@@ -3,6 +3,11 @@ name: Reproducible build
on:
workflow_dispatch:
permissions:
contents: read
id-token: write
attestations: write
jobs:
build:
strategy:
@@ -11,14 +16,19 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: ilammy/msvc-dev-cmd@v1
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up
uses: actions/setup-go@v5
with:
go-version: stable
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Build
shell: bash
run: .github/workflows/repro.sh
- uses: actions/attest-build-provenance@v2
if: matrix.os == 'ubuntu-latest'
with:
subject-path: |
embed/sqlite3.wasm
embed/bcw2/bcw2.wasm
util/sql3util/parse/sql3parse_table.wasm

200
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,200 @@
name: Test
on:
push:
branches: [ "main" ]
paths:
- '**.go'
- '**.mod'
- '**.wasm'
- '**.wasm.bz2'
pull_request:
branches: [ "main" ]
paths:
- '**.go'
- '**.mod'
- '**.wasm'
- '**.wasm.bz2'
workflow_dispatch:
jobs:
test:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Git LFS pull
uses: ./.github/actions/lfs
- name: Format
run: gofmt -s -w . && git diff --exit-code
if: matrix.os != 'windows-latest'
- name: Tidy
run: go mod tidy && git diff --exit-code
- name: Download
run: go mod download
- name: Verify
run: go mod verify
- name: Vet
run: go vet ./...
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./... -bench . -benchtime=1x
- name: Test BSD locks
run: go test -v -tags sqlite3_flock ./...
if: matrix.os == 'macos-latest'
- name: Test dot locks
run: go test -v -tags sqlite3_dotlk ./...
if: matrix.os != 'windows-latest'
- name: Test GORM
shell: bash
run: gormlite/test.sh
- name: Collect coverage
run: go run github.com/dave/courtney@latest
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-bsd:
strategy:
matrix:
os:
- name: freebsd
version: '14.2'
flags: '-test.v'
- name: netbsd
version: '10.0'
flags: '-test.v'
- name: openbsd
version: '7.6'
flags: '-test.v -test.short'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Git LFS pull
uses: ./.github/actions/lfs
- name: Build
env:
GOOS: ${{ matrix.os.name }}
TESTFLAGS: ${{ matrix.os.flags }}
run: .github/workflows/build-test.sh
- name: Test
uses: cross-platform-actions/action@v0.26.0
with:
operating_system: ${{ matrix.os.name }}
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: Git LFS pull
uses: ./.github/actions/lfs
- name: Build
env:
GOOS: ${{ matrix.os.name }}
BUILDFLAGS: ${{ matrix.os.bflags }}
TESTFLAGS: ${{ matrix.os.tflags }}
VMACTIONS: ${{ matrix.os.action }}
run: .github/workflows/build-test.sh
- name: Test
uses: ./.github/actions/vmactions
with:
usesh: true
copyback: false
run: . ./test.sh
test-qemu:
runs-on: ubuntu-latest
needs: test
steps:
- uses: docker/setup-qemu-action@v3
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Git LFS pull
uses: ./.github/actions/lfs
- name: Test 386 (32-bit)
run: GOARCH=386 go test -v -short ./...
- name: Test arm64 (compiler)
run: GOARCH=arm64 go test -v -short ./...
- name: Test riscv64 (interpreter)
run: GOARCH=riscv64 go test -v -short ./...
- name: Test ppc64le (interpreter)
run: GOARCH=ppc64le go test -v -short ./...
- name: Test s390x (big-endian)
run: GOARCH=s390x go test -v -short -tags sqlite3_dotlk ./...
test-macintel:
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 ./...

124
README.md
View File

@@ -1,15 +1,30 @@
# 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)
[![Go Coverage](https://github.com/ncruces/go-sqlite3/wiki/coverage.svg)](https://github.com/ncruces/go-sqlite3/wiki/Test-coverage-report)
Go module `github.com/ncruces/go-sqlite3` is `cgo`-free [SQLite](https://sqlite.org/) wrapper.\
Go module `github.com/ncruces/go-sqlite3` is a `cgo`-free [SQLite](https://sqlite.org/) wrapper.\
It provides a [`database/sql`](https://pkg.go.dev/database/sql) compatible driver,
as well as direct access to most of the [C SQLite API](https://sqlite.org/cintro.html).
It wraps a [WASM](https://webassembly.org/) build of SQLite, and uses [wazero](https://wazero.io/) as the runtime.\
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ runtime dependencies.
It wraps a [Wasm](https://webassembly.org/) [build](embed/) of SQLite,
and uses [wazero](https://wazero.io/) as the runtime.\
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ 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
@@ -26,33 +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/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/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)
@@ -65,55 +53,20 @@ 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
This module replaces the SQLite [OS Interface](https://sqlite.org/vfs.html)
(aka VFS) with a [pure Go](vfs/) implementation.
This has benefits, but also comes with some drawbacks.
(aka VFS) with a [pure Go](vfs/) implementation,
which has advantages and disadvantages.
#### Write-Ahead Logging
Because WASM does not support shared memory,
[WAL](https://sqlite.org/wal.html) support is [limited](https://sqlite.org/wal.html#noshm).
To work around this limitation, SQLite is [patched](sqlite3/locking_mode.patch)
to always use `EXCLUSIVE` locking mode for WAL databases.
Because connection pooling is incompatible with `EXCLUSIVE` locking mode,
to use the [`database/sql`](https://pkg.go.dev/database/sql) driver
with WAL mode databases you should disable connection pooling by calling
[`db.SetMaxOpenConns(1)`](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns).
#### File Locking
POSIX advisory locks, which SQLite uses on Unix, are
[broken by design](https://sqlite.org/src/artifact/2e8b12?ln=1073-1161).
On Linux, macOS and illumos, this module uses
[OFD locks](https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html)
to synchronize access to database files.
OFD locks are fully compatible with POSIX advisory locks.
On BSD Unixes, this module uses
[BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2).
On BSD Unixes, BSD locks are fully compatible with POSIX advisory locks.
On Windows, this module uses `LockFileEx` and `UnlockFileEx`,
like SQLite.
On all other platforms, file locking is not supported, and you must use
[`nolock=1`](https://sqlite.org/uri.html#urinolock)
(or [`immutable=1`](https://sqlite.org/uri.html#uriimmutable))
to open database files.
To use the [`database/sql`](https://pkg.go.dev/database/sql) driver
with `nolock=1` you must disable connection pooling by calling
[`db.SetMaxOpenConns(1)`](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns).
You can use [`vfs.SupportsFileLocking`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs#SupportsFileLocking)
to check if your platform supports file locking.
Read more about the Go VFS design [here](vfs/README.md).
### Testing
@@ -121,18 +74,33 @@ This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
The pure Go VFS is tested by running SQLite's
[mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c)
on Linux, macOS, Windows and FreeBSD.
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Test-matrix) on
Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64),
Windows (amd64), FreeBSD (amd64), OpenBSD (amd64), NetBSD (amd64),
DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64).
The Go VFS is tested by running SQLite's
[mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c).
### Performance
Perfomance of the [`database/sql`](https://pkg.go.dev/database/sql) driver is
[competitive](https://github.com/cvilsmeier/go-sqlite-bench) with alternatives.
The WASM and VFS layers are also tested by running SQLite's
The Wasm and VFS layers are also tested by running SQLite's
[speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c).
### 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)

53
blob.go
View File

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

298
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,8 +18,19 @@ import (
//
// https://sqlite.org/c3ref/db_config.html
func (c *Conn) Config(op DBConfig, arg ...bool) (bool, error) {
if op < DBCONFIG_ENABLE_FKEY || op > DBCONFIG_REVERSE_SCANORDER {
return false, MISUSE
}
// We need to call sqlite3_db_config, a variadic function.
// We only support the `int int*` variants.
// The int is a three-valued bool: -1 queries, 0/1 sets false/true.
// The int* points to where new state will be written to.
// The vararg is a pointer to an array containing these arguments:
// an int and an int* pointing to that int.
defer c.arena.mark()()
argsPtr := c.arena.new(2 * ptrlen)
argsPtr := c.arena.new(intlen + ptrlen)
var flag int
switch {
@@ -56,6 +71,112 @@ func logCallback(ctx context.Context, mod api.Module, _, iCode, zMsg uint32) {
}
}
// Log writes a message into the error log established by [Conn.ConfigLog].
//
// https://sqlite.org/c3ref/log.html
func (c *Conn) Log(code ExtendedErrorCode, format string, a ...any) {
if c.log != nil {
c.log(code, fmt.Sprintf(format, a...))
}
}
// FileControl allows low-level control of database files.
// Only a subset of opcodes are supported.
//
// https://sqlite.org/c3ref/file_control.html
func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, error) {
defer c.arena.mark()()
ptr := c.arena.new(max(ptrlen, intlen))
var schemaPtr uint32
if schema != "" {
schemaPtr = c.arena.string(schema)
}
var rc uint64
var res any
switch op {
default:
return nil, MISUSE
case FCNTL_RESET_CACHE:
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), 0)
case FCNTL_PERSIST_WAL, FCNTL_POWERSAFE_OVERWRITE:
var flag int
switch {
case len(arg) == 0:
flag = -1
case arg[0]:
flag = 1
}
util.WriteUint32(c.mod, ptr, uint32(flag))
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
res = util.ReadUint32(c.mod, ptr) != 0
case FCNTL_CHUNK_SIZE:
util.WriteUint32(c.mod, ptr, uint32(arg[0].(int)))
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
case FCNTL_RESERVE_BYTES:
bytes := -1
if len(arg) > 0 {
bytes = arg[0].(int)
}
util.WriteUint32(c.mod, ptr, uint32(bytes))
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
res = int(util.ReadUint32(c.mod, ptr))
case FCNTL_DATA_VERSION:
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
res = util.ReadUint32(c.mod, ptr)
case FCNTL_LOCKSTATE:
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
res = vfs.LockLevel(util.ReadUint32(c.mod, ptr))
case FCNTL_VFS_POINTER:
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
if rc == _OK {
const zNameOffset = 16
ptr = util.ReadUint32(c.mod, ptr)
ptr = util.ReadUint32(c.mod, ptr+zNameOffset)
name := util.ReadString(c.mod, ptr, _MAX_NAME)
res = vfs.Find(name)
}
case FCNTL_FILE_POINTER, FCNTL_JOURNAL_POINTER:
rc = c.call("sqlite3_file_control",
uint64(c.handle), uint64(schemaPtr),
uint64(op), uint64(ptr))
if rc == _OK {
const fileHandleOffset = 4
ptr = util.ReadUint32(c.mod, ptr)
ptr = util.ReadUint32(c.mod, ptr+fileHandleOffset)
res = util.GetHandle(c.ctx, ptr)
}
}
if err := c.error(rc); err != nil {
return nil, err
}
return res, nil
}
// Limit allows the size of various constructs to be
// limited on a connection by connection basis.
//
@@ -68,7 +189,7 @@ func (c *Conn) Limit(id LimitCategory, value int) int {
// 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 {
func (c *Conn) SetAuthorizer(cb func(action AuthorizerActionCode, name3rd, name4th, schema, inner string) AuthorizerReturnCode) error {
var enable uint64
if cb != nil {
enable = 1
@@ -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) AuthorizerReturnCode {
func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action AuthorizerActionCode, zName3rd, zName4th, zSchema, zInner uint32) (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,10 +215,169 @@ 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)
}
return c.authorizer(action, name3rd, name4th, schema, nameInner)
rc = c.authorizer(action, name3rd, name4th, schema, inner)
}
return AUTH_OK
return rc
}
// 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 {
r := c.call("sqlite3_trace_go", uint64(c.handle), uint64(mask))
if err := c.error(r); err != nil {
return err
}
c.trace = cb
return nil
}
func traceCallback(ctx context.Context, mod api.Module, evt TraceEvent, pDB, pArg1, pArg2 uint32) (rc uint32) {
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 = int64(util.ReadUint64(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) {
defer c.arena.mark()()
nLogPtr := c.arena.new(ptrlen)
nCkptPtr := c.arena.new(ptrlen)
schemaPtr := c.arena.string(schema)
r := c.call("sqlite3_wal_checkpoint_v2",
uint64(c.handle), uint64(schemaPtr), uint64(mode),
uint64(nLogPtr), uint64(nCkptPtr))
nLog = int(int32(util.ReadUint32(c.mod, nLogPtr)))
nCkpt = int(int32(util.ReadUint32(c.mod, nCkptPtr)))
return nLog, nCkpt, c.error(r)
}
// WALAutoCheckpoint configures WAL auto-checkpoints.
//
// https://sqlite.org/c3ref/wal_autocheckpoint.html
func (c *Conn) WALAutoCheckpoint(pages int) error {
r := c.call("sqlite3_wal_autocheckpoint", uint64(c.handle), uint64(pages))
return c.error(r)
}
// WALHook registers a callback function to be invoked
// each time data is committed to a database in WAL mode.
//
// https://sqlite.org/c3ref/wal_hook.html
func (c *Conn) WALHook(cb func(db *Conn, schema string, pages int) error) {
var enable uint64
if cb != nil {
enable = 1
}
c.call("sqlite3_wal_hook_go", uint64(c.handle), enable)
c.wal = cb
}
func walCallback(ctx context.Context, mod api.Module, _, pDB, zSchema uint32, pages int32) (rc uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.wal != nil {
schema := util.ReadString(mod, zSchema, _MAX_NAME)
err := c.wal(c, schema, int(pages))
_, rc = errorCode(err, ERROR)
}
return rc
}
// AutoVacuumPages registers a autovacuum compaction amount callback.
//
// https://sqlite.org/c3ref/autovacuum_pages.html
func (c *Conn) AutoVacuumPages(cb func(schema string, dbPages, freePages, bytesPerPage uint) uint) error {
var funcPtr uint32
if cb != nil {
funcPtr = util.AddHandle(c.ctx, cb)
}
r := c.call("sqlite3_autovacuum_pages_go", uint64(c.handle), uint64(funcPtr))
return c.error(r)
}
func autoVacuumCallback(ctx context.Context, mod api.Module, pApp, zSchema, nDbPage, nFreePage, nBytePerPage uint32) uint32 {
fn := util.GetHandle(ctx, pApp).(func(schema string, dbPages, freePages, bytesPerPage uint) uint)
schema := util.ReadString(mod, zSchema, _MAX_NAME)
return uint32(fn(schema, uint(nDbPage), uint(nFreePage), uint(nBytePerPage)))
}
// SoftHeapLimit imposes a soft limit on heap size.
//
// https://sqlite.org/c3ref/hard_heap_limit64.html
func (c *Conn) SoftHeapLimit(n int64) int64 {
return int64(c.call("sqlite3_soft_heap_limit64", uint64(n)))
}
// HardHeapLimit imposes a hard limit on heap size.
//
// https://sqlite.org/c3ref/hard_heap_limit64.html
func (c *Conn) HardHeapLimit(n int64) int64 {
return int64(c.call("sqlite3_hard_heap_limit64", uint64(n)))
}
// EnableChecksums enables checksums on a database.
//
// https://sqlite.org/cksumvfs.html
func (c *Conn) EnableChecksums(schema string) error {
r, err := c.FileControl(schema, FCNTL_RESERVE_BYTES)
if err != nil {
return err
}
if r == 8 {
// Correct value, enabled.
return nil
}
if r == 0 {
// Default value, enable.
_, err = c.FileControl(schema, FCNTL_RESERVE_BYTES, 8)
if err != nil {
return err
}
r, err = c.FileControl(schema, FCNTL_RESERVE_BYTES)
if err != nil {
return err
}
}
if r != 8 {
// Invalid value.
return util.ErrorString("sqlite3: reserve bytes must be 8, is: " + strconv.Itoa(r.(int)))
}
// VACUUM the database.
if schema != "" {
err = c.Exec(`VACUUM ` + QuoteIdentifier(schema))
} else {
err = c.Exec(`VACUUM`)
}
if err != nil {
return err
}
// Checkpoint the WAL.
_, _, err = c.WALCheckpoint(schema, CHECKPOINT_RESTART)
return err
}

258
conn.go
View File

@@ -2,15 +2,16 @@ package sqlite3
import (
"context"
"errors"
"fmt"
"math"
"net/url"
"strings"
"time"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/vfs"
)
// Conn is a database connection handle.
@@ -22,9 +23,13 @@ type Conn struct {
interrupt context.Context
pending *Stmt
busy func(int) bool
stmts []*Stmt
timer *time.Timer
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
@@ -34,14 +39,20 @@ type Conn struct {
handle uint32
}
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE], [OPEN_URI] and [OPEN_NOFOLLOW].
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE] and [OPEN_URI].
func Open(filename string) (*Conn, error) {
return newConn(filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI|OPEN_NOFOLLOW)
return newConn(context.Background(), filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
}
// OpenContext is like [Open] but includes a context,
// which is used to interrupt the process of opening the connectiton.
func OpenContext(ctx context.Context, filename string) (*Conn, error) {
return newConn(ctx, filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
}
// OpenFlags opens an SQLite database file as specified by the filename argument.
//
// If none of the required flags is used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
// If none of the required flags are used, a combination of [OPEN_READWRITE] and [OPEN_CREATE] is used.
// If a URI filename is used, PRAGMA statements to execute can be specified using "_pragma":
//
// sqlite3.Open("file:demo.db?_pragma=busy_timeout(10000)")
@@ -51,26 +62,37 @@ func OpenFlags(filename string, flags OpenFlag) (*Conn, error) {
if flags&(OPEN_READONLY|OPEN_READWRITE|OPEN_CREATE) == 0 {
flags |= OPEN_READWRITE | OPEN_CREATE
}
return newConn(filename, flags)
return newConn(context.Background(), filename, flags)
}
type connKey struct{}
func newConn(filename string, flags OpenFlag) (conn *Conn, err error) {
sqlite, err := instantiateSQLite()
func newConn(ctx context.Context, filename string, flags OpenFlag) (res *Conn, _ error) {
err := ctx.Err()
if err != nil {
return nil, err
}
c := &Conn{interrupt: ctx}
c.sqlite, err = instantiateSQLite()
if err != nil {
return nil, err
}
defer func() {
if conn == nil {
sqlite.close()
if res == nil {
c.Close()
c.sqlite.close()
} else {
c.interrupt = context.Background()
}
}()
c := &Conn{sqlite: sqlite}
c.arena = c.newArena(1024)
c.ctx = context.WithValue(c.ctx, connKey{}, c)
c.arena = c.newArena(1024)
c.handle, err = c.openDB(filename, flags)
if err == nil {
err = initExtensions(c)
}
if err != nil {
return nil, err
}
@@ -91,6 +113,7 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
return 0, err
}
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
if flags|OPEN_URI != 0 && strings.HasPrefix(filename, "file:") {
var pragmas strings.Builder
if _, after, ok := strings.Cut(filename, "?"); ok {
@@ -101,18 +124,17 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
pragmas.WriteString(`;`)
}
}
pragmaPtr := c.arena.string(pragmas.String())
r := c.call("sqlite3_exec", uint64(handle), uint64(pragmaPtr), 0, 0, 0)
if err := c.sqlite.error(r, handle, pragmas.String()); err != nil {
if errors.Is(err, ERROR) {
if pragmas.Len() != 0 {
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 {
err = fmt.Errorf("sqlite3: invalid _pragma: %w", err)
c.closeDB(handle)
return 0, err
}
c.closeDB(handle)
return 0, err
}
}
c.call("sqlite3_progress_handler_go", uint64(handle), 100)
return handle, nil
}
@@ -154,10 +176,10 @@ func (c *Conn) Close() error {
//
// https://sqlite.org/c3ref/exec.html
func (c *Conn) Exec(sql string) error {
c.checkInterrupt()
defer c.arena.mark()()
sqlPtr := c.arena.string(sql)
c.checkInterrupt(c.handle)
r := c.call("sqlite3_exec", uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
return c.error(r, sql)
}
@@ -174,7 +196,7 @@ func (c *Conn) Prepare(sql string) (stmt *Stmt, tail string, err error) {
//
// https://sqlite.org/c3ref/prepare.html
func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail string, err error) {
if len(sql) > _MAX_LENGTH {
if len(sql) > _MAX_SQL_LENGTH {
return nil, "", TOOBIG
}
@@ -183,6 +205,7 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
tailPtr := c.arena.new(ptrlen)
sqlPtr := c.arena.string(sql)
c.checkInterrupt(c.handle)
r := c.call("sqlite3_prepare_v3", uint64(c.handle),
uint64(sqlPtr), uint64(len(sql)+1), uint64(flags),
uint64(stmtPtr), uint64(tailPtr))
@@ -199,6 +222,7 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
if stmt.handle == 0 {
return nil, "", nil
}
c.stmts = append(c.stmts, stmt)
return stmt, tail, nil
}
@@ -215,6 +239,19 @@ func (c *Conn) DBName(n int) string {
return util.ReadString(c.mod, ptr, _MAX_NAME)
}
// Filename returns the filename for a database.
//
// https://sqlite.org/c3ref/db_filename.html
func (c *Conn) Filename(schema string) *vfs.Filename {
var ptr uint32
if schema != "" {
defer c.arena.mark()()
ptr = c.arena.string(schema)
}
r := c.call("sqlite3_db_filename", uint64(c.handle), uint64(ptr))
return vfs.GetFilename(c.ctx, c.mod, uint32(r), vfs.OPEN_MAIN_DB)
}
// ReadOnly determines if a database is read-only.
//
// https://sqlite.org/c3ref/db_readonly.html
@@ -281,6 +318,11 @@ func (c *Conn) ReleaseMemory() error {
return c.error(r)
}
// GetInterrupt gets the context set with [Conn.SetInterrupt].
func (c *Conn) GetInterrupt() context.Context {
return c.interrupt
}
// SetInterrupt interrupts a long-running query when a context is done.
//
// Subsequent uses of the connection will return [INTERRUPT]
@@ -296,42 +338,45 @@ func (c *Conn) ReleaseMemory() error {
//
// 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", uint64(c.handle), uint64(loopPtr), math.MaxUint64,
uint64(PREPARE_PERSISTENT), uint64(stmtPtr), 0)
c.pending = &Stmt{c: c}
c.pending.handle = util.ReadUint32(c.mod, stmtPtr)
}
old = c.interrupt
c.interrupt = ctx
if old != nil && old.Done() != nil && (ctx == nil || ctx.Err() == nil) {
if old.Done() != nil && ctx.Err() == nil {
c.pending.Reset()
}
if ctx != nil && ctx.Done() != nil {
if ctx.Done() != nil {
c.pending.Step()
}
return old
}
func (c *Conn) checkInterrupt() {
if c.interrupt != nil && c.interrupt.Err() != nil {
c.call("sqlite3_interrupt", uint64(c.handle))
func (c *Conn) checkInterrupt(handle uint32) {
if c.interrupt.Err() != nil {
c.call("sqlite3_interrupt", uint64(handle))
}
}
func progressCallback(ctx context.Context, mod api.Module, pDB uint32) uint32 {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.commit != nil {
if c.interrupt != nil && c.interrupt.Err() != nil {
return 1
}
func progressCallback(ctx context.Context, mod api.Module, _ uint32) (interrupt uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.interrupt.Err() != nil {
interrupt = 1
}
return 0
return interrupt
}
// BusyTimeout sets a busy timeout.
@@ -343,10 +388,47 @@ func (c *Conn) BusyTimeout(timeout time.Duration) error {
return c.error(r)
}
func timeoutCallback(ctx context.Context, mod api.Module, count, tmout int32) (retry uint32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.interrupt.Err() == nil {
const delays = "\x01\x02\x05\x0a\x0f\x14\x19\x19\x19\x32\x32\x64"
const totals = "\x00\x01\x03\x08\x12\x21\x35\x4e\x67\x80\xb2\xe4"
const ndelay = int32(len(delays) - 1)
var delay, prior int32
if count <= ndelay {
delay = int32(delays[count])
prior = int32(totals[count])
} else {
delay = int32(delays[ndelay])
prior = int32(totals[ndelay]) + delay*(count-ndelay)
}
if delay = min(delay, tmout-prior); delay > 0 {
delay := time.Duration(delay) * time.Millisecond
if c.interrupt.Done() == nil {
time.Sleep(delay)
return 1
}
if c.timer == nil {
c.timer = time.NewTimer(delay)
} else {
c.timer.Reset(delay)
}
select {
case <-c.interrupt.Done():
c.timer.Stop()
case <-c.timer.C:
return 1
}
}
}
return 0
}
// BusyHandler registers a callback to handle [BUSY] errors.
//
// https://sqlite.org/c3ref/busy_handler.html
func (c *Conn) BusyHandler(cb func(count int) (retry bool)) error {
func (c *Conn) BusyHandler(cb func(ctx context.Context, count int) (retry bool)) error {
var enable uint64
if cb != nil {
enable = 1
@@ -359,39 +441,87 @@ func (c *Conn) BusyHandler(cb func(count int) (retry bool)) error {
return nil
}
func busyCallback(ctx context.Context, mod api.Module, pDB, count uint32) uint32 {
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 {
if retry := c.busy(int(count)); retry {
return 1
interrupt := c.interrupt
if interrupt == nil {
interrupt = context.Background()
}
if interrupt.Err() == nil && c.busy(interrupt, int(count)) {
retry = 1
}
}
return 0
return retry
}
// Deprecated: executes a PRAGMA statement and returns results.
func (c *Conn) Pragma(str string) ([]string, error) {
stmt, _, err := c.Prepare(`PRAGMA ` + str)
if err != nil {
return nil, err
}
defer stmt.Close()
// 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 pragmas []string
for stmt.Step() {
pragmas = append(pragmas, stmt.ColumnText(0))
var i uint64
if reset {
i = 1
}
return pragmas, stmt.Close()
r := c.call("sqlite3_db_status", uint64(c.handle),
uint64(op), uint64(curPtr), uint64(hiPtr), i)
if err = c.error(r); err == nil {
current = int(util.ReadUint32(c.mod, curPtr))
highwater = int(util.ReadUint32(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 uint32
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)
}
r := c.call("sqlite3_table_column_metadata", uint64(c.handle),
uint64(schemaPtr), uint64(tablePtr), uint64(columnPtr),
uint64(declTypePtr), uint64(collSeqPtr),
uint64(notNullPtr), uint64(primaryKeyPtr), uint64(autoIncPtr))
if err = c.error(r); err == nil && column != "" {
if ptr := util.ReadUint32(c.mod, declTypePtr); ptr != 0 {
declType = util.ReadString(c.mod, ptr, _MAX_NAME)
}
if ptr := util.ReadUint32(c.mod, collSeqPtr); ptr != 0 {
collSeq = util.ReadString(c.mod, ptr, _MAX_NAME)
}
notNull = util.ReadUint32(c.mod, notNullPtr) != 0
autoInc = util.ReadUint32(c.mod, autoIncPtr) != 0
primaryKey = util.ReadUint32(c.mod, primaryKeyPtr) != 0
}
return
}
func (c *Conn) error(rc uint64, 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 }

159
const.go
View File

@@ -7,15 +7,13 @@ const (
_ROW = 100 /* sqlite3_step() has another row ready */
_DONE = 101 /* sqlite3_step() has finished executing */
_UTF8 = 1
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
_MAX_LENGTH = 1e9
_MAX_SQL_LENGTH = 1e9
_MAX_ALLOCATION_SIZE = 0x7ffffeff
_MAX_FUNCTION_ARG = 100
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
_MAX_LENGTH = 1e9
_MAX_SQL_LENGTH = 1e9
_MAX_FUNCTION_ARG = 100
ptrlen = 4
intlen = 4
)
// ErrorCode is a result code that [Error.Code] might return.
@@ -109,7 +107,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)
@@ -177,11 +175,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 +200,27 @@ 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
)
// DBConfig are the available database connection configuration options.
//
// https://sqlite.org/c3ref/c_dbconfig_defensive.html
@@ -227,6 +247,25 @@ const (
DBCONFIG_TRUSTED_SCHEMA DBConfig = 1017
DBCONFIG_STMT_SCANSTATUS DBConfig = 1018
DBCONFIG_REVERSE_SCANORDER DBConfig = 1019
// DBCONFIG_MAX DBConfig = 1019
)
// FcntlOpcode are the available opcodes for [Conn.FileControl].
//
// 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.
@@ -256,41 +295,41 @@ const (
type AuthorizerActionCode uint32
const (
/************************************************ 3rd ************ 4th ***********/
CREATE_INDEX AuthorizerActionCode = 1 /* Index Name Table Name */
CREATE_TABLE AuthorizerActionCode = 2 /* Table Name NULL */
CREATE_TEMP_INDEX AuthorizerActionCode = 3 /* Index Name Table Name */
CREATE_TEMP_TABLE AuthorizerActionCode = 4 /* Table Name NULL */
CREATE_TEMP_TRIGGER AuthorizerActionCode = 5 /* Trigger Name Table Name */
CREATE_TEMP_VIEW AuthorizerActionCode = 6 /* View Name NULL */
CREATE_TRIGGER AuthorizerActionCode = 7 /* Trigger Name Table Name */
CREATE_VIEW AuthorizerActionCode = 8 /* View Name NULL */
DELETE AuthorizerActionCode = 9 /* Table Name NULL */
DROP_INDEX AuthorizerActionCode = 10 /* Index Name Table Name */
DROP_TABLE AuthorizerActionCode = 11 /* Table Name NULL */
DROP_TEMP_INDEX AuthorizerActionCode = 12 /* Index Name Table Name */
DROP_TEMP_TABLE AuthorizerActionCode = 13 /* Table Name NULL */
DROP_TEMP_TRIGGER AuthorizerActionCode = 14 /* Trigger Name Table Name */
DROP_TEMP_VIEW AuthorizerActionCode = 15 /* View Name NULL */
DROP_TRIGGER AuthorizerActionCode = 16 /* Trigger Name Table Name */
DROP_VIEW AuthorizerActionCode = 17 /* View Name NULL */
INSERT AuthorizerActionCode = 18 /* Table Name NULL */
PRAGMA AuthorizerActionCode = 19 /* Pragma Name 1st arg or NULL */
READ AuthorizerActionCode = 20 /* Table Name Column Name */
SELECT AuthorizerActionCode = 21 /* NULL NULL */
TRANSACTION AuthorizerActionCode = 22 /* Operation NULL */
UPDATE AuthorizerActionCode = 23 /* Table Name Column Name */
ATTACH AuthorizerActionCode = 24 /* Filename NULL */
DETACH AuthorizerActionCode = 25 /* Database Name NULL */
ALTER_TABLE AuthorizerActionCode = 26 /* Database Name Table Name */
REINDEX AuthorizerActionCode = 27 /* Index Name NULL */
ANALYZE AuthorizerActionCode = 28 /* Table Name NULL */
CREATE_VTABLE AuthorizerActionCode = 29 /* Table Name Module Name */
DROP_VTABLE AuthorizerActionCode = 30 /* Table Name Module Name */
FUNCTION AuthorizerActionCode = 31 /* NULL Function Name */
SAVEPOINT AuthorizerActionCode = 32 /* Operation Savepoint Name */
COPY AuthorizerActionCode = 0 /* No longer used */
RECURSIVE AuthorizerActionCode = 33 /* NULL NULL */
/***************************************************** 3rd ************ 4th ***********/
AUTH_CREATE_INDEX AuthorizerActionCode = 1 /* Index Name Table Name */
AUTH_CREATE_TABLE AuthorizerActionCode = 2 /* Table Name NULL */
AUTH_CREATE_TEMP_INDEX AuthorizerActionCode = 3 /* Index Name Table Name */
AUTH_CREATE_TEMP_TABLE AuthorizerActionCode = 4 /* Table Name NULL */
AUTH_CREATE_TEMP_TRIGGER AuthorizerActionCode = 5 /* Trigger Name Table Name */
AUTH_CREATE_TEMP_VIEW AuthorizerActionCode = 6 /* View Name NULL */
AUTH_CREATE_TRIGGER AuthorizerActionCode = 7 /* Trigger Name Table Name */
AUTH_CREATE_VIEW AuthorizerActionCode = 8 /* View Name NULL */
AUTH_DELETE AuthorizerActionCode = 9 /* Table Name NULL */
AUTH_DROP_INDEX AuthorizerActionCode = 10 /* Index Name Table Name */
AUTH_DROP_TABLE AuthorizerActionCode = 11 /* Table Name NULL */
AUTH_DROP_TEMP_INDEX AuthorizerActionCode = 12 /* Index Name Table Name */
AUTH_DROP_TEMP_TABLE AuthorizerActionCode = 13 /* Table Name NULL */
AUTH_DROP_TEMP_TRIGGER AuthorizerActionCode = 14 /* Trigger Name Table Name */
AUTH_DROP_TEMP_VIEW AuthorizerActionCode = 15 /* View Name NULL */
AUTH_DROP_TRIGGER AuthorizerActionCode = 16 /* Trigger Name Table Name */
AUTH_DROP_VIEW AuthorizerActionCode = 17 /* View Name NULL */
AUTH_INSERT AuthorizerActionCode = 18 /* Table Name NULL */
AUTH_PRAGMA AuthorizerActionCode = 19 /* Pragma Name 1st arg or NULL */
AUTH_READ AuthorizerActionCode = 20 /* Table Name Column Name */
AUTH_SELECT AuthorizerActionCode = 21 /* NULL NULL */
AUTH_TRANSACTION AuthorizerActionCode = 22 /* Operation NULL */
AUTH_UPDATE AuthorizerActionCode = 23 /* Table Name Column Name */
AUTH_ATTACH AuthorizerActionCode = 24 /* Filename NULL */
AUTH_DETACH AuthorizerActionCode = 25 /* Database Name NULL */
AUTH_ALTER_TABLE AuthorizerActionCode = 26 /* Database Name Table Name */
AUTH_REINDEX AuthorizerActionCode = 27 /* Index Name NULL */
AUTH_ANALYZE AuthorizerActionCode = 28 /* Table Name NULL */
AUTH_CREATE_VTABLE AuthorizerActionCode = 29 /* Table Name Module Name */
AUTH_DROP_VTABLE AuthorizerActionCode = 30 /* Table Name Module Name */
AUTH_FUNCTION AuthorizerActionCode = 31 /* NULL Function Name */
AUTH_SAVEPOINT AuthorizerActionCode = 32 /* Operation Savepoint Name */
AUTH_RECURSIVE AuthorizerActionCode = 33 /* NULL NULL */
// AUTH_COPY AuthorizerActionCode = 0 /* No longer used */
)
// AuthorizerReturnCode are the integer codes
@@ -305,6 +344,18 @@ const (
AUTH_IGNORE AuthorizerReturnCode = 2 /* Don't allow access, but don't generate an error */
)
// CheckpointMode are all the checkpoint mode values.
//
// https://sqlite.org/c3ref/c_checkpoint_full.html
type CheckpointMode uint32
const (
CHECKPOINT_PASSIVE CheckpointMode = 0 /* Do as much as possible w/o blocking */
CHECKPOINT_FULL CheckpointMode = 1 /* Wait for writers, then checkpoint */
CHECKPOINT_RESTART CheckpointMode = 2 /* Like FULL but wait for readers */
CHECKPOINT_TRUNCATE CheckpointMode = 3 /* Like RESTART but also truncate WAL */
)
// TxnState are the allowed return values from [Conn.TxnState].
//
// https://sqlite.org/c3ref/c_txn_none.html
@@ -316,6 +367,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

@@ -84,19 +84,18 @@ func (ctx Context) ResultFloat(value float64) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultText(value string) {
ptr := ctx.c.newString(value)
ctx.c.call("sqlite3_result_text64",
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
uint64(ctx.c.freer), _UTF8)
ctx.c.call("sqlite3_result_text_go",
uint64(ctx.handle), uint64(ptr), uint64(len(value)))
}
// ResultRawText sets the text result of the function to a []byte.
// Returning a nil slice is the same as calling [Context.ResultNull].
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultRawText(value []byte) {
ptr := ctx.c.newBytes(value)
ctx.c.call("sqlite3_result_text64",
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
uint64(ctx.c.freer), _UTF8)
ctx.c.call("sqlite3_result_text_go",
uint64(ctx.handle), uint64(ptr), uint64(len(value)))
}
// ResultBlob sets the result of the function to a []byte.
@@ -105,9 +104,8 @@ func (ctx Context) ResultRawText(value []byte) {
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultBlob(value []byte) {
ptr := ctx.c.newBytes(value)
ctx.c.call("sqlite3_result_blob64",
uint64(ctx.handle), uint64(ptr), uint64(len(value)),
uint64(ctx.c.freer))
ctx.c.call("sqlite3_result_blob_go",
uint64(ctx.handle), uint64(ptr), uint64(len(value)))
}
// ResultZeroBlob sets the result of the function to a zero-filled, length n BLOB.
@@ -130,7 +128,8 @@ func (ctx Context) ResultNull() {
//
// 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
}
@@ -153,9 +152,8 @@ func (ctx Context) resultRFC3339Nano(value time.Time) {
buf := util.View(ctx.c.mod, ptr, maxlen)
buf = value.AppendFormat(buf[:0], time.RFC3339Nano)
ctx.c.call("sqlite3_result_text64",
uint64(ctx.handle), uint64(ptr), uint64(len(buf)),
uint64(ctx.c.freer), _UTF8)
ctx.c.call("sqlite3_result_text_go",
uint64(ctx.handle), uint64(ptr), uint64(len(buf)))
}
// ResultPointer sets the result of the function to NULL, just like [Context.ResultNull],
@@ -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",
uint64(ctx.handle), uint64(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)
}

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"
@@ -63,39 +97,58 @@ var driverName = "sqlite3"
func init() {
if driverName != "" {
sql.Register(driverName, sqlite{})
sql.Register(driverName, &SQLite{})
}
}
// Open opens the SQLite database specified by dataSourceName as a [database/sql.DB].
//
// The init function is called by the driver on new connections.
// The conn can be used to execute queries, register functions, etc.
// Any error return closes the conn and passes the error to [database/sql].
func Open(dataSourceName string, init func(*sqlite3.Conn) error) (*sql.DB, error) {
c, err := newConnector(dataSourceName, init)
// 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.
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
}
return sql.OpenDB(c), nil
}
type sqlite struct{}
// SQLite implements [database/sql/driver.Driver].
type SQLite struct{}
func (sqlite) Open(name string) (driver.Conn, error) {
c, err := newConnector(name, nil)
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 := newConnector(name, nil, nil)
if err != nil {
return nil, err
}
return c.Connect(context.Background())
}
func (sqlite) OpenConnector(name string) (driver.Connector, error) {
return newConnector(name, nil)
// OpenConnector implements [database/sql/driver.DriverContext].
func (d *SQLite) OpenConnector(name string) (driver.Connector, error) {
return newConnector(name, nil, nil)
}
func newConnector(name string, init func(*sqlite3.Conn) error) (*connector, error) {
c := connector{name: name, init: init}
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:") {
@@ -111,10 +164,8 @@ func newConnector(name string, init func(*sqlite3.Conn) error) (*connector, erro
}
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)
}
@@ -138,30 +189,31 @@ func newConnector(name string, init func(*sqlite3.Conn) error) (*connector, erro
type connector struct {
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 sqlite{}
return &SQLite{}
}
func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) {
c := &conn{
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 res == nil {
c.Close()
}
}()
@@ -170,7 +222,7 @@ 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
}
@@ -186,6 +238,7 @@ func (n *connector) Connect(ctx context.Context) (_ driver.Conn, err error) {
if err != nil {
return nil, err
}
defer s.Close()
if s.Step() && s.ColumnBool(0) {
c.readOnly = '1'
} else {
@@ -196,57 +249,93 @@ 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)
// }
//
// 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 {
return c.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)
@@ -260,7 +349,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()
}
@@ -268,16 +357,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)
}
@@ -293,7 +383,7 @@ func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, e
s.Close()
return nil, util.TailErr
}
return &stmt{Stmt: s, tmRead: c.tmRead, tmWrite: c.tmWrite}, nil
return &stmt{Stmt: s, tmRead: c.tmRead, tmWrite: c.tmWrite, inputs: -2}, nil
}
func (c *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
@@ -319,7 +409,9 @@ func (c *conn) ExecContext(ctx context.Context, query string, args []driver.Name
return newResult(c.Conn), nil
}
func (*conn) CheckNamedValue(arg *driver.NamedValue) error {
func (c *conn) CheckNamedValue(arg *driver.NamedValue) error {
// Fast path: short circuit argument verification.
// Arguments will be rejected by conn.ExecContext.
return nil
}
@@ -327,6 +419,7 @@ type stmt struct {
*sqlite3.Stmt
tmWrite sqlite3.TimeFormat
tmRead sqlite3.TimeFormat
inputs int
}
var (
@@ -337,22 +430,29 @@ var (
)
func (s *stmt) NumInput() int {
if s.inputs >= -1 {
return s.inputs
}
n := s.Stmt.BindCount()
for i := 1; i <= n; i++ {
if s.Stmt.BindName(i) != "" {
s.inputs = -1
return -1
}
}
s.inputs = n
return n
}
// 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))
}
@@ -366,6 +466,7 @@ func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (drive
defer s.Stmt.Conn().SetInterrupt(old)
err = s.Stmt.Exec()
s.Stmt.ClearBindings()
if err != nil {
return nil, err
}
@@ -381,19 +482,14 @@ func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driv
return &rows{ctx: ctx, stmt: s}, nil
}
func (s *stmt) setupBindings(args []driver.NamedValue) error {
err := s.Stmt.ClearBindings()
if err != nil {
return err
}
func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
var ids [3]int
for _, arg := range args {
ids := ids[:0]
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)
}
@@ -427,9 +523,9 @@ func (s *stmt) setupBindings(args []driver.NamedValue) error {
default:
panic(util.AssertErr())
}
}
if err != nil {
return err
if err != nil {
return err
}
}
}
return nil
@@ -483,8 +579,29 @@ type rows struct {
*stmt
names []string
types []string
nulls []bool
scans []scantype
}
type scantype byte
const (
_ANY scantype = iota
_INT scantype = scantype(sqlite3.INTEGER)
_REAL scantype = scantype(sqlite3.FLOAT)
_TEXT scantype = scantype(sqlite3.TEXT)
_BLOB scantype = scantype(sqlite3.BLOB)
_NULL scantype = scantype(sqlite3.NULL)
_BOOL scantype = iota
_TIME
)
var (
// Ensure these interfaces are implemented:
_ driver.RowsColumnTypeDatabaseTypeName = &rows{}
_ driver.RowsColumnTypeNullable = &rows{}
)
func (r *rows) Close() error {
r.Stmt.ClearBindings()
return r.Stmt.Reset()
@@ -493,27 +610,69 @@ func (r *rows) Close() error {
func (r *rows) Columns() []string {
if r.names == nil {
count := r.Stmt.ColumnCount()
r.names = make([]string, count)
for i := range r.names {
r.names[i] = r.Stmt.ColumnName(i)
names := make([]string, count)
for i := range names {
names[i] = r.Stmt.ColumnName(i)
}
r.names = names
}
return r.names
}
func (r *rows) loadColumnMetadata() {
if r.nulls == nil {
count := r.Stmt.ColumnCount()
nulls := make([]bool, count)
types := make([]string, count)
scans := make([]scantype, count)
for i := range nulls {
if col := r.Stmt.ColumnOriginName(i); col != "" {
types[i], _, nulls[i], _, _, _ = r.Stmt.Conn().TableColumnMetadata(
r.Stmt.ColumnDatabaseName(i),
r.Stmt.ColumnTableName(i),
col)
types[i] = strings.ToUpper(types[i])
// These types are only used before we have rows,
// and otherwise as type hints.
// The first few ensure STRICT tables are strictly typed.
// The other two are type hints for booleans and time.
switch types[i] {
case "INT", "INTEGER":
scans[i] = _INT
case "REAL":
scans[i] = _REAL
case "TEXT":
scans[i] = _TEXT
case "BLOB":
scans[i] = _BLOB
case "BOOLEAN":
scans[i] = _BOOL
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
scans[i] = _TIME
}
}
}
r.nulls = nulls
r.types = types
r.scans = scans
}
}
func (r *rows) declType(index int) string {
if r.types == nil {
count := r.Stmt.ColumnCount()
r.types = make([]string, count)
for i := range r.types {
r.types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
types := make([]string, count)
for i := range types {
types[i] = strings.ToUpper(r.Stmt.ColumnDeclType(i))
}
r.types = types
}
return r.types[index]
}
func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
decltype := r.declType(index)
r.loadColumnMetadata()
decltype := r.types[index]
if len := len(decltype); len > 0 && decltype[len-1] == ')' {
if i := strings.LastIndexByte(decltype, '('); i >= 0 {
decltype = decltype[:i]
@@ -522,6 +681,58 @@ func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
return strings.TrimSpace(decltype)
}
func (r *rows) ColumnTypeNullable(index int) (nullable, ok bool) {
r.loadColumnMetadata()
if r.nulls[index] {
return false, true
}
return true, false
}
func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
r.loadColumnMetadata()
scan := r.scans[index]
if r.Stmt.Busy() {
// SQLite is dynamically typed and we now have a row.
// Always use the type of the value itself,
// unless the scan type is more specific
// and can scan the actual value.
val := scantype(r.Stmt.ColumnType(index))
useValType := true
switch {
case scan == _TIME && val != _BLOB && val != _NULL:
t := r.Stmt.ColumnTime(index, r.tmRead)
useValType = t == time.Time{}
case scan == _BOOL && val == _INT:
i := r.Stmt.ColumnInt64(index)
useValType = i != 0 && i != 1
case scan == _BLOB && val == _NULL:
useValType = false
}
if useValType {
scan = val
}
}
switch scan {
case _INT:
return reflect.TypeOf(int64(0))
case _REAL:
return reflect.TypeOf(float64(0))
case _TEXT:
return reflect.TypeOf("")
case _BLOB:
return reflect.TypeOf([]byte{})
case _BOOL:
return reflect.TypeOf(false)
case _TIME:
return reflect.TypeOf(time.Time{})
default:
return reflect.TypeOf((*any)(nil)).Elem()
}
}
func (r *rows) Next(dest []driver.Value) error {
old := r.Stmt.Conn().SetInterrupt(r.ctx)
defer r.Stmt.Conn().SetInterrupt(old)
@@ -534,35 +745,33 @@ 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, _ bool) {
if r.tmRead == sqlite3.TimeFormatDefault {
func (r *rows) decodeTime(i int, v any) (_ time.Time, ok bool) {
switch v := v.(type) {
case int64, float64:
// could be a time value
case string:
if r.tmWrite != "" && r.tmWrite != time.RFC3339 && r.tmWrite != time.RFC3339Nano {
break
}
t, ok := maybeTime(v)
if ok {
return t, true
}
default:
return
}
switch r.declType(i) {
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
// maybe
default:
return
}
switch v.(type) {
case int64, float64, string:
// maybe
// could be a time value
default:
return
}

View File

@@ -7,15 +7,29 @@ import (
"errors"
"math"
"net/url"
"path/filepath"
"reflect"
"testing"
"time"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/vfs/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()
@@ -36,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)
}
@@ -55,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)
}
@@ -80,10 +100,12 @@ func Test_Open_pragma_invalid(t *testing.T) {
func Test_Open_txLock(t *testing.T) {
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)
}
@@ -114,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")
}
@@ -126,13 +151,15 @@ func Test_Open_txLock_invalid(t *testing.T) {
func Test_BeginTx(t *testing.T) {
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)
}
@@ -153,7 +180,7 @@ func Test_BeginTx(t *testing.T) {
t.Fatal(err)
}
_, err = tx1.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
_, err = tx1.Exec(`CREATE TABLE test (col)`)
if err == nil {
t.Error("want error")
}
@@ -174,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)
}
@@ -214,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)
}
@@ -266,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)
}
@@ -302,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)
}
@@ -310,7 +344,7 @@ func Test_time(t *testing.T) {
twosday := time.Date(2022, 2, 22, 22, 22, 22, 0, time.UTC)
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS test (at DATETIME)`)
_, err = db.Exec(`CREATE TABLE test (at DATETIME)`)
if err != nil {
t.Fatal(err)
}
@@ -332,3 +366,104 @@ func Test_time(t *testing.T) {
})
}
}
func Test_ColumnType_ScanType(t *testing.T) {
var (
INT = reflect.TypeOf(int64(0))
REAL = reflect.TypeOf(float64(0))
TEXT = reflect.TypeOf("")
BLOB = reflect.TypeOf([]byte{})
BOOL = reflect.TypeOf(false)
TIME = reflect.TypeOf(time.Time{})
ANY = reflect.TypeOf((*any)(nil)).Elem()
)
t.Parallel()
tmp := memdb.TestDB(t)
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE test (
col_int INTEGER,
col_real REAL,
col_text TEXT,
col_blob BLOB,
col_bool BOOLEAN,
col_time DATETIME,
col_decimal DECIMAL
);
INSERT INTO test VALUES
(1, 1, 1, 1, 1, 1, 1),
(2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0),
('1', '1', '1', '1', '1', '1', '1'),
('x', 'x', 'x', 'x', 'x', 'x', 'x'),
(x'', x'', x'', x'', x'', x'', x''),
('2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z',
'2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z'),
(TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
(NULL, NULL, NULL, NULL, NULL, NULL, NULL);
`)
if err != nil {
t.Fatal(err)
}
rows, err := db.Query(`SELECT * FROM test`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
cols, err := rows.ColumnTypes()
if err != nil {
t.Fatal(err)
}
want := [][]reflect.Type{
{INT, REAL, TEXT, BLOB, BOOL, TIME, ANY},
{INT, REAL, TEXT, INT, BOOL, TIME, INT},
{INT, REAL, TEXT, REAL, INT, TIME, INT},
{INT, REAL, TEXT, TEXT, BOOL, TIME, INT},
{TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT},
{BLOB, BLOB, BLOB, BLOB, BLOB, BLOB, BLOB},
{TEXT, TEXT, TEXT, TEXT, TEXT, TIME, TEXT},
{INT, REAL, TEXT, INT, BOOL, TIME, INT},
{ANY, ANY, ANY, BLOB, ANY, ANY, ANY},
}
for j, c := range cols {
got := c.ScanType()
if got != want[0][j] {
t.Errorf("want %v, got %v, at column %d", want[0][j], got, j)
}
}
dest := make([]any, len(cols))
for i := 1; rows.Next(); i++ {
cols, err := rows.ColumnTypes()
if err != nil {
t.Fatal(err)
}
for j, c := range cols {
got := c.ScanType()
if got != want[i][j] {
t.Errorf("want %v, got %v, at row %d column %d", want[i][j], got, i, j)
}
dest[j] = reflect.New(got).Interface()
}
err = rows.Scan(dest...)
if err != nil {
t.Error(err)
}
}
err = rows.Err()
if err != nil {
t.Fatal(err)
}
}

144
driver/example2_test.go Normal file
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,3 +1,5 @@
//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk
package driver_test
// Adapted from: https://go.dev/doc/tutorial/database-access
@@ -10,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,18 +11,18 @@ 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)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS orders (
CREATE TABLE orders (
cart_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
cart TEXT
);
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

@@ -10,13 +10,13 @@ import (
)
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)
}
defer db.Close()
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
_, err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`)
if err != nil {
log.Fatal(err)
}

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

@@ -1,26 +1,41 @@
# Embeddable WASM build of SQLite
# Embeddable Wasm build of SQLite
This folder includes an embeddable WASM build of SQLite 3.45.1 for use with
This folder includes an embeddable Wasm build of SQLite 3.47.2 for use with
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
The following optional features are compiled in:
- [math functions](https://sqlite.org/lang_mathfunc.html)
- [FTS3/4](https://sqlite.org/fts3.html)/[5](https://sqlite.org/fts5.html)
- [FTS5](https://sqlite.org/fts5.html)
- [JSON](https://sqlite.org/json1.html)
- [R*Tree](https://sqlite.org/rtree.html)
- [GeoPoly](https://sqlite.org/geopoly.html)
- [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)
- [decimal](https://github.com/sqlite/sqlite/blob/master/ext/misc/decimal.c)
- [ieee754](https://github.com/sqlite/sqlite/blob/master/ext/misc/ieee754.c)
- [regexp](https://github.com/sqlite/sqlite/blob/master/ext/misc/regexp.c)
- [series](https://github.com/sqlite/sqlite/blob/master/ext/misc/series.c)
- [uint](https://github.com/sqlite/sqlite/blob/master/ext/misc/uint.c)
- [uuid](https://github.com/sqlite/sqlite/blob/master/ext/misc/uuid.c)
- [time](../sqlite3/time.c)
See the [configuration options](../sqlite3/sqlite_cfg.h),
See the [configuration options](../sqlite3/sqlite_opt.h),
and [patches](../sqlite3) applied.
Built using [`wasi-sdk`](https://github.com/WebAssembly/wasi-sdk),
and [`binaryen`](https://github.com/WebAssembly/binaryen).
and [`binaryen`](https://github.com/WebAssembly/binaryen).
The build is easily reproducible, and verifiable, using
[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/

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

@@ -0,0 +1,16 @@
# Embeddable Wasm build of SQLite
This folder includes an embeddable Wasm build of SQLite, including the experimental
[`BEGIN CONCURRENT`](https://sqlite.org/src/doc/begin-concurrent/doc/begin_concurrent.md) and
[Wal2](https://sqlite.org/cgi/src/doc/wal2/doc/wal2.md) patches.
> [!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 both 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.

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

@@ -0,0 +1,48 @@
package bcw2
import (
"path/filepath"
"testing"
"github.com/ncruces/go-sqlite3/driver"
"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")
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.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.48.0" {
t.Error(version)
}
}

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

@@ -0,0 +1,65 @@
#!/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/08cfa7e8b3090151
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=08cfa7e8 | tar xz
cd sqlite
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c
else
sh configure
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 --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 \
-matomics -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_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

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

@@ -0,0 +1,13 @@
module github.com/ncruces/go-sqlite3/embed/bcw2
go 1.21
toolchain go1.23.0
require github.com/ncruces/go-sqlite3 v0.21.0
require (
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.8.2 // indirect
golang.org/x/sys v0.28.0 // indirect
)

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

@@ -0,0 +1,10 @@
github.com/ncruces/go-sqlite3 v0.21.0 h1:EwKFoy1hHEopN4sFZarmi+McXdbCcbTuLixhEayXVbQ=
github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=

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

@@ -0,0 +1,23 @@
// 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"
"github.com/ncruces/go-sqlite3"
)
//go:embed bcw2.wasm
var binary []byte
func init() {
sqlite3.Binary = binary
}

View File

@@ -4,25 +4,27 @@ set -euo pipefail
cd -P -- "$(dirname -- "$0")"
ROOT=../
BINARYEN="$ROOT/tools/binaryen-version_116/bin"
WASI_SDK="$ROOT/tools/wasi-sdk-21.0/bin"
BINARYEN="$ROOT/tools/binaryen/bin"
WASI_SDK="$ROOT/tools/wasi-sdk/bin"
"$WASI_SDK/clang" --target=wasm32-wasi -std=c17 -flto -g0 -O2 \
-Wall -Wextra -Wno-unused-parameter \
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 \
-matomics -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,13 +1,12 @@
free
malloc
malloc_destructor
aligned_alloc
sqlite3_anycollseq_init
sqlite3_autovacuum_pages_go
sqlite3_backup_finish
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
@@ -15,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
@@ -34,10 +33,13 @@ sqlite3_collation_needed_go
sqlite3_column_blob
sqlite3_column_bytes
sqlite3_column_count
sqlite3_column_database_name
sqlite3_column_decltype
sqlite3_column_double
sqlite3_column_int64
sqlite3_column_name
sqlite3_column_origin_name
sqlite3_column_table_name
sqlite3_column_text
sqlite3_column_type
sqlite3_column_value
@@ -49,28 +51,42 @@ 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_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
@@ -79,27 +95,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
@@ -114,4 +133,7 @@ sqlite3_vtab_in_first
sqlite3_vtab_in_next
sqlite3_vtab_nochange
sqlite3_vtab_on_conflict
sqlite3_vtab_rhs_value
sqlite3_vtab_rhs_value
sqlite3_wal_autocheckpoint
sqlite3_wal_checkpoint_v2
sqlite3_wal_hook_go

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.47.2" {
t.Error(version)
}
}

Binary file not shown.

View File

@@ -106,6 +106,11 @@ func (e ErrorCode) Temporary() bool {
return e == BUSY
}
// ExtendedCode returns the extended error code for this error.
func (e ErrorCode) ExtendedCode() ExtendedErrorCode {
return ExtendedErrorCode(e)
}
// Error implements the error interface.
func (e ExtendedErrorCode) Error() string {
return util.ErrorCodeString(uint32(e))
@@ -136,6 +141,11 @@ func (e ExtendedErrorCode) Timeout() bool {
return e == BUSY_TIMEOUT
}
// Code returns the primary error code for this error.
func (e ExtendedErrorCode) Code() ErrorCode {
return ErrorCode(e)
}
func errorCode(err error, def ErrorCode) (msg string, code uint32) {
switch code := err.(type) {
case nil:

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)
}
}
@@ -166,3 +167,32 @@ func Test_ExtendedErrorCode_Error(t *testing.T) {
}
}
}
func Test_errorCode(t *testing.T) {
tests := []struct {
arg error
wantMsg string
wantCode uint32
}{
{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 != uint32(tt.wantCode) {
t.Errorf("errorCode() gotCode = %d, want %d", gotCode, tt.wantCode)
}
})
}
}

View File

@@ -16,7 +16,7 @@ func Example() {
log.Fatal(err)
}
err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`)
if err != nil {
log.Fatal(err)
}

37
ext/README.md Normal file
View File

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

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[array](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

@@ -11,13 +11,12 @@ import (
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/array"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"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)
}
@@ -52,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
@@ -89,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)
}
@@ -125,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)
}
}
@@ -138,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,12 +27,17 @@ 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.
@@ -42,28 +47,33 @@ func readblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
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)
}
@@ -71,19 +81,25 @@ func writeblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
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 +114,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)
@@ -128,12 +144,12 @@ func getAuxBlob(ctx sqlite3.Context, arg []sqlite3.Value, write bool) (*sqlite3.
return ctx.Conn().OpenBlob(db, table, column, row, write)
}
func setAuxBlob(ctx sqlite3.Context, blob *sqlite3.Blob, writer bool) {
func setAuxBlob(ctx sqlite3.Context, blob *sqlite3.Blob, open bool) {
// This ensures the blob is closed if db, table, column or write change.
ctx.SetAuxData(0, blob) // db
ctx.SetAuxData(1, blob) // table
ctx.SetAuxData(2, blob) // column
if writer {
if open {
ctx.SetAuxData(4, blob) // write
}
}

View File

@@ -5,6 +5,7 @@ import (
"log"
"os"
"reflect"
"strings"
"testing"
"github.com/ncruces/go-sqlite3"
@@ -12,22 +13,20 @@ import (
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/array"
"github.com/ncruces/go-sqlite3/ext/blobio"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
)
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)
}
defer db.Close()
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
_, err = db.Exec(`CREATE TABLE test (col)`)
if err != nil {
log.Fatal(err)
}
@@ -35,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)
}
@@ -59,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()
@@ -68,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")
@@ -78,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 IF NOT EXISTS test1 (col);
CREATE TABLE IF NOT EXISTS test2 (col);
CREATE TABLE test1 (col);
CREATE TABLE test2 (col);
INSERT INTO test1 VALUES (x'cafe');
INSERT INTO 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)
}
}
@@ -128,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")
@@ -138,9 +231,16 @@ 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 IF NOT EXISTS test1 (col);
CREATE TABLE IF NOT EXISTS test2 (col);
CREATE TABLE test1 (col);
CREATE TABLE test2 (col);
INSERT INTO test1 VALUES (x'cafe');
INSERT INTO test2 VALUES (x'babe');
`)

346
ext/bloom/bloom.go Normal file
View File

@@ -0,0 +1,346 @@
// Package bloom provides a Bloom filter virtual table.
//
// A Bloom filter is a space-efficient probabilistic data structure
// used to test whether an element is a member of a set.
//
// https://github.com/nalgeon/sqlean/issues/27#issuecomment-1002267134
package bloom
import (
"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) error {
return sqlite3.CreateModule(db, "bloom_filter", create, connect)
}
type bloom struct {
db *sqlite3.Conn
schema string
storage string
prob float64
bytes int64
hashes int
}
func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom, err error) {
b := bloom{
db: db,
schema: schema,
storage: table + "_storage",
}
var nelem int64
if len(arg) > 0 {
nelem, err = strconv.ParseInt(arg[0], 10, 64)
if err != nil {
return nil, err
}
if nelem <= 0 {
return nil, util.ErrorString("bloom: number of elements in filter must be positive")
}
} else {
nelem = 100
}
if len(arg) > 1 {
b.prob, err = strconv.ParseFloat(arg[1], 64)
if err != nil {
return nil, err
}
if b.prob <= 0 || b.prob >= 1 {
return nil, util.ErrorString("bloom: probability must be in the range (0,1)")
}
} else {
b.prob = 0.01
}
if len(arg) > 2 {
b.hashes, err = strconv.Atoi(arg[2])
if err != nil {
return nil, err
}
if b.hashes <= 0 {
return nil, util.ErrorString("bloom: number of hash functions must be positive")
}
} else {
b.hashes = max(1, numHashes(b.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(b.schema), sqlite3.QuoteIdentifier(b.storage)))
if err != nil {
return nil, err
}
id := db.LastInsertRowID()
defer db.SetLastInsertRowID(id)
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(b.schema), sqlite3.QuoteIdentifier(b.storage),
b.bytes, b.prob, nelem, 8*b.bytes, b.hashes))
if err != nil {
b.Destroy()
return nil, err
}
return &b, nil
}
func connect(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom, err error) {
b := bloom{
db: db,
schema: schema,
storage: table + "_storage",
}
err = db.DeclareVTab(
`CREATE TABLE x(present, word HIDDEN NOT NULL PRIMARY KEY) WITHOUT ROWID`)
if err != nil {
return nil, err
}
load, _, err := db.Prepare(fmt.Sprintf(
`SELECT m/8, p, k FROM %s.%s WHERE rowid = 1`,
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 {
return nil, err
}
return nil, sqlite3.CORRUPT_VTAB
}
b.bytes = load.ColumnInt64(0)
b.prob = load.ColumnFloat(1)
b.hashes = load.ColumnInt(2)
return &b, nil
}
func (b *bloom) Destroy() error {
return b.db.Exec(fmt.Sprintf(`DROP TABLE %s.%s`,
sqlite3.QuoteIdentifier(b.schema),
sqlite3.QuoteIdentifier(b.storage)))
}
func (b *bloom) Rename(new string) error {
new += "_storage"
err := b.db.Exec(fmt.Sprintf(`ALTER TABLE %s.%s RENAME TO %s`,
sqlite3.QuoteIdentifier(b.schema),
sqlite3.QuoteIdentifier(b.storage),
sqlite3.QuoteIdentifier(new),
))
if err == nil {
b.storage = new
}
return err
}
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(
`SELECT typeof(data), length(data), p, n, m, k FROM %s.%s WHERE rowid = 1`,
sqlite3.QuoteIdentifier(t.schema), sqlite3.QuoteIdentifier(t.storage)))
if err != nil {
return fmt.Errorf("bloom: %v", err) // can't wrap!
}
defer load.Close()
err = util.ErrorString("bloom: invalid parameters")
if !load.Step() {
return err
}
if t := load.ColumnText(0); t != "blob" {
return err
}
if m := load.ColumnInt64(4); m <= 0 || m%8 != 0 {
return err
} else if load.ColumnInt64(1) != m/8 {
return err
}
if p := load.ColumnFloat(2); p <= 0 || p >= 1 {
return err
}
if n := load.ColumnInt64(3); n <= 0 {
return err
}
if k := load.ColumnInt(5); k <= 0 {
return err
}
return nil
}
func (b *bloom) BestIndex(idx *sqlite3.IndexInfo) error {
for i, cst := range idx.Constraint {
if cst.Usable && cst.Column == 1 &&
cst.Op == sqlite3.INDEX_CONSTRAINT_EQ {
idx.ConstraintUsage[i].ArgvIndex = 1
idx.OrderByConsumed = true
idx.EstimatedRows = 1
idx.EstimatedCost = float64(b.hashes)
idx.IdxFlags = sqlite3.INDEX_SCAN_UNIQUE
return nil
}
}
return sqlite3.CONSTRAINT
}
func (b *bloom) Update(arg ...sqlite3.Value) (rowid int64, err error) {
if arg[0].Type() != sqlite3.NULL {
if len(arg) == 1 {
return 0, util.ErrorString("bloom: elements cannot be deleted")
}
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)
if err != nil {
return 0, err
}
defer f.Close()
for n := 0; n < b.hashes; n++ {
hash := calcHash(n, blob)
hash %= uint64(b.bytes * 8)
bitpos := byte(hash % 8)
bytepos := int64(hash / 8)
var buf [1]byte
_, err = f.Seek(bytepos, io.SeekStart)
if err != nil {
return 0, err
}
_, err = f.Read(buf[:])
if err != nil {
return 0, err
}
buf[0] |= 1 << bitpos
_, err = f.Seek(bytepos, io.SeekStart)
if err != nil {
return 0, err
}
_, err = f.Write(buf[:])
if err != nil {
return 0, err
}
}
return 0, nil
}
func (b *bloom) Open() (sqlite3.VTabCursor, error) {
return &cursor{bloom: b}, nil
}
type cursor struct {
*bloom
arg *sqlite3.Value
eof bool
}
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
c.eof = false
c.arg = &arg[0]
blob := arg[0].RawBlob()
f, err := c.db.OpenBlob(c.schema, c.storage, "data", 1, false)
if err != nil {
return err
}
defer f.Close()
for n := 0; n < c.hashes && !c.eof; n++ {
hash := calcHash(n, blob)
hash %= uint64(c.bytes * 8)
bitpos := byte(hash % 8)
bytepos := int64(hash / 8)
var buf [1]byte
_, err = f.Seek(bytepos, io.SeekStart)
if err != nil {
return err
}
_, err = f.Read(buf[:])
if err != nil {
return err
}
c.eof = buf[0]&(1<<bitpos) == 0
}
return nil
}
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
if ctx.VTabNoChange() {
return nil
}
switch n {
case 0:
ctx.ResultBool(true)
case 1:
ctx.ResultValue(*c.arg)
}
return nil
}
func (c *cursor) Next() error {
c.eof = true
return nil
}
func (c *cursor) EOF() bool {
return c.eof
}
func (c *cursor) RowID() (int64, error) {
// notest // WITHOUT ROWID
return 0, nil
}
func calcHash(k int, b []byte) uint64 {
return siphash.Hash(^uint64(k), uint64(k), b)
}
func numHashes(p float64) int {
k := math.Round(-math.Log2(p))
return max(1, int(k))
}
func numBytes(n int64, p float64) int64 {
m := math.Ceil(float64(n) * math.Log(p) / -(math.Ln2 * math.Ln2))
return (int64(m) + 7) / 8
}

195
ext/bloom/bloom_test.go Normal file
View File

@@ -0,0 +1,195 @@
package bloom_test
import (
_ "embed"
"os"
"path/filepath"
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/bloom"
_ "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()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
err = db.Exec(`
CREATE VIRTUAL TABLE sports_cars USING bloom_filter();
INSERT INTO sports_cars VALUES ('ferrari'), ('lamborghini'), ('alfa romeo')
`)
if err != nil {
t.Fatal(err)
}
query, _, err := db.Prepare(`SELECT COUNT(*) FROM sports_cars(?)`)
if err != nil {
t.Fatal(err)
}
err = query.BindText(1, "ferrari")
if err != nil {
t.Fatal(err)
}
if !query.Step() {
t.Error("no rows")
}
if !query.ColumnBool(0) {
t.Error("want true")
}
err = query.Reset()
if err != nil {
t.Fatal(err)
}
err = query.BindText(1, "bmw")
if err != nil {
t.Fatal(err)
}
if !query.Step() {
t.Error("no rows")
}
if query.ColumnBool(0) {
t.Error("want false")
}
err = query.Close()
if err != nil {
t.Fatal(err)
}
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)
}
}
//go:embed testdata/bloom.db
var testDB []byte
func Test_compatible(t *testing.T) {
t.Parallel()
tmp := filepath.Join(t.TempDir(), "bloom.db")
err := os.WriteFile(tmp, testDB, 0666)
if err != nil {
t.Fatal(err)
}
db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1")
if err != nil {
t.Fatal(err)
}
defer db.Close()
query, _, err := db.Prepare(`SELECT COUNT(*) FROM plants(?)`)
if err != nil {
t.Fatal(err)
}
defer query.Close()
err = query.BindText(1, "apple")
if err != nil {
t.Fatal(err)
}
if !query.Step() {
t.Error("no rows")
}
if !query.ColumnBool(0) {
t.Error("want true")
}
err = query.Reset()
if err != nil {
t.Fatal(err)
}
err = query.BindText(1, "lemon")
if err != nil {
t.Fatal(err)
}
if !query.Step() {
t.Error("no rows")
}
if query.ColumnBool(0) {
t.Error("want false")
}
err = query.Reset()
if err != nil {
t.Fatal(err)
}
err = db.Exec(`PRAGMA integrity_check`)
if err != nil {
t.Error(err)
}
err = db.Exec(`PRAGMA quick_check`)
if err != nil {
t.Error(err)
}
}
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")
}
}

BIN
ext/bloom/testdata/bloom.db vendored Normal file

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 {
if !cst.Usable {
continue
}
if 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,
}
}
continue
}
if 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
}
}
continue
}
if 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,
}
}
continue
}
if 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,
}
}
continue
}
if 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,
}
}
continue
}
}
if plan&1 == 0 ||
c.table == "" && plan&0xf00 == 0 ||
c.column == "" && plan&0xf000 == 0 ||
c.parent == "" && plan&0xf0000 == 0 {
return sqlite3.CONSTRAINT
}
idx.EstimatedCost = cost
idx.IdxNum = plan
return nil
}
func (c *closure) Open() (sqlite3.VTabCursor, error) {
return &cursor{closure: c}, nil
}
type cursor struct {
*closure
nodes []node
}
type node struct {
id int64
depth int
}
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
root := arg[0].Int64()
maxDepth := math.MaxInt
if idxNum&0xf0 != 0 {
maxDepth = arg[(idxNum>>4)&0xf].Int()
if idxNum&2 != 0 {
maxDepth -= 1
}
}
table := c.table
if idxNum&0xf00 != 0 {
table = arg[(idxNum>>8)&0xf].Text()
}
column := c.column
if idxNum&0xf000 != 0 {
column = arg[(idxNum>>12)&0xf].Text()
}
parent := c.parent
if idxNum&0xf0000 != 0 {
parent = arg[(idxNum>>16)&0xf].Text()
}
sql := fmt.Sprintf(
`SELECT %[1]s.%[2]s FROM %[1]s WHERE %[1]s.%[3]s=?`,
sqlite3.QuoteIdentifier(table),
sqlite3.QuoteIdentifier(column),
sqlite3.QuoteIdentifier(parent),
)
stmt, _, err := c.db.Prepare(sql)
if err != nil {
return err
}
defer stmt.Close()
c.nodes = []node{{root, 0}}
set := util.Set[int64]{}
set.Add(root)
for i := 0; i < len(c.nodes); i++ {
curr := c.nodes[i]
if curr.depth >= maxDepth {
continue
}
stmt.BindInt64(1, curr.id)
for stmt.Step() {
if stmt.ColumnType(0) == sqlite3.INTEGER {
next := stmt.ColumnInt64(0)
if !set.Contains(next) {
set.Add(next)
c.nodes = append(c.nodes, node{next, curr.depth + 1})
}
}
}
stmt.Reset()
}
return nil
}
func (c *cursor) Column(ctx sqlite3.Context, n int) error {
switch n {
case _COL_ID:
ctx.ResultInt64(c.nodes[0].id)
case _COL_DEPTH:
ctx.ResultInt(c.nodes[0].depth)
case _COL_TABLENAME:
ctx.ResultText(c.table)
case _COL_IDCOLUMN:
ctx.ResultText(c.column)
case _COL_PARENTCOLUMN:
ctx.ResultText(c.parent)
}
return nil
}
func (c *cursor) Next() error {
c.nodes = c.nodes[1:]
return nil
}
func (c *cursor) EOF() bool {
return len(c.nodes) == 0
}
func (c *cursor) RowID() (int64, error) {
return c.nodes[0].id, nil
}

184
ext/closure/closure_test.go Normal file
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)
}
@@ -40,6 +40,8 @@ func Test_uintArg(t *testing.T) {
}
func Test_boolArg(t *testing.T) {
t.Parallel()
tests := []struct {
arg string
key string
@@ -60,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)
}
@@ -76,6 +78,8 @@ func Test_boolArg(t *testing.T) {
}
func Test_runeArg(t *testing.T) {
t.Parallel()
tests := []struct {
arg string
key string
@@ -92,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

@@ -12,23 +12,25 @@ import (
"fmt"
"io"
"io/fs"
"strconv"
"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) {
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err error) {
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {
declare := func(db *sqlite3.Conn, _, _, _ string, arg ...string) (res *table, err error) {
var (
filename string
data string
@@ -36,53 +38,57 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
header bool
columns int = -1
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":
columns, err = uintArg(key, val)
case "comma":
comma, err = runeArg(key, val)
case "comment":
comment, err = runeArg(key, val)
default:
return nil, fmt.Errorf("csv: unknown %q parameter", key)
}
if err != nil {
return nil, err
}
done[key] = struct{}{}
done.Add(key)
}
if (filename == "") == (data == "") {
return nil, fmt.Errorf(`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{
fsys: fsys,
name: filename,
data: data,
comma: comma,
header: header,
t := &table{
fsys: fsys,
name: filename,
data: data,
comma: comma,
comment: comment,
header: header,
}
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
@@ -93,28 +99,34 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
}
}
schema = getSchema(header, columns, row)
} else {
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 {
fsys fs.FS
name string
data string
comma rune
header bool
fsys fs.FS
name string
data string
typs []affinity
comma rune
comment rune
header bool
}
func (t *table) BestIndex(idx *sqlite3.IndexInfo) error {
@@ -171,6 +183,7 @@ func (t *table) newReader() (*csv.Reader, io.Closer, error) {
csv := csv.NewReader(r)
csv.ReuseRecord = true
csv.Comma = t.comma
csv.Comment = t.comment
return csv, c, nil
}
@@ -224,9 +237,38 @@ 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) {
ctx.ResultText(c.row[col])
typ := text
if col < len(c.table.typs) {
typ = c.table.typs[col]
}
txt := c.row[col]
if txt == "" && typ != text {
return nil
}
switch typ {
case numeric, integer:
if strings.TrimLeft(txt, "+-0123456789") == "" {
if i, err := strconv.ParseInt(txt, 10, 64); err == nil {
ctx.ResultInt64(i)
return nil
}
}
fallthrough
case real:
if strings.TrimLeft(txt, "+-.0123456789Ee") == "" {
if f, err := strconv.ParseFloat(txt, 64); err == nil {
ctx.ResultFloat(f)
return nil
}
}
fallthrough
default:
}
ctx.ResultText(txt)
}
return nil
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/csv"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func Example() {
@@ -17,10 +18,13 @@ func Example() {
}
defer db.Close()
csv.Register(db)
err = csv.Register(db)
if err != nil {
log.Fatal(err)
}
err = db.Exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS eurofxref USING csv(
CREATE VIRTUAL TABLE eurofxref USING csv(
filename = 'testdata/eurofxref.csv',
header = YES,
columns = 42,
@@ -50,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()
@@ -59,17 +68,17 @@ func TestRegister(t *testing.T) {
}
defer db.Close()
csv.Register(db)
const data = `
# Comment
"Rob" "Pike" rob
"Ken" Thompson ken
Robert "Griesemer" "gri"`
err = db.Exec(`
CREATE VIRTUAL TABLE temp.users USING csv(
data = ` + sqlite3.Quote(data) + `,
schema = 'CREATE TABLE x(first_name, last_name, username)',
comma = '\t'
data = ` + sqlite3.Quote(data) + `,
schema = 'CREATE TABLE x(first_name, last_name, username)',
comma = '\t',
comment = '#'
)`)
if err != nil {
t.Fatal(err)
@@ -112,7 +121,7 @@ Robert "Griesemer" "gri"`
}
}
func TestRegister_errors(t *testing.T) {
func TestAffinity(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
@@ -121,7 +130,47 @@ func TestRegister_errors(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(
data = ` + sqlite3.Quote(data) + `,
schema = 'CREATE TABLE x(a numeric)'
)`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT * FROM temp.nums`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if stmt.Step() {
if got := stmt.ColumnText(0); got != "1" {
t.Errorf("got %q want 1", got)
}
}
if stmt.Step() {
if got := stmt.ColumnText(0); got != "0.1" {
t.Errorf("got %q want 0.1", got)
}
}
if stmt.Step() {
if got := stmt.ColumnText(0); got != "e" {
t.Errorf("got %q want e", got)
}
}
}
func TestRegister_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 temp.users USING csv()`)
if err == nil {

52
ext/csv/types.go Normal file
View File

@@ -0,0 +1,52 @@
package csv
import (
"strings"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
type affinity byte
const (
blob affinity = 0
text affinity = 1
numeric affinity = 2
integer affinity = 3
real affinity = 4
)
func getColumnAffinities(schema string) ([]affinity, error) {
tab, err := sql3util.ParseTable(schema)
if err != nil {
return nil, err
}
columns := tab.Columns
types := make([]affinity, len(columns))
for i, col := range columns {
types[i] = getAffinity(col.Type)
}
return types, nil
}
func getAffinity(declType string) affinity {
// https://sqlite.org/datatype3.html#determination_of_column_affinity
if declType == "" {
return blob
}
name := strings.ToUpper(declType)
if strings.Contains(name, "INT") {
return integer
}
if strings.Contains(name, "CHAR") || strings.Contains(name, "CLOB") || strings.Contains(name, "TEXT") {
return text
}
if strings.Contains(name, "BLOB") {
return blob
}
if strings.Contains(name, "REAL") || strings.Contains(name, "FLOA") || strings.Contains(name, "DOUB") {
return real
}
return numeric
}

32
ext/csv/types_test.go Normal file
View File

@@ -0,0 +1,32 @@
package csv
import "testing"
func Test_getAffinity(t *testing.T) {
tests := []struct {
decl string
want affinity
}{
{"", blob},
{"INTEGER", integer},
{"TINYINT", integer},
{"TEXT", text},
{"CHAR", text},
{"CLOB", text},
{"BLOB", blob},
{"REAL", real},
{"FLOAT", real},
{"DOUBLE", real},
{"NUMERIC", numeric},
{"DECIMAL", numeric},
{"BOOLEAN", numeric},
{"DATETIME", numeric},
}
for _, tt := range tests {
t.Run(tt.decl, func(t *testing.T) {
if got := getAffinity(tt.decl); got != tt.want {
t.Errorf("getAffinity() = %v, want %v", got, tt.want)
}
})
}
}

70
ext/fileio/coro.go Normal file
View File

@@ -0,0 +1,70 @@
//go:build !go1.23
package fileio
import (
"fmt"
"github.com/ncruces/go-sqlite3/internal/util"
)
// Adapted from: https://research.swtch.com/coro
const errCoroCanceled = util.ErrorString("coroutine canceled")
func coroNew[In, Out any](f func(In, func(Out) In) Out) (resume func(In) (Out, bool), cancel func()) {
type msg[T any] struct {
panic any
val T
}
cin := make(chan msg[In])
cout := make(chan msg[Out])
running := true
resume = func(in In) (out Out, ok bool) {
if !running {
return
}
cin <- msg[In]{val: in}
m := <-cout
if m.panic != nil {
panic(m.panic)
}
return m.val, running
}
cancel = func() {
if !running {
return
}
e := fmt.Errorf("%w", errCoroCanceled)
cin <- msg[In]{panic: e}
m := <-cout
if m.panic != nil && m.panic != e {
panic(m.panic)
}
}
yield := func(out Out) In {
cout <- msg[Out]{val: out}
m := <-cin
if m.panic != nil {
panic(m.panic)
}
return m.val
}
go func() {
defer func() {
if running {
running = false
cout <- msg[Out]{panic: recover()}
}
}()
var out Out
m := <-cin
if m.panic == nil {
out = f(m.val, yield)
}
running = false
cout <- msg[Out]{val: out}
}()
return resume, cancel
}

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, 0, 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

@@ -11,15 +11,15 @@ import (
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/fileio"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"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)
}
@@ -53,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
}
@@ -53,12 +62,12 @@ func (d fsdir) Open() (sqlite3.VTabCursor, error) {
type cursor struct {
fsdir
curr entry
next chan entry
done chan struct{}
base string
rowID int64
eof bool
base string
resume resume
cancel func()
curr entry
eof bool
rowID int64
}
type entry struct {
@@ -68,12 +77,8 @@ type entry struct {
}
func (c *cursor) Close() error {
if c.done != nil {
close(c.done)
s := <-c.next
c.done = nil
c.next = nil
return s.err
if c.cancel != nil {
c.cancel()
}
return nil
}
@@ -96,16 +101,14 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
c.base = base
}
c.rowID = 0
c.resume, c.cancel = pull(c, root)
c.eof = false
c.next = make(chan entry)
c.done = make(chan struct{})
go c.WalkDir(root)
c.rowID = 0
return c.Next()
}
func (c *cursor) Next() error {
curr, ok := <-c.next
curr, ok := next(c)
c.curr = curr
c.eof = !ok
c.rowID++
@@ -120,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
@@ -165,22 +168,3 @@ func (c *cursor) Column(ctx *sqlite3.Context, n int) error {
}
return nil
}
func (c *cursor) WalkDir(path string) {
defer close(c.next)
if c.fsys != nil {
fs.WalkDir(c.fsys, path, c.WalkDirFunc)
} else {
filepath.WalkDir(path, c.WalkDirFunc)
}
}
func (c *cursor) WalkDirFunc(path string, d fs.DirEntry, err error) error {
select {
case <-c.done:
return fs.SkipAll
case c.next <- entry{d, err, path}:
return nil
}
}

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

@@ -12,6 +12,8 @@ import (
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/fileio"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func Test_fsdir(t *testing.T) {
@@ -19,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
})
@@ -28,7 +32,7 @@ func Test_fsdir(t *testing.T) {
}
defer db.Close()
rows, err := db.Query(`SELECT * FROM fsdir('.', '.') LIMIT 4`)
rows, err := db.Query(`SELECT * FROM fsdir('.', '.')`)
if err != nil {
t.Fatal(err)
}
@@ -67,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,18 +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,18 +7,21 @@ import (
_ "crypto/sha512"
"testing"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "golang.org/x/crypto/blake2b"
_ "golang.org/x/crypto/blake2s"
_ "golang.org/x/crypto/md4"
_ "golang.org/x/crypto/ripemd160"
_ "golang.org/x/crypto/sha3"
"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
@@ -52,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,27 +26,32 @@ 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[lines](db, "lines", nil,
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
db.VTabConfig(sqlite3.VTAB_INNOCUOUS)
return lines{}, err
})
sqlite3.CreateModule[lines](db, "lines_read", nil,
func(db *sqlite3.Conn, _, _, _ string, _ ...string) (lines, error) {
err := db.DeclareVTab(`CREATE TABLE x(line TEXT, data HIDDEN)`)
db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
return lines{fsys}, err
})
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)`)
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)`)
if err == nil {
err = db.VTabConfig(sqlite3.VTAB_DIRECTONLY)
}
return lines{fsys}, err
}))
}
type lines struct {
@@ -89,7 +95,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)
}

View File

@@ -14,13 +14,12 @@ import (
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/lines"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"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)
}
@@ -58,7 +57,7 @@ func Example() {
if err := rows.Err(); err != nil {
log.Fatal(err)
}
// Output:
// Expected output:
// US: 141001
// GB: 22560
// CA: 11759
@@ -68,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)
}
@@ -101,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)
}
@@ -128,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)
}
@@ -162,11 +155,9 @@ 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)
}

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) (res *table, err error) {
if len(arg) != 3 {
return nil, fmt.Errorf("pivot: wrong number of arguments")
}
table := &table{db: db}
t := &table{db: db}
defer func() {
if err != nil {
table.Close()
if res == nil {
t.Close()
}
}()
@@ -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, fmt.Errorf("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, fmt.Errorf("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,12 +95,12 @@ func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (_ *table, err err
if err != nil {
return nil, err
}
return table, nil
return t, nil
}
func (t *table) Close() error {
for i := range t.cols {
t.cols[i].Close()
for _, c := range t.cols {
c.Close()
}
return nil
}
@@ -224,7 +225,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

@@ -9,18 +9,19 @@ import (
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/pivot"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
// https://antonz.org/sqlite-pivot-table/
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
@@ -82,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()
@@ -91,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;
@@ -141,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) {
@@ -152,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")

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

@@ -0,0 +1,240 @@
// 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"
"strings"
"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(re *regexp.Regexp) string {
prefix, complete := re.LiteralPrefix()
i := strings.IndexAny(prefix, "*?[")
if i < 0 {
if complete {
return prefix
}
i = len(prefix)
}
return prefix[:i] + "*"
}
func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
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) {
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) {
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)
}

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

@@ -0,0 +1,126 @@
package regexp
import (
"database/sql"
"regexp"
"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`, "a"},
{`a*`, "*"},
{`a+`, "a*"},
{`ab*`, "a*"},
{`ab+`, "ab*"},
{`a\?b`, "a*"},
}
for _, tt := range tests {
t.Run(tt.re, func(t *testing.T) {
if got := GlobPrefix(regexp.MustCompile(tt.re)); got != tt.want {
t.Errorf("GlobPrefix() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -8,17 +8,17 @@ package statement
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"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 +29,12 @@ type table struct {
func declare(db *sqlite3.Conn, _, _, _ string, arg ...string) (*table, error) {
if len(arg) != 1 {
return nil, fmt.Errorf("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
}
@@ -123,12 +123,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
@@ -202,7 +201,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

@@ -8,17 +8,18 @@ import (
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/statement"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func Example() {
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
@@ -47,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()
@@ -56,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))
`)
@@ -106,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

@@ -41,7 +41,16 @@ https://sqlite.org/lang_aggfunc.html
- [X] `RANK() OVER window`
- [X] `DENSE_RANK() OVER window`
- [X] `PERCENT_RANK() OVER window`
- [ ] `PERCENTILE_CONT(percentile) OVER window`
- [ ] `PERCENTILE_DISC(percentile) OVER window`
https://sqlite.org/windowfunctions.html#builtins
https://sqlite.org/windowfunctions.html#builtins
## Boolean aggregates
- [X] `EVERY(boolean)`
- [X] `SOME(boolean)`
## Additional aggregates
- [X] `MEDIAN(expression)`
- [X] `PERCENTILE_CONT(expression, fraction)`
- [X] `PERCENTILE_DISC(expression, fraction)`

46
ext/stats/boolean.go Normal file
View File

@@ -0,0 +1,46 @@
package stats
import "github.com/ncruces/go-sqlite3"
const (
every = iota
some
)
func newBoolean(kind int) func() sqlite3.AggregateFunction {
return func() sqlite3.AggregateFunction { return &boolean{kind: kind} }
}
type boolean struct {
count int
total int
kind int
}
func (b *boolean) Value(ctx sqlite3.Context) {
if b.kind == every {
ctx.ResultBool(b.count == b.total)
} else {
ctx.ResultBool(b.count > 0)
}
}
func (b *boolean) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
a := arg[0]
if a.Bool() {
b.count++
}
if a.Type() != sqlite3.NULL {
b.total++
}
}
func (b *boolean) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
a := arg[0]
if a.Bool() {
b.count--
}
if a.Type() != sqlite3.NULL {
b.total--
}
}

71
ext/stats/boolean_test.go Normal file
View File

@@ -0,0 +1,71 @@
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_boolean(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
err = db.Exec(`CREATE TABLE data (x)`)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`INSERT INTO data (x) VALUES (4), (7.0), (13), (NULL), (16), (3.14)`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`
SELECT
every(x > 0),
every(x > 10),
some(x > 10),
some(x > 20)
FROM data`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnBool(0); got != true {
t.Errorf("got %v, want true", got)
}
if got := stmt.ColumnBool(1); got != false {
t.Errorf("got %v, want false", got)
}
if got := stmt.ColumnBool(2); got != true {
t.Errorf("got %v, want true", got)
}
if got := stmt.ColumnBool(3); got != false {
t.Errorf("got %v, want false", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`SELECT every(x > 10) OVER (ROWS 1 PRECEDING) FROM data`)
if err != nil {
t.Fatal(err)
}
want := [...]bool{false, false, false, true, true, false}
for i := 0; stmt.Step(); i++ {
if got := stmt.ColumnBool(0); got != want[i] {
t.Errorf("got %v, want %v", got, want[i])
}
if got := stmt.ColumnType(0); got != sqlite3.INTEGER {
t.Errorf("got %v, want INTEGER", got)
}
}
stmt.Close()
}

106
ext/stats/percentile.go Normal file
View File

@@ -0,0 +1,106 @@
package stats
import (
"encoding/json"
"fmt"
"math"
"slices"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/sort/quick"
)
const (
median = iota
percentile_100
percentile_cont
percentile_disc
)
func newPercentile(kind int) func() sqlite3.AggregateFunction {
return func() sqlite3.AggregateFunction { return &percentile{kind: kind} }
}
type percentile struct {
nums []float64
arg1 []byte
kind int
}
func (q *percentile) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
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 == nil {
q.arg1 = append(q.arg1, arg[1].RawText()...)
}
}
func (q *percentile) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
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) {
if len(q.nums) == 0 {
return
}
var (
err error
float float64
floats []float64
)
if q.kind == median {
float, err = q.at(0.5)
ctx.ResultFloat(float)
} else if err = json.Unmarshal(q.arg1, &float); err == nil {
float, err = q.at(float)
ctx.ResultFloat(float)
} else if err = json.Unmarshal(q.arg1, &floats); err == nil {
err = q.atMore(floats)
ctx.ResultJSON(floats)
}
if err != nil {
ctx.ResultError(fmt.Errorf("percentile: %w", err)) // notest
}
}
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(q.nums)-1))
m0 := quick.Select(q.nums, int(i))
if f == 0 || q.kind == percentile_disc {
return m0, nil
}
m1 := slices.Min(q.nums[int(i)+1:])
return util.Lerp(m0, m1, f), nil
}
func (q *percentile) atMore(pos []float64) error {
for i := range pos {
v, err := q.at(pos[i])
if err != nil {
return err
}
pos[i] = v
}
return nil
}

View File

@@ -0,0 +1,167 @@
package stats_test
import (
"slices"
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func TestRegister_percentile(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
err = db.Exec(`CREATE TABLE data (x)`)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`INSERT INTO data (x) VALUES (4), (7.0), ('13'), (NULL), (16)`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`
SELECT
median(x),
percentile(x, 50),
percentile_disc(x, 0.5),
percentile_cont(x, '[0.25, 0.5, 0.75]')
FROM data`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 10 {
t.Errorf("got %v, want 10", got)
}
if got := stmt.ColumnFloat(1); got != 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(3, &got); err != nil {
t.Error(err)
}
if !slices.Equal(got, []float64{6.25, 10, 13.75}) {
t.Errorf("got %v, want [6.25 10 13.75]", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`
SELECT
median(x) 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
WHERE x < 5`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 4 {
t.Errorf("got %v, want 4", got)
}
if got := stmt.ColumnFloat(1); got != 4 {
t.Errorf("got %v, want 4", got)
}
if got := stmt.ColumnFloat(2); got != 4 {
t.Errorf("got %v, want 4", got)
}
var got []float64
if err := stmt.ColumnJSON(3, &got); err != nil {
t.Error(err)
}
if !slices.Equal(got, []float64{4, 4, 4}) {
t.Errorf("got %v, want [4 4 4]", got)
}
}
stmt.Close()
stmt, _, err = db.Prepare(`
SELECT
median(x),
percentile(x, 50),
percentile_disc(x, 0.5),
percentile_cont(x, '[0.25, 0.5, 0.75]')
FROM data
WHERE x < 0`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != sqlite3.NULL {
t.Error("want NULL")
}
if got := stmt.ColumnType(1); got != sqlite3.NULL {
t.Error("want NULL")
}
if got := stmt.ColumnType(2); got != sqlite3.NULL {
t.Error("want NULL")
}
if got := stmt.ColumnType(3); got != sqlite3.NULL {
t.Error("want NULL")
}
}
stmt.Close()
stmt, _, err = db.Prepare(`
SELECT
percentile_disc(x, -2),
percentile_cont(x, +2),
percentile_cont(x, ''),
percentile_cont(x, '[100]')
FROM data`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
t.Error("want error")
}
stmt.Close()
}

View File

@@ -18,6 +18,11 @@
// - regr_slope: slope of the least-squares-fit linear equation
// - regr_intercept: y-intercept of the least-squares-fit linear equation
// - regr_json: all regr stats in a JSON object
// - percentile_disc: discrete percentile
// - percentile_cont: continuous percentile
// - median: median value
// - every: boolean and
// - some: boolean or
//
// These join the [Built-in Aggregate Functions]:
// - count: count rows/values
@@ -26,34 +31,53 @@
// - min: minimum value
// - max: maximum value
//
// And the [Built-in Window Functions]:
// - rank: rank of the current row with gaps
// - dense_rank: rank of the current row without gaps
// - percent_rank: relative rank of the row
// - cume_dist: cumulative distribution
//
// See: [ANSI SQL Aggregate Functions]
//
// [Built-in Aggregate Functions]: https://sqlite.org/lang_aggfunc.html
// [Built-in Window Functions]: https://sqlite.org/windowfunctions.html#builtins
// [ANSI SQL Aggregate Functions]: https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
package stats
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))
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("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)))
}
const (
@@ -99,14 +123,18 @@ func (fn *variance) Value(ctx sqlite3.Context) {
}
func (fn *variance) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
if a := arg[0]; a.NumericType() != sqlite3.NULL {
fn.enqueue(a.Float())
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
fn.enqueue(f)
}
}
func (fn *variance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
if a := arg[0]; a.NumericType() != sqlite3.NULL {
fn.dequeue(a.Float())
a := arg[0]
f := a.Float()
if f != 0.0 || a.NumericType() != sqlite3.NULL {
fn.dequeue(f)
}
}
@@ -155,15 +183,23 @@ func (fn *covariance) Value(ctx sqlite3.Context) {
}
func (fn *covariance) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
a, b := arg[0], arg[1]
if a.NumericType() != sqlite3.NULL && b.NumericType() != sqlite3.NULL {
fn.enqueue(a.Float(), b.Float())
b, a := arg[1], arg[0] // avoid a bounds check
fa := a.Float()
fb := b.Float()
if true &&
(fa != 0.0 || a.NumericType() != sqlite3.NULL) &&
(fb != 0.0 || b.NumericType() != sqlite3.NULL) {
fn.enqueue(fa, fb)
}
}
func (fn *covariance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
a, b := arg[0], arg[1]
if a.NumericType() != sqlite3.NULL && b.NumericType() != sqlite3.NULL {
fn.dequeue(a.Float(), b.Float())
b, a := arg[1], arg[0] // avoid a bounds check
fa := a.Float()
fb := b.Float()
if true &&
(fa != 0.0 || a.NumericType() != sqlite3.NULL) &&
(fb != 0.0 || b.NumericType() != sqlite3.NULL) {
fn.dequeue(fa, fb)
}
}

View File

@@ -7,8 +7,14 @@ import (
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/stats"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func TestMain(m *testing.M) {
sqlite3.AutoExtension(stats.Register)
m.Run()
}
func TestRegister_variance(t *testing.T) {
t.Parallel()
@@ -18,9 +24,7 @@ func TestRegister_variance(t *testing.T) {
}
defer db.Close()
stats.Register(db)
err = db.Exec(`CREATE TABLE IF NOT EXISTS data (x)`)
err = db.Exec(`CREATE TABLE data (x)`)
if err != nil {
t.Fatal(err)
}
@@ -39,8 +43,6 @@ func TestRegister_variance(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 40 {
t.Errorf("got %v, want 40", got)
@@ -61,24 +63,23 @@ func TestRegister_variance(t *testing.T) {
t.Errorf("got %v, want √22.5", got)
}
}
stmt.Close()
{
stmt, _, err := db.Prepare(`SELECT var_samp(x) OVER (ROWS 1 PRECEDING) FROM data`)
if err != nil {
t.Fatal(err)
stmt, _, err = db.Prepare(`SELECT var_samp(x) OVER (ROWS 1 PRECEDING) FROM data`)
if err != nil {
t.Fatal(err)
}
want := [...]float64{0, 4.5, 18, 0, 0}
for i := 0; stmt.Step(); i++ {
if got := stmt.ColumnFloat(0); got != want[i] {
t.Errorf("got %v, want %v", got, want[i])
}
defer stmt.Close()
want := [...]float64{0, 4.5, 18, 0, 0}
for i := 0; stmt.Step(); i++ {
if got := stmt.ColumnFloat(0); got != want[i] {
t.Errorf("got %v, want %v", got, want[i])
}
if got := stmt.ColumnType(0); (got == sqlite3.FLOAT) != (want[i] != 0) {
t.Errorf("got %v, want %v", got, want[i])
}
if got := stmt.ColumnType(0); (got == sqlite3.FLOAT) != (want[i] != 0) {
t.Errorf("got %v, want %v", got, want[i])
}
}
stmt.Close()
}
func TestRegister_covariance(t *testing.T) {
@@ -90,9 +91,7 @@ func TestRegister_covariance(t *testing.T) {
}
defer db.Close()
stats.Register(db)
err = db.Exec(`CREATE TABLE IF NOT EXISTS data (y, x)`)
err = db.Exec(`CREATE TABLE data (y, x)`)
if err != nil {
t.Fatal(err)
}
@@ -112,8 +111,6 @@ func TestRegister_covariance(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 0.9881049293224639 {
t.Errorf("got %v, want 0.9881049293224639", got)
@@ -158,27 +155,29 @@ func TestRegister_covariance(t *testing.T) {
t.Errorf("got %v, want 5", got)
}
}
stmt.Close()
{
stmt, _, err := db.Prepare(`SELECT covar_samp(y, x) OVER (ROWS 1 PRECEDING) FROM data`)
if err != nil {
t.Fatal(err)
stmt, _, err = db.Prepare(`SELECT covar_samp(y, x) OVER (ROWS 1 PRECEDING) FROM data`)
if err != nil {
t.Fatal(err)
}
want := [...]float64{0, 10, 30, 75, 22.5}
for i := 0; stmt.Step(); i++ {
if got := stmt.ColumnFloat(0); got != want[i] {
t.Errorf("got %v, want %v", got, want[i])
}
defer stmt.Close()
want := [...]float64{0, 10, 30, 75, 22.5}
for i := 0; stmt.Step(); i++ {
if got := stmt.ColumnFloat(0); got != want[i] {
t.Errorf("got %v, want %v", got, want[i])
}
if got := stmt.ColumnType(0); (got == sqlite3.FLOAT) != (want[i] != 0) {
t.Errorf("got %v, want %v", got, want[i])
}
if got := stmt.ColumnType(0); (got == sqlite3.FLOAT) != (want[i] != 0) {
t.Errorf("got %v, want %v", got, want[i])
}
}
stmt.Close()
}
func Benchmark_average(b *testing.B) {
sqlite3.Initialize()
b.ResetTimer()
db, err := sqlite3.Open(":memory:")
if err != nil {
b.Fatal(err)
@@ -210,14 +209,15 @@ func Benchmark_average(b *testing.B) {
}
func Benchmark_variance(b *testing.B) {
sqlite3.Initialize()
b.ResetTimer()
db, err := sqlite3.Open(":memory:")
if err != nil {
b.Fatal(err)
}
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

@@ -5,6 +5,10 @@
// - LIKE and REGEXP operators,
// - collation sequences.
//
// It also provides, from PostgreSQL:
// - unaccent(),
// - initcap().
//
// The implementation is not 100% compatible with the [ICU extension]:
// - upper() and lower() use [strings.ToUpper], [strings.ToLower] and [cases];
// - the LIKE operator follows [strings.EqualFold] rules;
@@ -18,41 +22,61 @@ 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 errs util.ErrorJoiner
if RegisterLike {
errs.Join(
db.CreateFunction("like", 2, flags, like),
db.CreateFunction("like", 3, flags, like))
}
errs.Join(
db.CreateFunction("upper", 1, flags, upper),
db.CreateFunction("upper", 2, flags, upper),
db.CreateFunction("lower", 1, flags, lower),
db.CreateFunction("lower", 2, flags, lower),
db.CreateFunction("regexp", 2, flags, regex),
db.CreateFunction("initcap", 1, flags, initcap),
db.CreateFunction("initcap", 2, flags, initcap),
db.CreateFunction("unaccent", 1, flags, unaccent),
db.CreateFunction("icu_load_collation", 2, sqlite3.DIRECTONLY,
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
name := arg[1].Text()
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
}
}))
return errors.Join(errs...)
}
// RegisterCollation registers a Unicode collation sequence for a database connection.
@@ -64,6 +88,15 @@ func RegisterCollation(db *sqlite3.Conn, locale, name string) error {
return db.CreateCollation(name, collate.New(tag).Compare)
}
// RegisterCollationsNeeded registers Unicode collation sequences on demand for a database connection.
func RegisterCollationsNeeded(db *sqlite3.Conn) error {
return db.CollationNeeded(func(db *sqlite3.Conn, name string) {
if tag, err := language.Parse(name); err == nil {
db.CreateCollation(name, collate.New(tag).Compare)
}
})
}
func upper(ctx sqlite3.Context, arg ...sqlite3.Value) {
if len(arg) == 1 {
ctx.ResultRawText(bytes.ToUpper(arg[0].RawText()))
@@ -74,7 +107,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 +126,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 +135,45 @@ func lower(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
}
func initcap(ctx sqlite3.Context, arg ...sqlite3.Value) {
if len(arg) == 1 {
ctx.ResultRawText(bytes.Title(arg[0].RawText()))
return
}
cs, ok := ctx.GetAuxData(1).(cases.Caser)
if !ok {
t, err := language.Parse(arg[1].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
c := cases.Title(t)
ctx.SetAuxData(1, c)
cs = c
}
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
}
func unaccent(ctx sqlite3.Context, arg ...sqlite3.Value) {
unaccent := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
res, _, err := transform.Bytes(unaccent, arg[0].RawText())
if err != nil {
ctx.ResultError(err) // notest
} else {
ctx.ResultRawText(res)
}
}
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, ok := ctx.GetAuxData(0).(*regexp.Regexp)
if !ok {
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()))
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func TestRegister(t *testing.T) {
@@ -46,6 +47,9 @@ func TestRegister(t *testing.T) {
{`upper('istanbul', 'tr-TR')`, "İSTANBUL"},
{`lower('Dünyanın İlk Borsası', 'tr-TR')`, "dünyanın ilk borsası"},
{`upper('Dünyanın İlk Borsası', 'tr-TR')`, "DÜNYANIN İLK BORSASI"},
{`initcap('Kad je hladno Marko nosi džemper')`, "Kad Je Hladno Marko Nosi Džemper"},
{`initcap('Kad je hladno Marko nosi džemper', 'hr-HR')`, "Kad Je Hladno Marko Nosi Džemper"},
{`unaccent('Hôtel')`, "Hotel"},
{`'Hello' REGEXP 'ell'`, "1"},
{`'Hello' REGEXP 'el.'`, "1"},
{`'Hello' LIKE 'hel_'`, "0"},
@@ -81,7 +85,7 @@ func TestRegister_collation(t *testing.T) {
Register(db)
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
if err != nil {
t.Fatal(err)
}
@@ -91,7 +95,7 @@ func TestRegister_collation(t *testing.T) {
t.Fatal(err)
}
err = db.Exec(`SELECT icu_load_collation('fr_FR', 'french')`)
err = db.Exec(`SELECT icu_load_collation('fr-FR', 'french')`)
if err != nil {
t.Fatal(err)
}
@@ -126,6 +130,57 @@ func TestRegister_collation(t *testing.T) {
}
}
func TestRegisterCollationsNeeded(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
RegisterCollationsNeeded(db)
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
if err != nil {
t.Fatal(err)
}
err = db.Exec(`INSERT INTO words (word) VALUES ('côte'), ('cote'), ('coter'), ('coté'), ('cotée'), ('côté')`)
if err != nil {
t.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT word FROM words ORDER BY word COLLATE fr_FR`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
got, want := []string{}, []string{"cote", "coté", "côte", "côté", "cotée", "coter"}
for stmt.Step() {
got = append(got, stmt.ColumnText(0))
}
if err := stmt.Err(); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, want) {
t.Error("not equal")
}
err = stmt.Close()
if err != nil {
t.Fatal(err)
}
err = db.Close()
if err != nil {
t.Fatal(err)
}
}
func TestRegister_error(t *testing.T) {
t.Parallel()

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

@@ -0,0 +1,169 @@
// Package uuid provides functions to generate RFC 4122 UUIDs.
//
// https://sqlite.org/src/file/ext/misc/uuid.c
package uuid
import (
"bytes"
"errors"
"fmt"
"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))
}
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())
}
}

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

@@ -0,0 +1,181 @@
package uuid
import (
"testing"
"github.com/google/uuid"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"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")
}
hash := []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 hash {
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
lits := []string{
"'6ba7b8119dad11d180b400c04fd430c8'",
"'6ba7b811-9dad-11d1-80b4-00c04fd430c8'",
"'{6ba7b811-9dad-11d1-80b4-00c04fd430c8}'",
"X'6ba7b8119dad11d180b400c04fd430c8'",
}
for _, tt := range lits {
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 lits {
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")
}
}

61
ext/zorder/zorder.go Normal file
View File

@@ -0,0 +1,61 @@
// Package zorder provides functions for z-order transformations.
//
// https://sqlite.org/src/doc/tip/ext/misc/zorder.c
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) 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
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 := range x {
j := i % len(arg)
z |= (x[j] & 1) << i
x[j] >>= 1
}
}
for i := range arg {
if x[i] != 0 {
ctx.ResultError(util.ErrorString("zorder: parameter too large"))
return
}
}
ctx.ResultInt64(z)
}
func unzorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
i := arg[2].Int64()
n := arg[1].Int64()
z := arg[0].Int64()
var k int
var x int64
for j := i; j < 63; j += n {
x |= ((z >> j) & 1) << k
k++
}
ctx.ResultInt64(x)
}

115
ext/zorder/zorder_test.go Normal file
View File

@@ -0,0 +1,115 @@
package zorder_test
import (
"strconv"
"strings"
"testing"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/zorder"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func TestRegister_zorder(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(tmp, zorder.Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var got int64
err = db.QueryRow(`SELECT zorder(2, 3)`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != 14 {
t.Errorf("got %d, want 14", got)
}
err = db.QueryRow(`SELECT zorder(4, 5)`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != 50 {
t.Errorf("got %d, want 14", got)
}
var check bool
err = db.QueryRow(`SELECT zorder(3, 4) BETWEEN zorder(2, 3) AND zorder(4, 5)`).Scan(&check)
if err != nil {
t.Fatal(err)
}
if !check {
t.Error("want true")
}
err = db.QueryRow(`SELECT zorder(2, 2) NOT BETWEEN zorder(2, 3) AND zorder(4, 5)`).Scan(&check)
if err != nil {
t.Fatal(err)
}
if !check {
t.Error("want true")
}
}
func TestRegister_unzorder(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(tmp, zorder.Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var got int64
err = db.QueryRow(`SELECT unzorder(zorder(3, 4), 2, 0)`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != 3 {
t.Errorf("got %d, want 3", got)
}
err = db.QueryRow(`SELECT unzorder(zorder(3, 4), 2, 1)`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != 4 {
t.Errorf("got %d, want 4", got)
}
}
func TestRegister_error(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
db, err := driver.Open(tmp, zorder.Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var got int64
err = db.QueryRow(`SELECT zorder(1, 2, 3, 100000)`).Scan(&got)
if err == nil {
t.Error("want error")
}
var buf strings.Builder
buf.WriteString("SELECT zorder(0")
for i := 1; i < 80; i++ {
buf.WriteByte(',')
buf.WriteString(strconv.Itoa(0))
}
buf.WriteByte(')')
err = db.QueryRow(buf.String()).Scan(&got)
if err == nil {
t.Error("want error")
}
}

34
func.go
View File

@@ -4,8 +4,9 @@ import (
"context"
"sync"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/util"
)
// CollationNeeded registers a callback to be invoked
@@ -31,17 +32,25 @@ func (c *Conn) CollationNeeded(cb func(db *Conn, name string)) error {
//
// This can be used to load schemas that contain
// one or more unknown collating sequences.
func (c *Conn) AnyCollationNeeded() {
c.call("sqlite3_anycollseq_init", uint64(c.handle), 0, 0)
func (c Conn) AnyCollationNeeded() error {
r := c.call("sqlite3_anycollseq_init", uint64(c.handle), 0, 0)
if err := c.error(r); err != nil {
return err
}
c.collation = nil
return nil
}
// CreateCollation defines a new collating sequence.
//
// https://sqlite.org/c3ref/create_collation.html
func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
var funcPtr uint32
defer c.arena.mark()()
namePtr := c.arena.string(name)
funcPtr := util.AddHandle(c.ctx, fn)
if fn != nil {
funcPtr = util.AddHandle(c.ctx, fn)
}
r := c.call("sqlite3_create_collation_go",
uint64(c.handle), uint64(namePtr), uint64(funcPtr))
return c.error(r)
@@ -51,9 +60,12 @@ func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
//
// https://sqlite.org/c3ref/create_function.html
func (c *Conn) CreateFunction(name string, nArg int, flag FunctionFlag, fn ScalarFunction) error {
var funcPtr uint32
defer c.arena.mark()()
namePtr := c.arena.string(name)
funcPtr := util.AddHandle(c.ctx, fn)
if fn != nil {
funcPtr = util.AddHandle(c.ctx, fn)
}
r := c.call("sqlite3_create_function_go",
uint64(c.handle), uint64(namePtr), uint64(nArg),
uint64(flag), uint64(funcPtr))
@@ -70,10 +82,13 @@ type ScalarFunction func(ctx Context, arg ...Value)
//
// https://sqlite.org/c3ref/create_function.html
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn func() AggregateFunction) error {
var funcPtr uint32
defer c.arena.mark()()
call := "sqlite3_create_aggregate_function_go"
namePtr := c.arena.string(name)
funcPtr := util.AddHandle(c.ctx, fn)
if fn != nil {
funcPtr = util.AddHandle(c.ctx, fn)
}
call := "sqlite3_create_aggregate_function_go"
if _, ok := fn().(WindowFunction); ok {
call = "sqlite3_create_window_function_go"
}
@@ -183,11 +198,12 @@ func callbackAggregate(db *Conn, pAgg, pApp uint32) (AggregateFunction, uint32)
// We need to create the aggregate.
fn := util.GetHandle(db.ctx, pApp).(func() AggregateFunction)()
handle := util.AddHandle(db.ctx, fn)
if pAgg != 0 {
handle := util.AddHandle(db.ctx, fn)
util.WriteUint32(db.mod, pAgg, handle)
return fn, handle
}
return fn, handle
return fn, 0
}
func callbackArgs(db *Conn, arg []Value, pArg uint32) {

View File

@@ -9,6 +9,7 @@ import (
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/unicode"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func ExampleConn_CreateCollation() {
@@ -18,7 +19,7 @@ func ExampleConn_CreateCollation() {
}
defer db.Close()
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
if err != nil {
log.Fatal(err)
}
@@ -66,7 +67,7 @@ func ExampleConn_CreateFunction() {
}
defer db.Close()
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
if err != nil {
log.Fatal(err)
}
@@ -111,7 +112,7 @@ func ExampleContext_SetAuxData() {
}
defer db.Close()
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
if err != nil {
log.Fatal(err)
}
@@ -129,8 +130,8 @@ func ExampleContext_SetAuxData() {
ctx.ResultError(err)
return
}
ctx.SetAuxData(0, r)
re = r
ctx.SetAuxData(0, r)
}
ctx.ResultBool(re.Match(arg[1].RawText()))
})

View File

@@ -16,7 +16,7 @@ func ExampleConn_CreateWindowFunction() {
}
defer db.Close()
err = db.Exec(`CREATE TABLE IF NOT EXISTS words (word VARCHAR(10))`)
err = db.Exec(`CREATE TABLE words (word VARCHAR(10))`)
if err != nil {
log.Fatal(err)
}
@@ -78,7 +78,7 @@ func (f *countASCII) isASCII(arg sqlite3.Value) bool {
if arg.Type() != sqlite3.TEXT {
return false
}
for _, c := range arg.RawBlob() {
for _, c := range arg.RawText() {
if c > unicode.MaxASCII {
return false
}

21
go.mod
View File

@@ -2,14 +2,23 @@ module github.com/ncruces/go-sqlite3
go 1.21
toolchain go1.23.0
require (
github.com/ncruces/julianday v1.0.0
github.com/psanford/httpreadat v0.1.0
github.com/tetratelabs/wazero v1.6.0
golang.org/x/crypto v0.18.0
golang.org/x/sync v0.6.0
golang.org/x/sys v0.16.0
golang.org/x/text v0.14.0
github.com/ncruces/sort v0.1.2
github.com/tetratelabs/wazero v1.8.2
golang.org/x/crypto v0.31.0
golang.org/x/sys v0.28.0
)
require (
github.com/dchest/siphash v1.2.3 // ext/bloom
github.com/google/uuid v1.6.0 // ext/uuid
github.com/psanford/httpreadat v0.1.0 // example
golang.org/x/sync v0.10.0 // test
golang.org/x/text v0.21.0 // ext/unicode
lukechampine.com/adiantum v1.1.1 // vfs/adiantum
)
retract v0.4.0 // tagged from the wrong branch

28
go.sum
View File

@@ -1,14 +1,22 @@
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/ncruces/sort v0.1.2 h1:zKQ9CA4fpHPF6xsUhRTfi5EEryspuBpe/QA4VWQOV1U=
github.com/ncruces/sort v0.1.2/go.mod h1:vEJUTBJtebIuCMmXD18GKo5GJGhsay+xZFOoBEIXFmE=
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g=
github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=

View File

@@ -3,4 +3,5 @@ go 1.21
use (
.
./gormlite
./embed/bcw2
)

View File

@@ -1,3 +1,16 @@
github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=

View File

@@ -13,6 +13,7 @@ import (
var (
sqliteSeparator = "`|\"|'|\t"
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^CONSTRAINT [%v]?[\w-]+[%v]? UNIQUE (.*)$`, sqliteSeparator, sqliteSeparator))
indexRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)CREATE(?: UNIQUE)? INDEX [%v]?[\w\d-]+[%v]?(?s:.*?)ON (.*)$`, sqliteSeparator, sqliteSeparator))
tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?`, sqliteSeparator, sqliteSeparator))
separatorRegexp = regexp.MustCompile(fmt.Sprintf("[%v]", sqliteSeparator))
@@ -103,11 +104,24 @@ func parseDDL(strs ...string) (*ddl, error) {
for _, f := range result.fields {
fUpper := strings.ToUpper(f)
if strings.HasPrefix(fUpper, "CHECK") ||
strings.HasPrefix(fUpper, "CONSTRAINT") {
if strings.HasPrefix(fUpper, "CHECK") {
continue
}
if strings.HasPrefix(fUpper, "CONSTRAINT") {
matches := uniqueRegexp.FindStringSubmatch(f)
if len(matches) > 0 {
if columns := getAllColumns(matches[1]); len(columns) == 1 {
for idx, column := range result.columns {
if column.NameValue.String == columns[0] {
column.UniqueValue = sql.NullBool{Bool: true, Valid: true}
result.columns[idx] = column
break
}
}
}
}
continue
}
if strings.HasPrefix(fUpper, "PRIMARY KEY") {
for _, name := range getAllColumns(f) {
for idx, column := range result.columns {
@@ -159,14 +173,7 @@ func parseDDL(strs ...string) (*ddl, error) {
}
}
} else if matches := indexRegexp.FindStringSubmatch(str); len(matches) > 0 {
for _, column := range getAllColumns(matches[1]) {
for idx, c := range result.columns {
if c.NameValue.String == column {
c.UniqueValue = sql.NullBool{Bool: strings.ToUpper(strings.Fields(str)[1]) == "UNIQUE", Valid: true}
result.columns[idx] = c
}
}
}
// don't report Unique by UniqueIndex
} else {
return nil, errors.New("invalid DDL")
}
@@ -269,20 +276,6 @@ func (d *ddl) getColumns() []string {
return res
}
func (d *ddl) alterColumn(name, sql string) bool {
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
for i := 0; i < len(d.fields); i++ {
if reg.MatchString(d.fields[i]) {
d.fields[i] = sql
return false
}
}
d.fields = append(d.fields, sql)
return true
}
func (d *ddl) removeColumn(name string) bool {
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")

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