Compare commits

...

166 Commits

Author SHA1 Message Date
Nuno Cruces
99b097de3b Windows ARM runners. 2025-11-09 12:44:32 +00:00
Nuno Cruces
4a956e80a2 wasi-sdk-28. 2025-11-09 01:32:25 +00:00
Nuno Cruces
5f4ff03f6f wazero v1.10.0. 2025-11-09 01:32:15 +00:00
Nuno Cruces
5890049488 Shim modernc. 2025-11-09 01:32:14 +00:00
Nuno Cruces
5e73c5d714 Issue #330. 2025-11-06 12:07:37 +00:00
Nuno Cruces
6d92aa16ef SQLite 3.51.0. 2025-11-05 12:30:09 +00:00
Nuno Cruces
191d1337e7 Gorm v1.31.1. 2025-11-05 12:30:09 +00:00
Nuno Cruces
b65e894849 Improve error reporting. (#327) 2025-10-17 16:40:15 +01:00
Nuno Cruces
0b040d3f09 Prepare 3.51.0. 2025-10-16 15:18:44 +01:00
Nuno Cruces
1db4366226 VFS error handling. 2025-10-16 15:18:40 +01:00
Nuno Cruces
9e1cbfb5bb Release snapshot. 2025-10-10 17:37:44 +01:00
dependabot[bot]
7f2d70a0f3 Bump golang.org/x/crypto from 0.42.0 to 0.43.0 (#324)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.42.0 to 0.43.0.
- [Commits](https://github.com/golang/crypto/compare/v0.42.0...v0.43.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 23:11:06 +01:00
Nuno Cruces
ea860e407d Docs. 2025-10-01 11:00:13 +01:00
Nuno Cruces
d4561d08f9 Refactor. 2025-10-01 10:48:54 +01:00
Nuno Cruces
14c1e490b4 Optimize fullfsync. 2025-09-30 12:54:18 +01:00
Nuno Cruces
23aad5f62f MVCC API. 2025-09-29 12:52:01 +01:00
Nuno Cruces
e5bd10a1ff Fix TestDB. 2025-09-29 12:44:09 +01:00
Nuno Cruces
5cf06c45f7 Scan improvements. 2025-09-24 18:13:26 +01:00
Nuno Cruces
08f9fc758a JSON experiment. 2025-09-24 18:13:21 +01:00
Nuno Cruces
b588d5f991 Error messages, test contexts. 2025-09-23 12:51:36 +01:00
Nuno Cruces
4c24bd0cb6 Verify download. 2025-09-23 11:49:59 +01:00
Nuno Cruces
cc353e4848 Time improvements. 2025-09-23 11:49:53 +01:00
Nuno Cruces
c3ebb04045 Use crypto/pbkdf2. 2025-09-18 18:41:10 +01:00
Nuno Cruces
11e064574c Weight-balanced trees. 2025-09-17 11:01:24 +01:00
Nuno Cruces
770420289a Updated dependencies. 2025-09-10 17:09:00 +01:00
Nuno Cruces
62f69011f1 Updated dependencies. 2025-09-08 13:59:41 +01:00
Nuno Cruces
4f9e3f900b binaryen-version_124. 2025-09-08 12:23:58 +01:00
dependabot[bot]
4e90618350 Bump actions/setup-go from 5 to 6 (#318)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  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>
2025-09-05 00:25:35 +02:00
Nuno Cruces
54bb94ce58 Improve WithMemoryCapacityFromMax (#317). 2025-09-03 09:03:59 +02:00
Nuno Cruces
07fec784e1 Grow memory geometrically. (#316) 2025-09-01 12:57:33 +02:00
dependabot[bot]
da4638cbff Bump actions/attest-build-provenance from 2 to 3 (#313)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2 to 3.
- [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/v2...v3)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: '3'
  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>
2025-08-29 00:19:53 +02:00
dependabot[bot]
085872c2f3 Bump github.com/ncruces/aa from 0.3.1 to 0.3.2 (#311)
Bumps [github.com/ncruces/aa](https://github.com/ncruces/aa) from 0.3.1 to 0.3.2.
- [Commits](https://github.com/ncruces/aa/compare/v0.3.1...v0.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 17:20:13 +02:00
dependabot[bot]
de49aa2b06 Bump github.com/ncruces/aa from 0.3.0 to 0.3.1 (#310)
Bumps [github.com/ncruces/aa](https://github.com/ncruces/aa) from 0.3.0 to 0.3.1.
- [Commits](https://github.com/ncruces/aa/compare/v0.3.0...v0.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-23 05:21:11 +01:00
Nuno Cruces
1f3ad0165e SQLite 3.50.4. 2025-08-21 19:05:44 +01:00
Nuno Cruces
0bda48d1d9 Gorm v1.30.1. 2025-08-21 18:56:05 +01:00
Nuno Cruces
0026bc91aa MVCC memory VFS. (#309) 2025-08-21 18:44:40 +01:00
Nuno Cruces
d84ca9d627 Fix #308. 2025-08-16 19:45:10 +01:00
Nuno Cruces
5d14e01f94 Fix #304. 2025-08-16 19:27:00 +01:00
Nuno Cruces
342df983d4 Fix #305. 2025-08-14 23:46:48 +01:00
Nuno Cruces
00476fb1e2 Tests. 2025-08-14 15:04:10 +01:00
Nuno Cruces
8a64ee6eaa Implement RowsColumnScanner. 2025-08-14 01:42:00 +01:00
Nuno Cruces
8f9a8e2752 Learnings from truffle. 2025-08-13 13:10:50 +01:00
Nuno Cruces
d8880e4cee Fixes. 2025-08-13 03:34:21 +01:00
dependabot[bot]
4b154a842c Bump actions/checkout from 4 to 5 (#303)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  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>
2025-08-12 08:47:41 +01:00
dependabot[bot]
758a53e9bf Bump golang.org/x/crypto from 0.40.0 to 0.41.0 (#300)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/crypto/compare/v0.40.0...v0.41.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-08 00:30:25 +01:00
Nuno Cruces
1a42b4c590 Better fuzzing. 2025-08-07 16:27:45 +01:00
Nuno Cruces
7e4ec1df1c Fix #299. 2025-08-07 01:52:01 +01:00
Nuno Cruces
2c582a1d66 VFS improvements. 2025-08-05 14:15:21 +01:00
Nuno Cruces
20a67ca669 WAL mode serdes. 2025-08-02 11:48:37 +01:00
Nuno Cruces
789e2dc136 wasi-sdk-27. 2025-07-29 16:50:07 +01:00
Nuno Cruces
0399f10c06 VFS improvements. 2025-07-23 09:57:53 +01:00
Nuno Cruces
75c6744b5b FreeBSD 14.3. 2025-07-22 23:47:22 +01:00
Nuno Cruces
754e806164 Tests. 2025-07-22 10:34:30 +01:00
Nuno Cruces
2640c9fb54 SQLite 3.50.3. 2025-07-17 19:42:01 +01:00
Nuno Cruces
9719d4b0e3 Better tests. 2025-07-17 01:11:16 +01:00
Nuno Cruces
b21c69dc1f Fix mode. 2025-07-17 00:50:39 +01:00
Nuno Cruces
b0f8ff44a5 Support subtype. 2025-07-17 00:47:04 +01:00
Nuno Cruces
f37bca6a80 Speedup memdb. 2025-07-15 23:08:41 +01:00
Nuno Cruces
b4e8fcb752 Avoid UB. 2025-07-15 15:52:39 +01:00
dependabot[bot]
14b98a5d05 Bump golang.org/x/crypto from 0.39.0 to 0.40.0 (#295)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.39.0 to 0.40.0.
- [Commits](https://github.com/golang/crypto/compare/v0.39.0...v0.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-11 00:30:01 +01:00
Nuno Cruces
36a62264f9 Remove unneeded generics. 2025-07-10 13:05:55 +01:00
Nuno Cruces
33ea564f38 Deps. 2025-07-10 00:50:14 +01:00
Nuno Cruces
5c55d8692f Fix bitset. 2025-07-04 17:10:40 +01:00
Nuno Cruces
be2f3036b4 SQLite 3.50.2. 2025-06-30 12:29:54 +01:00
Nuno Cruces
784f82f42f Avoid UB. 2025-06-25 15:27:11 +01:00
Nuno Cruces
cd6ba43e77 Less SIMD. 2025-06-24 02:23:54 +01:00
Nuno Cruces
d7aef63844 Naming, volatile. 2025-06-20 12:45:42 +01:00
Nuno Cruces
64e5046f10 Improved byteset search. 2025-06-17 11:36:53 +01:00
Nuno Cruces
0bdce8aa68 Avoid overflow. 2025-06-12 15:12:20 +01:00
Nuno Cruces
69a2881a10 SQLite 3.50.1. 2025-06-08 00:38:01 +01:00
dependabot[bot]
24ad4445f1 Bump golang.org/x/crypto from 0.38.0 to 0.39.0 (#285)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.38.0 to 0.39.0.
- [Commits](https://github.com/golang/crypto/compare/v0.38.0...v0.39.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-05 23:49:59 +01:00
Nuno Cruces
c159bbd88f Docs, tweaks. 2025-06-04 12:19:01 +01:00
Nuno Cruces
c90f8205f7 Remove. 2025-06-03 12:54:56 +01:00
Nuno Cruces
b64b9b0415 Better strcasestr. 2025-06-02 10:25:10 +01:00
Nuno Cruces
9142e19d61 SQLite 3.50.0. 2025-05-31 01:14:16 +01:00
Daenney
4a76f2b064 README: Make the testing section simpler to read (#282) 2025-05-30 14:29:34 +01:00
Nuno Cruces
c9b364507e Avoid copy, alloc. 2025-05-27 11:35:43 +01:00
Nuno Cruces
2204b96ff6 Gorm. 2025-05-27 10:51:42 +01:00
Nuno Cruces
b46f480d79 FCNTL_NULL_IO. 2025-05-26 16:32:52 +01:00
Nuno Cruces
040a026925 More examples. 2025-05-26 12:26:14 +01:00
Michael Lynch
e678040a4e Remove duplicate link to 'custom VFSes' in README (#279) 2025-05-26 11:49:03 +01:00
Michael Lynch
f1cc12569c Link to example of incremental BLOB I/O usage (#280) 2025-05-26 11:48:53 +01:00
Nuno Cruces
721a987e0e Line numbers. 2025-05-26 11:48:02 +01:00
Michael Lynch
f3d65142cc Use sql.Named to clarify the blobio example (#281)
I find the example SQL queries a bit difficult to read at the callsite with the magic numbers and ? placeholders. I think sql.Named makes it more obvious to the reader what the different parts of the SQL query represent.
2025-05-25 19:02:19 +01:00
Nuno Cruces
93f711c77b More fuzzing. 2025-05-21 11:36:56 +01:00
Nuno Cruces
341bd063e8 More fuzzing. 2025-05-21 08:16:52 +01:00
Nuno Cruces
f765882670 Sunday's Quick Search. 2025-05-17 13:20:11 +01:00
Nuno Cruces
ff3676ff4a Case insensitive search, fixes. 2025-05-16 13:10:03 +01:00
Nuno Cruces
54877a53cd Differential fuzzing. 2025-05-15 14:55:34 +01:00
Nuno Cruces
fccc6c10a7 Issue #277. 2025-05-14 13:46:01 +01:00
Nuno Cruces
fc21ffcc71 Tweaks. 2025-05-14 01:10:44 +01:00
Nuno Cruces
687e643d7a Case insensitive compare. 2025-05-13 18:12:14 +01:00
Nuno Cruces
fc5ced209c Fix bcmp. 2025-05-13 14:28:21 +01:00
Nuno Cruces
c1bed07e3a Issue #276. 2025-05-13 14:27:04 +01:00
Nuno Cruces
a0771f2363 Relaxed SIMD. 2025-05-12 17:30:32 +01:00
Nuno Cruces
6bad547d3d Unknown haystack length. 2025-05-12 12:00:06 +01:00
Nuno Cruces
c2c1aea578 Handle repetitive needles. 2025-05-10 01:44:25 +01:00
Nuno Cruces
60ab485b29 Comments. 2025-05-09 14:21:23 +01:00
Nuno Cruces
e17a432fde Adds strstr and memmem. (#275) 2025-05-09 00:59:39 +01:00
Nuno Cruces
c780ef16e2 SQLite 3.49.2. 2025-05-07 14:08:18 +01:00
Nuno Cruces
b609930142 Refactor #274. 2025-05-07 12:46:13 +01:00
Nuno Cruces
fd165ce724 Issue #274. 2025-05-07 01:37:52 +01:00
Nuno Cruces
d3973b23e3 More memcmp. 2025-05-06 15:48:58 +01:00
Nuno Cruces
320b68e74f More tests. 2025-05-05 14:47:43 +01:00
Nuno Cruces
2c3850e5d1 Reuse fast funcs. 2025-05-03 01:18:10 +01:00
Nuno Cruces
db7aacff9f Add strrchr. 2025-05-02 14:35:14 +01:00
Nuno Cruces
d748d98e39 Fix. 2025-05-01 12:49:38 +01:00
Nuno Cruces
13b8642384 Compile as C++. 2025-04-29 14:03:59 +01:00
Nuno Cruces
29c5c816cb More libc. 2025-04-27 23:35:13 +01:00
Nuno Cruces
b32db76da6 Fix flake. 2025-04-25 00:40:52 +01:00
Nuno Cruces
383f620a1e Action permissions. 2025-04-25 00:28:03 +01:00
Nuno Cruces
a3c3515e96 Update binaries. 2025-04-25 00:20:26 +01:00
Nuno Cruces
e580f080b9 Test libc. 2025-04-24 23:59:53 +01:00
Nuno Cruces
9ea7099c24 Size optimized versions. 2025-04-24 23:17:30 +01:00
Nuno Cruces
29aa365806 Fix. 2025-04-24 00:41:48 +01:00
Nuno Cruces
bb87a920f7 Fix memchr. 2025-04-22 02:27:50 +01:00
Nuno Cruces
48379336dc Improve strspn. 2025-04-21 18:59:20 +01:00
Nuno Cruces
251a92fa1a Weak symbols. 2025-04-18 14:50:57 +01:00
Nuno Cruces
f5206ea8da Shellsort. 2025-04-18 09:56:18 +01:00
Nuno Cruces
68ef4593d6 Make libc easier to use. 2025-04-17 13:55:44 +01:00
Nuno Cruces
79bf171210 Fix #263. 2025-04-16 16:39:36 +01:00
Nuno Cruces
ad16d329ea Optimize strlen and strchr on ARM (#262) 2025-04-15 00:44:31 +01:00
Nuno Cruces
9706fa9607 Benchmark more CPUs 2025-04-13 13:28:36 +01:00
Nuno Cruces
45494f5fb6 Redundant defers. 2025-04-09 10:21:54 +01:00
Nuno Cruces
1b0bf3495e Fix flake. 2025-04-09 10:21:54 +01:00
Nuno Cruces
73ac7e06f6 Use SIMD libc. 2025-04-09 10:21:44 +01:00
Nuno Cruces
a3ce8f9de5 More. 2025-04-07 01:32:15 +01:00
Nuno Cruces
2043d5fca4 Benchmark. 2025-04-06 23:24:34 +01:00
Nuno Cruces
3bd11a0a86 More SIMD. 2025-04-05 11:16:47 +01:00
Nuno Cruces
39f3fa64eb More SIMD. 2025-04-05 02:03:31 +01:00
Nuno Cruces
4c19387535 SIMD. 2025-04-04 17:47:20 +01:00
Nuno Cruces
e6c9f18934 Benchmark libc. 2025-04-04 10:56:12 +01:00
Nuno Cruces
970eb6a2f9 Fix. 2025-04-02 15:33:21 +01:00
Nuno Cruces
fac27b8bab libc. 2025-04-02 11:55:20 +01:00
Nuno Cruces
9f626b2f52 Fix #255. 2025-03-31 16:33:31 +01:00
Nuno Cruces
1f5d8bf7df Avoid escaping times (#256) 2025-03-31 13:02:41 +01:00
Nuno Cruces
41dc46af7e Optimize errors a bit. 2025-03-28 17:01:04 +00:00
Nuno Cruces
e5c285b783 Discussion #250. 2025-03-28 11:30:47 +00:00
Nuno Cruces
6290a14990 Fix interrupt race. 2025-03-26 19:02:14 +00:00
Nuno Cruces
948641194b Rework context cancellation. (#251) 2025-03-26 11:39:06 +00:00
Nuno Cruces
befed7cf23 binaryen-version_123. 2025-03-26 11:25:54 +00:00
Nuno Cruces
3547d9ffb0 Fix WAL flakiness on Windows (#254) 2025-03-26 10:17:19 +00:00
Nuno Cruces
a67165eb09 Fix WAL flakiness on Windows (#253) 2025-03-26 02:13:30 +00:00
Nuno Cruces
0ba393199a Mitigate #252. 2025-03-25 23:36:57 +00:00
Nuno Cruces
9e4258bc46 Better fix #249. 2025-03-25 12:45:27 +00:00
Nuno Cruces
b645721d10 IP/CIDR functions. (#246) 2025-03-24 22:38:22 +00:00
Nuno Cruces
6c296231a5 Fix #249. 2025-03-24 19:59:19 +00:00
Nuno Cruces
c067e3630b Improve SetInterrupt performance. 2025-03-24 10:41:50 +00:00
Nuno Cruces
35a2dbd847 Improve context cancellation performance. (#248) 2025-03-21 11:06:29 +00:00
Nuno Cruces
b36f73c66d Optimize. 2025-03-17 12:24:36 +00:00
Nuno Cruces
d36f19fd91 Docs. 2025-03-14 11:37:48 +00:00
Nuno Cruces
eba71b1f42 Go 1.24.1. 2025-03-14 00:54:12 +00:00
Nuno Cruces
d78239bfbf More EINTR. 2025-03-14 00:07:09 +00:00
Nuno Cruces
49852732b2 Optimize. 2025-03-12 17:29:12 +00:00
Nuno Cruces
9b90d076cb Update README.md 2025-03-12 12:01:13 +00:00
Nuno Cruces
15b94577b1 Tweak. 2025-03-11 20:15:53 +00:00
Nuno Cruces
25557244cc Global ConfigLog. 2025-03-11 17:07:56 +00:00
Nuno Cruces
c2d3bf0cfc Reduce flakyness. 2025-03-11 14:57:48 +00:00
Nuno Cruces
58a5682084 Handle EINTR. 2025-03-11 12:07:14 +00:00
Nuno Cruces
1ed954e96f Fix #243. 2025-03-10 14:54:34 +00:00
Nuno Cruces
9e7a0a875d Improved arg reuse. 2025-03-10 12:01:15 +00:00
Nuno Cruces
26adda4529 Seq aggregate functions (#229) 2025-03-08 14:07:43 +00:00
Nuno Cruces
2f6cd8de1d Docs. 2025-03-07 11:47:02 +00:00
dependabot[bot]
e027e055ff Bump golang.org/x/crypto from 0.35.0 to 0.36.0 (#239)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/crypto/compare/v0.35.0...v0.36.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 23:11:20 +00:00
dependabot[bot]
63fdc141e5 Bump golang.org/x/text from 0.22.0 to 0.23.0 (#240)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.22.0...v0.23.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 22:57:38 +00:00
Nuno Cruces
0bbd145a49 Update modules. 2025-02-28 16:57:25 +00:00
200 changed files with 7821 additions and 2266 deletions

23
.github/workflows/libc.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Benchmark libc
on:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Benchmark
shell: bash
run: sqlite3/libc/benchmark.sh

View File

@@ -1,28 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "$OSTYPE" == "linux"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-x86_64-linux.tar.gz"
elif [[ "$OSTYPE" == "darwin"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-arm64-macos.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-arm64-macos.tar.gz"
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-windows.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-x86_64-windows.tar.gz"
fi
# Download tools
mkdir -p tools/
[ -d "tools/wasi-sdk" ] || curl -#L "$WASI_SDK" | tar xzC tools &
[ -d "tools/binaryen" ] || curl -#L "$BINARYEN" | tar xzC tools &
wait
[ -d "tools/wasi-sdk" ] || mv "tools/wasi-sdk"* "tools/wasi-sdk"
[ -d "tools/binaryen" ] || mv "tools/binaryen"* "tools/binaryen"
# Download and build SQLite
sqlite3/download.sh
sqlite3/tools.sh
embed/build.sh
embed/bcw2/build.sh

View File

@@ -17,13 +17,13 @@ jobs:
steps:
- uses: ilammy/msvc-dev-cmd@v1
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build
shell: bash
run: .github/workflows/repro.sh
- uses: actions/attest-build-provenance@v2
- uses: actions/attest-build-provenance@v3
if: matrix.os == 'ubuntu-latest'
with:
subject-path: |

View File

@@ -17,16 +17,21 @@ on:
- '**.yml'
workflow_dispatch:
permissions:
contents: read
jobs:
test:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Format
@@ -93,27 +98,27 @@ jobs:
matrix:
os:
- name: freebsd
version: '14.2'
version: '14.3'
flags: '-test.v'
- name: netbsd
version: '10.1'
flags: '-test.v'
- name: freebsd
arch: arm64
version: '14.2'
version: '14.3'
flags: '-test.v -test.short'
- name: netbsd
arch: arm64
version: '10.1'
flags: '-test.v -test.short'
- name: openbsd
version: '7.6'
version: '7.8'
flags: '-test.v -test.short'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build
env:
@@ -123,7 +128,7 @@ jobs:
run: .github/workflows/build-test.sh
- name: Test
uses: cross-platform-actions/action@v0.27.0
uses: cross-platform-actions/action@v0.30.0
with:
operating_system: ${{ matrix.os.name }}
architecture: ${{ matrix.os.arch }}
@@ -150,7 +155,7 @@ jobs:
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build
env:
@@ -169,8 +174,8 @@ jobs:
steps:
- uses: bytecodealliance/actions/wasmtime/setup@v1
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Set path
@@ -190,8 +195,8 @@ jobs:
steps:
- uses: docker/setup-qemu-action@v3
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test 386 (32-bit)
@@ -211,20 +216,32 @@ jobs:
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test
run: go test -v ./...
test-macintel:
runs-on: macos-13
runs-on: macos-15-intel
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test
run: go test -v ./...
test-winarm:
runs-on: windows-11-arm
needs: test
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test

View File

@@ -30,10 +30,10 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
- [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
wraps the [C SQLite API](https://sqlite.org/cintro.html)
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-package)).
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-package)).
- [`github.com/ncruces/go-sqlite3/driver`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver)
provides a [`database/sql`](https://pkg.go.dev/database/sql) driver
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package)).
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package)).
- [`github.com/ncruces/go-sqlite3/embed`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/embed)
embeds a build of SQLite into your application.
- [`github.com/ncruces/go-sqlite3/vfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs)
@@ -44,12 +44,19 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
### Advanced features
- [incremental BLOB I/O](https://sqlite.org/c3ref/blob_open.html)
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/blobio#example-package))
- [nested transactions](https://sqlite.org/lang_savepoint.html)
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-Savepoint))
- [custom functions](https://sqlite.org/c3ref/create_function.html)
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-Conn.CreateFunction))
- [virtual tables](https://sqlite.org/vtab.html)
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-CreateModule))
- [custom VFSes](https://sqlite.org/vfs.html)
([examples](vfs/README.md#custom-vfses))
- [online backup](https://sqlite.org/backup.html)
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#Conn))
- [JSON support](https://sqlite.org/json1.html)
([example](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package-Json))
- [math functions](https://sqlite.org/lang_mathfunc.html)
- [full-text search](https://sqlite.org/fts5.html)
- [geospatial search](https://sqlite.org/geopoly.html)
@@ -57,7 +64,6 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
- [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
@@ -65,31 +71,52 @@ db.QueryRow(`SELECT sqlite_version()`).Scan(&version)
This module replaces the SQLite [OS Interface](https://sqlite.org/vfs.html)
(aka VFS) with a [pure Go](vfs/) implementation,
which has advantages and disadvantages.
Read more about the Go VFS design [here](vfs/README.md).
Because each database connection executes within a Wasm sandboxed environment,
memory usage will be higher than alternatives.
### Testing
This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3/wiki/Test-coverage-report).
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach)
thorough testing.
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) on
Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64),
Windows (amd64), FreeBSD (amd64/arm64), OpenBSD (amd64), NetBSD (amd64/arm64),
DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64).
Every commit is tested on:
* Linux: amd64, arm64, 386, riscv64, ppc64le, s390x
* macOS: amd64, arm64
* Windows: amd64, arm64
* BSD:
* FreeBSD: amd64, arm64
* NetBSD: amd64, arm64
* DragonFly BSD: amd64
* OpenBSD: amd64
* illumos: amd64
* Solaris: amd64
Certain operating system and CPU combinations have some limitations. See the [support matrix](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) for a complete overview.
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
Performance 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 benchmarked by running SQLite's
[speedtest1](https://github.com/sqlite/sqlite/blob/master/test/speedtest1.c).
### Concurrency
This module behaves similarly to SQLite in [multi-thread](https://sqlite.org/threadsafe.html) mode:
it is goroutine-safe, provided that no single database connection, or object derived from it,
is used concurrently by multiple goroutines.
The [`database/sql`](https://pkg.go.dev/database/sql) API is safe to use concurrently,
according to its documentation.
### FAQ, issues, new features
For questions, please see [Discussions](https://github.com/ncruces/go-sqlite3/discussions/categories/q-a).
@@ -106,4 +133,4 @@ and features we're working on, planning to work on, or asking for help with.
- [`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite)
- [`crawshaw.io/sqlite`](https://pkg.go.dev/crawshaw.io/sqlite)
- [`github.com/mattn/go-sqlite3`](https://pkg.go.dev/github.com/mattn/go-sqlite3)
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)

View File

@@ -31,6 +31,10 @@ 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) {
if c.interrupt.Err() != nil {
return nil, INTERRUPT
}
defer c.arena.mark()()
blobPtr := c.arena.new(ptrlen)
dbPtr := c.arena.string(db)
@@ -42,7 +46,6 @@ func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob,
flags = 1
}
c.checkInterrupt(c.handle)
rc := res_t(c.call("sqlite3_blob_open", stk_t(c.handle),
stk_t(dbPtr), stk_t(tablePtr), stk_t(columnPtr),
stk_t(row), stk_t(flags), stk_t(blobPtr)))
@@ -253,7 +256,9 @@ func (b *Blob) Seek(offset int64, whence int) (int64, error) {
//
// https://sqlite.org/c3ref/blob_reopen.html
func (b *Blob) Reopen(row int64) error {
b.c.checkInterrupt(b.c.handle)
if b.c.interrupt.Err() != nil {
return INTERRUPT
}
err := b.c.error(res_t(b.c.call("sqlite3_blob_reopen", stk_t(b.handle), stk_t(row))))
b.bytes = int64(int32(b.c.call("sqlite3_blob_bytes", stk_t(b.handle))))
b.offset = 0

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"sync/atomic"
"github.com/tetratelabs/wazero/api"
@@ -48,6 +49,15 @@ func (c *Conn) Config(op DBConfig, arg ...bool) (bool, error) {
return util.ReadBool(c.mod, argsPtr), c.error(rc)
}
var defaultLogger atomic.Pointer[func(code ExtendedErrorCode, msg string)]
// ConfigLog sets up the default error logging callback for new connections.
//
// https://sqlite.org/errlog.html
func ConfigLog(cb func(code ExtendedErrorCode, msg string)) {
defaultLogger.Store(&cb)
}
// ConfigLog sets up the error logging callback for the connection.
//
// https://sqlite.org/errlog.html
@@ -99,7 +109,7 @@ func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, erro
default:
return nil, MISUSE
case FCNTL_RESET_CACHE:
case FCNTL_RESET_CACHE, FCNTL_NULL_IO:
rc = res_t(c.call("sqlite3_file_control",
stk_t(c.handle), stk_t(schemaPtr),
stk_t(op), 0))
@@ -255,7 +265,7 @@ func traceCallback(ctx context.Context, mod api.Module, evt TraceEvent, pDB, pAr
}
}
if arg1 != nil {
_, rc = errorCode(c.trace(evt, arg1, arg2), ERROR)
_ = c.trace(evt, arg1, arg2)
}
}
return rc
@@ -265,6 +275,10 @@ func traceCallback(ctx context.Context, mod api.Module, evt TraceEvent, pDB, pAr
//
// https://sqlite.org/c3ref/wal_checkpoint_v2.html
func (c *Conn) WALCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt int, err error) {
if c.interrupt.Err() != nil {
return 0, 0, INTERRUPT
}
defer c.arena.mark()()
nLogPtr := c.arena.new(ptrlen)
nCkptPtr := c.arena.new(ptrlen)
@@ -378,6 +392,6 @@ func (c *Conn) EnableChecksums(schema string) error {
}
// Checkpoint the WAL.
_, _, err = c.WALCheckpoint(schema, CHECKPOINT_RESTART)
_, _, err = c.WALCheckpoint(schema, CHECKPOINT_FULL)
return err
}

118
conn.go
View File

@@ -25,7 +25,6 @@ type Conn struct {
*sqlite
interrupt context.Context
pending *Stmt
stmts []*Stmt
busy func(context.Context, int) bool
log func(xErrorCode, string)
@@ -41,6 +40,7 @@ type Conn struct {
busylst time.Time
arena arena
handle ptr_t
gosched uint8
}
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE] and [OPEN_URI].
@@ -49,7 +49,7 @@ func Open(filename string) (*Conn, error) {
}
// OpenContext is like [Open] but includes a context,
// which is used to interrupt the process of opening the connectiton.
// which is used to interrupt the process of opening the connection.
func OpenContext(ctx context.Context, filename string) (*Conn, error) {
return newConn(ctx, filename, OPEN_READWRITE|OPEN_CREATE|OPEN_URI)
}
@@ -92,6 +92,9 @@ func newConn(ctx context.Context, filename string, flags OpenFlag) (ret *Conn, _
}()
c.ctx = context.WithValue(c.ctx, connKey{}, c)
if logger := defaultLogger.Load(); logger != nil {
c.ConfigLog(*logger)
}
c.arena = c.newArena()
c.handle, err = c.openDB(filename, flags)
if err == nil {
@@ -117,7 +120,7 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (ptr_t, error) {
return 0, err
}
c.call("sqlite3_progress_handler_go", stk_t(handle), 100)
c.call("sqlite3_progress_handler_go", stk_t(handle), 1000)
if flags|OPEN_URI != 0 && strings.HasPrefix(filename, "file:") {
var pragmas strings.Builder
if _, after, ok := strings.Cut(filename, "?"); ok {
@@ -129,7 +132,6 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (ptr_t, error) {
}
}
if pragmas.Len() != 0 {
c.checkInterrupt(handle)
pragmaPtr := c.arena.string(pragmas.String())
rc := res_t(c.call("sqlite3_exec", stk_t(handle), stk_t(pragmaPtr), 0, 0, 0))
if err := c.sqlite.error(rc, handle, pragmas.String()); err != nil {
@@ -163,9 +165,6 @@ func (c *Conn) Close() error {
return nil
}
c.pending.Close()
c.pending = nil
rc := res_t(c.call("sqlite3_close", stk_t(c.handle)))
if err := c.error(rc); err != nil {
return err
@@ -180,11 +179,16 @@ func (c *Conn) Close() error {
//
// https://sqlite.org/c3ref/exec.html
func (c *Conn) Exec(sql string) error {
defer c.arena.mark()()
sqlPtr := c.arena.string(sql)
if c.interrupt.Err() != nil {
return INTERRUPT
}
return c.exec(sql)
}
c.checkInterrupt(c.handle)
rc := res_t(c.call("sqlite3_exec", stk_t(c.handle), stk_t(sqlPtr), 0, 0, 0))
func (c *Conn) exec(sql string) error {
defer c.arena.mark()()
textPtr := c.arena.string(sql)
rc := res_t(c.call("sqlite3_exec", stk_t(c.handle), stk_t(textPtr), 0, 0, 0))
return c.error(rc, sql)
}
@@ -203,20 +207,22 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
if len(sql) > _MAX_SQL_LENGTH {
return nil, "", TOOBIG
}
if c.interrupt.Err() != nil {
return nil, "", INTERRUPT
}
defer c.arena.mark()()
stmtPtr := c.arena.new(ptrlen)
tailPtr := c.arena.new(ptrlen)
sqlPtr := c.arena.string(sql)
textPtr := c.arena.string(sql)
c.checkInterrupt(c.handle)
rc := res_t(c.call("sqlite3_prepare_v3", stk_t(c.handle),
stk_t(sqlPtr), stk_t(len(sql)+1), stk_t(flags),
stk_t(textPtr), stk_t(len(sql)+1), stk_t(flags),
stk_t(stmtPtr), stk_t(tailPtr)))
stmt = &Stmt{c: c}
stmt = &Stmt{c: c, sql: sql}
stmt.handle = util.Read32[ptr_t](c.mod, stmtPtr)
if sql := sql[util.Read32[ptr_t](c.mod, tailPtr)-sqlPtr:]; sql != "" {
if sql := sql[util.Read32[ptr_t](c.mod, tailPtr)-textPtr:]; sql != "" {
tail = sql
}
@@ -337,43 +343,17 @@ func (c *Conn) GetInterrupt() context.Context {
//
// https://sqlite.org/c3ref/interrupt.html
func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
if ctx == nil {
panic("nil Context")
}
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 {
defer c.arena.mark()()
stmtPtr := c.arena.new(ptrlen)
loopPtr := c.arena.string(`WITH RECURSIVE c(x) AS (VALUES(0) UNION ALL SELECT x FROM c) SELECT x FROM c`)
c.call("sqlite3_prepare_v3", stk_t(c.handle), stk_t(loopPtr), math.MaxUint64,
stk_t(PREPARE_PERSISTENT), stk_t(stmtPtr), 0)
c.pending = &Stmt{c: c}
c.pending.handle = util.Read32[ptr_t](c.mod, stmtPtr)
}
if old.Done() != nil && ctx.Err() == nil {
c.pending.Reset()
}
if ctx.Done() != nil {
c.pending.Step()
}
return old
}
func (c *Conn) checkInterrupt(handle ptr_t) {
if c.interrupt.Err() != nil {
c.call("sqlite3_interrupt", stk_t(handle))
}
}
func progressCallback(ctx context.Context, mod api.Module, _ ptr_t) (interrupt int32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok {
if c.interrupt.Done() != nil {
if c.gosched++; c.gosched%16 == 0 {
runtime.Gosched()
}
if c.interrupt.Err() != nil {
@@ -429,11 +409,8 @@ func (c *Conn) BusyHandler(cb func(ctx context.Context, count int) (retry bool))
func busyCallback(ctx context.Context, mod api.Module, pDB ptr_t, count int32) (retry int32) {
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil {
interrupt := c.interrupt
if interrupt == nil {
interrupt = context.Background()
}
if interrupt.Err() == nil && c.busy(interrupt, int(count)) {
if interrupt := c.interrupt; interrupt.Err() == nil &&
c.busy(interrupt, int(count)) {
retry = 1
}
}
@@ -443,21 +420,21 @@ func busyCallback(ctx context.Context, mod api.Module, pDB ptr_t, count int32) (
// 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) {
func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int64, err error) {
defer c.arena.mark()()
hiPtr := c.arena.new(intlen)
curPtr := c.arena.new(intlen)
hiPtr := c.arena.new(8)
curPtr := c.arena.new(8)
var i int32
if reset {
i = 1
}
rc := res_t(c.call("sqlite3_db_status", stk_t(c.handle),
rc := res_t(c.call("sqlite3_db_status64", stk_t(c.handle),
stk_t(op), stk_t(curPtr), stk_t(hiPtr), stk_t(i)))
if err = c.error(rc); err == nil {
current = int(util.Read32[int32](c.mod, curPtr))
highwater = int(util.Read32[int32](c.mod, hiPtr))
current = util.Read64[int64](c.mod, curPtr)
highwater = util.Read64[int64](c.mod, hiPtr)
}
return
}
@@ -467,20 +444,27 @@ func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int, err erro
// https://sqlite.org/c3ref/table_column_metadata.html
func (c *Conn) TableColumnMetadata(schema, table, column string) (declType, collSeq string, notNull, primaryKey, autoInc bool, err error) {
defer c.arena.mark()()
var schemaPtr, columnPtr ptr_t
declTypePtr := c.arena.new(ptrlen)
collSeqPtr := c.arena.new(ptrlen)
notNullPtr := c.arena.new(ptrlen)
autoIncPtr := c.arena.new(ptrlen)
primaryKeyPtr := c.arena.new(ptrlen)
var (
declTypePtr ptr_t
collSeqPtr ptr_t
notNullPtr ptr_t
primaryKeyPtr ptr_t
autoIncPtr ptr_t
columnPtr ptr_t
schemaPtr ptr_t
)
if column != "" {
declTypePtr = c.arena.new(ptrlen)
collSeqPtr = c.arena.new(ptrlen)
notNullPtr = c.arena.new(ptrlen)
primaryKeyPtr = c.arena.new(ptrlen)
autoIncPtr = c.arena.new(ptrlen)
columnPtr = c.arena.string(column)
}
if schema != "" {
schemaPtr = c.arena.string(schema)
}
tablePtr := c.arena.string(table)
if column != "" {
columnPtr = c.arena.string(column)
}
rc := res_t(c.call("sqlite3_table_column_metadata", stk_t(c.handle),
stk_t(schemaPtr), stk_t(tablePtr), stk_t(columnPtr),

View File

@@ -11,10 +11,9 @@ const (
_ROW = 100 /* sqlite3_step() has another row ready */
_DONE = 101 /* sqlite3_step() has finished executing */
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
_MAX_LENGTH = 1e9
_MAX_SQL_LENGTH = 1e9
_MAX_FUNCTION_ARG = 100
_MAX_NAME = 1e6 // Self-imposed limit for most NUL terminated strings.
_MAX_LENGTH = 1e9
_MAX_SQL_LENGTH = 1e9
ptrlen = util.PtrLen
intlen = util.IntLen
@@ -74,6 +73,9 @@ const (
ERROR_MISSING_COLLSEQ ExtendedErrorCode = xErrorCode(ERROR) | (1 << 8)
ERROR_RETRY ExtendedErrorCode = xErrorCode(ERROR) | (2 << 8)
ERROR_SNAPSHOT ExtendedErrorCode = xErrorCode(ERROR) | (3 << 8)
ERROR_RESERVESIZE ExtendedErrorCode = xErrorCode(ERROR) | (4 << 8)
ERROR_KEY ExtendedErrorCode = xErrorCode(ERROR) | (5 << 8)
ERROR_UNABLE ExtendedErrorCode = xErrorCode(ERROR) | (6 << 8)
IOERR_READ ExtendedErrorCode = xErrorCode(IOERR) | (1 << 8)
IOERR_SHORT_READ ExtendedErrorCode = xErrorCode(IOERR) | (2 << 8)
IOERR_WRITE ExtendedErrorCode = xErrorCode(IOERR) | (3 << 8)
@@ -108,6 +110,8 @@ const (
IOERR_DATA ExtendedErrorCode = xErrorCode(IOERR) | (32 << 8)
IOERR_CORRUPTFS ExtendedErrorCode = xErrorCode(IOERR) | (33 << 8)
IOERR_IN_PAGE ExtendedErrorCode = xErrorCode(IOERR) | (34 << 8)
IOERR_BADKEY ExtendedErrorCode = xErrorCode(IOERR) | (35 << 8)
IOERR_CODEC ExtendedErrorCode = xErrorCode(IOERR) | (36 << 8)
LOCKED_SHAREDCACHE ExtendedErrorCode = xErrorCode(LOCKED) | (1 << 8)
LOCKED_VTAB ExtendedErrorCode = xErrorCode(LOCKED) | (2 << 8)
BUSY_RECOVERY ExtendedErrorCode = xErrorCode(BUSY) | (1 << 8)
@@ -186,12 +190,12 @@ const (
type FunctionFlag uint32
const (
DETERMINISTIC FunctionFlag = 0x000000800
DIRECTONLY FunctionFlag = 0x000080000
INNOCUOUS FunctionFlag = 0x000200000
SELFORDER1 FunctionFlag = 0x002000000
// SUBTYPE FunctionFlag = 0x000100000
// RESULT_SUBTYPE FunctionFlag = 0x001000000
DETERMINISTIC FunctionFlag = 0x000000800
DIRECTONLY FunctionFlag = 0x000080000
SUBTYPE FunctionFlag = 0x000100000
INNOCUOUS FunctionFlag = 0x000200000
RESULT_SUBTYPE FunctionFlag = 0x001000000
SELFORDER1 FunctionFlag = 0x002000000
)
// StmtStatus name counter values associated with the [Stmt.Status] method.
@@ -230,7 +234,8 @@ const (
DBSTATUS_DEFERRED_FKS DBStatus = 10
DBSTATUS_CACHE_USED_SHARED DBStatus = 11
DBSTATUS_CACHE_SPILL DBStatus = 12
// DBSTATUS_MAX DBStatus = 12
DBSTATUS_TEMPBUF_SPILL DBStatus = 13
// DBSTATUS_MAX DBStatus = 13
)
// DBConfig are the available database connection configuration options.
@@ -281,6 +286,7 @@ const (
FCNTL_DATA_VERSION FcntlOpcode = 35
FCNTL_RESERVE_BYTES FcntlOpcode = 38
FCNTL_RESET_CACHE FcntlOpcode = 42
FCNTL_NULL_IO FcntlOpcode = 43
)
// LimitCategory are the available run-time limit categories.
@@ -362,13 +368,14 @@ const (
// CheckpointMode are all the checkpoint mode values.
//
// https://sqlite.org/c3ref/c_checkpoint_full.html
type CheckpointMode uint32
type CheckpointMode int32
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 */
CHECKPOINT_NOOP CheckpointMode = -1 /* Do no work at all */
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].

View File

@@ -1,7 +1,6 @@
package sqlite3
import (
"encoding/json"
"errors"
"math"
"time"
@@ -89,20 +88,26 @@ func (ctx Context) ResultText(value string) {
}
// 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) {
if len(value) == 0 {
ctx.ResultText("")
return
}
ptr := ctx.c.newBytes(value)
ctx.c.call("sqlite3_result_text_go",
stk_t(ctx.handle), stk_t(ptr), stk_t(len(value)))
}
// ResultBlob sets the result of the function to a []byte.
// Returning a nil slice is the same as calling [Context.ResultNull].
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultBlob(value []byte) {
if len(value) == 0 {
ctx.ResultZeroBlob(0)
return
}
ptr := ctx.c.newBytes(value)
ctx.c.call("sqlite3_result_blob_go",
stk_t(ctx.handle), stk_t(ptr), stk_t(len(value)))
@@ -167,18 +172,6 @@ func (ctx Context) ResultPointer(ptr any) {
stk_t(ctx.handle), stk_t(valPtr))
}
// ResultJSON sets the result of the function to the JSON encoding of value.
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultJSON(value any) {
data, err := json.Marshal(value)
if err != nil {
ctx.ResultError(err)
return // notest
}
ctx.ResultRawText(data)
}
// ResultValue sets the result of the function to a copy of [Value].
//
// https://sqlite.org/c3ref/result_blob.html
@@ -205,19 +198,27 @@ func (ctx Context) ResultError(err error) {
return
}
msg, code := errorCode(err, _OK)
msg, code := errorCode(err, ERROR)
if msg != "" {
defer ctx.c.arena.mark()()
ptr := ctx.c.arena.string(msg)
ctx.c.call("sqlite3_result_error",
stk_t(ctx.handle), stk_t(ptr), stk_t(len(msg)))
}
if code != _OK {
if code != res_t(ERROR) {
ctx.c.call("sqlite3_result_error_code",
stk_t(ctx.handle), stk_t(code))
}
}
// ResultSubtype sets the subtype of the result of the function.
//
// https://sqlite.org/c3ref/result_subtype.html
func (ctx Context) ResultSubtype(t uint) {
ctx.c.call("sqlite3_result_subtype",
stk_t(ctx.handle), stk_t(uint32(t)))
}
// VTabNoChange may return true if a column is being fetched as part
// of an update during which the column value will not change.
//

View File

@@ -20,22 +20,45 @@
// - a [serializable] transaction is always "immediate";
// - a [read-only] transaction is always "deferred".
//
// # Datatypes In SQLite
//
// SQLite is dynamically typed.
// Columns can mostly hold any value regardless of their declared type.
// SQLite supports most [driver.Value] types out of the box,
// but bool and [time.Time] require special care.
//
// Booleans can be stored on any column type and scanned back to a *bool.
// However, if scanned to a *any, booleans may either become an
// int64, string or bool, depending on the declared type of the column.
// If you use BOOLEAN for your column type,
// 1 and 0 will always scan as true and false.
//
// # Working with time
//
// Time values can similarly be stored on any column type.
// 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";
// Special 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.
//
// If you encode as RFC 3339 (the default),
// consider using the TIME [collating sequence] to produce a time-ordered sequence.
// You can also set "_timefmt" to an arbitrary [sqlite3.TimeFormat] or [time.Layout].
//
// To scan values in other formats, [sqlite3.TimeFormat.Scanner] may be helpful.
// To bind values in other formats, [sqlite3.TimeFormat.Encode] them before binding.
// If you encode as RFC 3339 (the default),
// consider using the TIME [collating sequence] to produce time-ordered sequences.
//
// If you encode as RFC 3339 (the default),
// time values will scan back to a *time.Time unless your column type is TEXT.
// Otherwise, if scanned to a *any, time values may either become an
// int64, float64 or string, depending on the time format and declared type of the column.
// If you use DATE, TIME, DATETIME, or TIMESTAMP for your column type,
// "_timefmt" will be used to decode values.
//
// To scan values in custom formats, [sqlite3.TimeFormat.Scanner] may be helpful.
// To bind values in custom 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].
@@ -48,7 +71,7 @@
// 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.
// depending on the column type and whoever wrote the value.
// [sqlite3.TimeFormat.Decode] may help.
//
// # Setting PRAGMAs
@@ -218,8 +241,9 @@ func (n *connector) Connect(ctx context.Context) (ret driver.Conn, err error) {
}
}()
old := c.Conn.SetInterrupt(ctx)
defer c.Conn.SetInterrupt(old)
if old := c.Conn.SetInterrupt(ctx); old != ctx {
defer c.Conn.SetInterrupt(old)
}
if !n.pragmas {
err = c.Conn.BusyTimeout(time.Minute)
@@ -339,8 +363,9 @@ func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, e
c.txReset = `; PRAGMA query_only=` + string(c.readOnly)
}
old := c.Conn.SetInterrupt(ctx)
defer c.Conn.SetInterrupt(old)
if old := c.Conn.SetInterrupt(ctx); old != ctx {
defer c.Conn.SetInterrupt(old)
}
err := c.Conn.Exec(txBegin)
if err != nil {
@@ -358,13 +383,12 @@ func (c *conn) Commit() error {
}
func (c *conn) Rollback() error {
err := c.Conn.Exec(`ROLLBACK` + c.txReset)
if errors.Is(err, sqlite3.INTERRUPT) {
old := c.Conn.SetInterrupt(context.Background())
// ROLLBACK even if interrupted.
ctx := context.Background()
if old := c.Conn.SetInterrupt(ctx); old != ctx {
defer c.Conn.SetInterrupt(old)
err = c.Conn.Exec(`ROLLBACK` + c.txReset)
}
return err
return c.Conn.Exec(`ROLLBACK` + c.txReset)
}
func (c *conn) Prepare(query string) (driver.Stmt, error) {
@@ -373,8 +397,9 @@ func (c *conn) Prepare(query string) (driver.Stmt, error) {
}
func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
old := c.Conn.SetInterrupt(ctx)
defer c.Conn.SetInterrupt(old)
if old := c.Conn.SetInterrupt(ctx); old != ctx {
defer c.Conn.SetInterrupt(old)
}
s, tail, err := c.Conn.Prepare(query)
if err != nil {
@@ -399,8 +424,9 @@ func (c *conn) ExecContext(ctx context.Context, query string, args []driver.Name
return resultRowsAffected(0), nil
}
old := c.Conn.SetInterrupt(ctx)
defer c.Conn.SetInterrupt(old)
if old := c.Conn.SetInterrupt(ctx); old != ctx {
defer c.Conn.SetInterrupt(old)
}
err := c.Conn.Exec(query)
if err != nil {
@@ -416,6 +442,22 @@ func (c *conn) CheckNamedValue(arg *driver.NamedValue) error {
return nil
}
// Deprecated: for Litestream use only; may be removed at any time.
func (c *conn) FileControlPersistWAL(schema string, mode int) (int, error) {
// notest
arg := make([]any, 1)
if mode >= 0 {
arg[0] = mode > 0
} else {
arg = arg[:0]
}
res, err := c.Conn.FileControl(schema, sqlite3.FCNTL_PERSIST_WAL, arg...)
if res == true {
return 1, err
}
return 0, err
}
type stmt struct {
*sqlite3.Stmt
tmWrite sqlite3.TimeFormat
@@ -463,8 +505,10 @@ func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (drive
return nil, err
}
old := s.Stmt.Conn().SetInterrupt(ctx)
defer s.Stmt.Conn().SetInterrupt(old)
c := s.Stmt.Conn()
if old := c.SetInterrupt(ctx); old != ctx {
defer c.SetInterrupt(old)
}
err = errors.Join(
s.Stmt.Exec(),
@@ -473,7 +517,7 @@ func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (drive
return nil, err
}
return newResult(s.Stmt.Conn()), nil
return newResult(c), nil
}
func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
@@ -518,8 +562,8 @@ func (s *stmt) setupBindings(args []driver.NamedValue) (err error) {
err = s.Stmt.BindTime(id, a, s.tmWrite)
case util.JSON:
err = s.Stmt.BindJSON(id, a.Value)
case util.PointerUnwrap:
err = s.Stmt.BindPointer(id, util.UnwrapPointer(a))
case util.Pointer:
err = s.Stmt.BindPointer(id, a.Value)
case nil:
err = s.Stmt.BindNull(id)
default:
@@ -537,7 +581,7 @@ func (s *stmt) CheckNamedValue(arg *driver.NamedValue) error {
switch arg.Value.(type) {
case bool, int, int64, float64, string, []byte,
time.Time, sqlite3.ZeroBlob,
util.JSON, util.PointerUnwrap,
util.JSON, util.Pointer,
nil:
return nil
default:
@@ -576,32 +620,65 @@ func (r resultRowsAffected) RowsAffected() (int64, error) {
return int64(r), nil
}
type scantype byte
const (
_ANY scantype = iota
_INT
_REAL
_TEXT
_BLOB
_NULL
_BOOL
_TIME
_NOT_NULL
)
var (
_ [0]struct{} = [scantype(sqlite3.INTEGER) - _INT]struct{}{}
_ [0]struct{} = [scantype(sqlite3.FLOAT) - _REAL]struct{}{}
_ [0]struct{} = [scantype(sqlite3.TEXT) - _TEXT]struct{}{}
_ [0]struct{} = [scantype(sqlite3.BLOB) - _BLOB]struct{}{}
_ [0]struct{} = [scantype(sqlite3.NULL) - _NULL]struct{}{}
_ [0]struct{} = [_NOT_NULL & (_NOT_NULL - 1)]struct{}{}
)
func scanFromDecl(decl string) scantype {
// 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 decl {
case "INT", "INTEGER":
return _INT
case "REAL":
return _REAL
case "TEXT":
return _TEXT
case "BLOB":
return _BLOB
case "BOOLEAN":
return _BOOL
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
return _TIME
}
return _ANY
}
type rows struct {
ctx context.Context
*stmt
names []string
types []string
nulls []bool
scans []scantype
dest []driver.Value
}
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{}
// _ driver.RowsColumnScanner = &rows{}
)
func (r *rows) Close() error {
@@ -622,79 +699,63 @@ func (r *rows) Columns() []string {
return r.names
}
func (r *rows) scanType(index int) scantype {
if r.scans == nil {
count := len(r.names)
scans := make([]scantype, count)
for i := range scans {
scans[i] = scanFromDecl(strings.ToUpper(r.Stmt.ColumnDeclType(i)))
}
r.scans = scans
}
return r.scans[index] &^ _NOT_NULL
}
func (r *rows) loadColumnMetadata() {
if r.nulls == nil {
count := r.Stmt.ColumnCount()
nulls := make([]bool, count)
if r.types == nil {
c := r.Stmt.Conn()
count := len(r.names)
types := make([]string, count)
scans := make([]scantype, count)
for i := range nulls {
for i := range types {
var notnull bool
if col := r.Stmt.ColumnOriginName(i); col != "" {
types[i], _, nulls[i], _, _, _ = r.Stmt.Conn().TableColumnMetadata(
types[i], _, notnull, _, _, _ = c.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
scans[i] = scanFromDecl(types[i])
if notnull {
scans[i] |= _NOT_NULL
}
}
}
r.nulls = nulls
r.types = types
r.scans = scans
}
}
func (r *rows) declType(index int) string {
if r.types == nil {
count := r.Stmt.ColumnCount()
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 {
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]
decl := r.types[index]
if len := len(decl); len > 0 && decl[len-1] == ')' {
if i := strings.LastIndexByte(decl, '('); i >= 0 {
decl = decl[:i]
}
}
return strings.TrimSpace(decltype)
return strings.TrimSpace(decl)
}
func (r *rows) ColumnTypeNullable(index int) (nullable, ok bool) {
r.loadColumnMetadata()
if r.nulls[index] {
return false, true
}
return true, false
nullable = r.scans[index]&^_NOT_NULL == 0
return nullable, !nullable
}
func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
r.loadColumnMetadata()
scan := r.scans[index]
scan := r.scans[index] &^ _NOT_NULL
if r.Stmt.Busy() {
// SQLite is dynamically typed and we now have a row.
@@ -706,7 +767,7 @@ func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
switch {
case scan == _TIME && val != _BLOB && val != _NULL:
t := r.Stmt.ColumnTime(index, r.tmRead)
useValType = t == time.Time{}
useValType = t.IsZero()
case scan == _BOOL && val == _INT:
i := r.Stmt.ColumnInt64(index)
useValType = i != 0 && i != 1
@@ -737,8 +798,11 @@ func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
}
func (r *rows) Next(dest []driver.Value) error {
old := r.Stmt.Conn().SetInterrupt(r.ctx)
defer r.Stmt.Conn().SetInterrupt(old)
r.dest = nil
c := r.Stmt.Conn()
if old := c.SetInterrupt(r.ctx); old != r.ctx {
defer c.SetInterrupt(old)
}
if !r.Stmt.Step() {
if err := r.Stmt.Err(); err != nil {
@@ -748,36 +812,69 @@ func (r *rows) Next(dest []driver.Value) error {
}
data := unsafe.Slice((*any)(unsafe.SliceData(dest)), len(dest))
err := r.Stmt.Columns(data...)
if err := r.Stmt.ColumnsRaw(data...); err != nil {
return err
}
for i := range dest {
if t, ok := r.decodeTime(i, dest[i]); ok {
dest[i] = t
scan := r.scanType(i)
if v, ok := dest[i].([]byte); ok {
if len(v) == cap(v) { // a BLOB
continue
}
if scan != _TEXT {
switch r.tmWrite {
case "", time.RFC3339, time.RFC3339Nano:
t, ok := maybeTime(v)
if ok {
dest[i] = t
continue
}
}
}
dest[i] = string(v)
}
switch scan {
case _TIME:
t, err := r.tmRead.Decode(dest[i])
if err == nil {
dest[i] = t
}
case _BOOL:
switch dest[i] {
case int64(0):
dest[i] = false
case int64(1):
dest[i] = true
}
}
}
r.dest = dest
return nil
}
func (r *rows) ScanColumn(dest any, index int) (err error) {
// notest // Go 1.26
var tm *time.Time
var ok *bool
switch d := dest.(type) {
case *time.Time:
tm = d
case *sql.NullTime:
tm = &d.Time
ok = &d.Valid
case *sql.Null[time.Time]:
tm = &d.V
ok = &d.Valid
default:
return driver.ErrSkip
}
value := r.dest[index]
*tm, err = r.tmRead.Decode(value)
if ok != nil {
*ok = err == nil
if value == nil {
return nil
}
}
return err
}
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":
// could be a time value
default:
return
}
t, err := r.tmRead.Decode(v)
return t, err == nil
}

View File

@@ -8,6 +8,7 @@ import (
"math"
"net/url"
"reflect"
"strings"
"testing"
"time"
@@ -33,7 +34,7 @@ func Test_Open_error(t *testing.T) {
func Test_Open_dir(t *testing.T) {
t.Parallel()
db, err := sql.Open("sqlite3", ".")
db, err := Open(".")
if err != nil {
t.Fatal(err)
}
@@ -43,18 +44,18 @@ func Test_Open_dir(t *testing.T) {
if err == nil {
t.Fatal("want error")
}
if !errors.Is(err, sqlite3.CANTOPEN) {
t.Errorf("got %v, want sqlite3.CANTOPEN", err)
if !errors.Is(err, sqlite3.CANTOPEN_ISDIR) {
t.Errorf("got %v, want sqlite3.CANTOPEN_ISDIR", err)
}
}
func Test_Open_pragma(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_pragma": {"busy_timeout(1000)"},
})
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -72,11 +73,11 @@ func Test_Open_pragma(t *testing.T) {
func Test_Open_pragma_invalid(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_pragma": {"busy_timeout 1000"},
})
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -100,12 +101,12 @@ func Test_Open_pragma_invalid(t *testing.T) {
func Test_Open_txLock(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_txlock": {"exclusive"},
"_pragma": {"busy_timeout(1000)"},
})
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -136,11 +137,11 @@ func Test_Open_txLock(t *testing.T) {
func Test_Open_txLock_invalid(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_txlock": {"xclusive"},
})
_, err := sql.Open("sqlite3", tmp+"_txlock=xclusive")
_, err := Open(dsn)
if err == nil {
t.Fatal("want error")
}
@@ -151,31 +152,28 @@ func Test_Open_txLock_invalid(t *testing.T) {
func Test_BeginTx(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_txlock": {"exclusive"},
"_pragma": {"busy_timeout(0)"},
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
_, err = db.BeginTx(t.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err.Error() != string(util.IsolationErr) {
t.Error("want isolationErr")
}
tx1, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
tx1, err := db.BeginTx(t.Context(), &sql.TxOptions{ReadOnly: true})
if err != nil {
t.Fatal(err)
}
tx2, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
tx2, err := db.BeginTx(t.Context(), &sql.TxOptions{ReadOnly: true})
if err != nil {
t.Fatal(err)
}
@@ -199,11 +197,69 @@ func Test_BeginTx(t *testing.T) {
}
}
func Test_nested_context(t *testing.T) {
t.Parallel()
dsn := memdb.TestDB(t)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
outer, err := tx.Query(`SELECT value FROM generate_series(0)`)
if err != nil {
t.Fatal(err)
}
defer outer.Close()
want := func(rows *sql.Rows, want int) {
t.Helper()
var got int
rows.Next()
if err := rows.Scan(&got); err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
want(outer, 0)
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
inner, err := tx.QueryContext(ctx, `SELECT value FROM generate_series(0)`)
if err != nil {
t.Fatal(err)
}
defer inner.Close()
want(inner, 0)
cancel()
var terr interface{ Temporary() bool }
if inner.Next() || !errors.Is(inner.Err(), context.Canceled) &&
(!errors.As(inner.Err(), &terr) || !terr.Temporary()) {
t.Fatalf("got %v, want cancellation", inner.Err())
}
want(outer, 1)
}
func Test_Prepare(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -242,24 +298,21 @@ func Test_Prepare(t *testing.T) {
func Test_QueryRow_named(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(ctx)
conn, err := db.Conn(t.Context())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
stmt, err := conn.PrepareContext(ctx, `SELECT ?, ?5, :AAA, @AAA, $AAA`)
stmt, err := conn.PrepareContext(t.Context(), `SELECT ?, ?5, :AAA, @AAA, $AAA`)
if err != nil {
t.Fatal(err)
}
@@ -295,9 +348,9 @@ func Test_QueryRow_named(t *testing.T) {
func Test_QueryRow_blob_null(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -332,11 +385,11 @@ func Test_time(t *testing.T) {
for _, fmt := range []string{"auto", "sqlite", "rfc3339", time.ANSIC} {
t.Run(fmt, func(t *testing.T) {
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_timefmt": {fmt},
})
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -379,9 +432,9 @@ func Test_ColumnType_ScanType(t *testing.T) {
)
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -467,3 +520,58 @@ func Test_ColumnType_ScanType(t *testing.T) {
t.Fatal(err)
}
}
func Test_rows_ScanColumn(t *testing.T) {
t.Parallel()
dsn := memdb.TestDB(t)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var tm time.Time
err = db.QueryRow(`SELECT NULL`).Scan(&tm)
if err == nil {
t.Error("want error")
}
// Go 1.26
err = db.QueryRow(`SELECT datetime()`).Scan(&tm)
if err != nil && !strings.HasPrefix(err.Error(), "sql: Scan error") {
t.Error(err)
}
var nt sql.NullTime
err = db.QueryRow(`SELECT NULL`).Scan(&nt)
if err != nil {
t.Error(err)
}
// Go 1.26
err = db.QueryRow(`SELECT datetime()`).Scan(&nt)
if err != nil && !strings.HasPrefix(err.Error(), "sql: Scan error") {
t.Error(err)
}
}
func Benchmark_loop(b *testing.B) {
db, err := Open(":memory:")
if err != nil {
b.Fatal(err)
}
defer db.Close()
var version string
err = db.QueryRow(`SELECT sqlite_version();`).Scan(&version)
if err != nil {
b.Fatal(err)
}
for b.Loop() {
_, err := db.ExecContext(b.Context(),
`WITH RECURSIVE c(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM c WHERE x < 1000000) SELECT x FROM c;`)
if err != nil {
b.Fatal(err)
}
}
}

View File

@@ -1,9 +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
import (
"database/sql"
"database/sql/driver"
@@ -27,7 +23,7 @@ func Example_customTime() {
_, err = db.Exec(`
CREATE TABLE data (
id INTEGER PRIMARY KEY,
date_time TEXT
date_time ANY
) STRICT;
`)
if err != nil {

View File

@@ -1,12 +1,15 @@
package driver
import "time"
import (
"bytes"
"time"
)
// Convert a string in [time.RFC3339Nano] format into a [time.Time]
// if it roundtrips back to the same string.
// This way times can be persisted to, and recovered from, the database,
// but if a string is needed, [database/sql] will recover the same string.
func maybeTime(text string) (_ time.Time, _ bool) {
func maybeTime(text []byte) (_ time.Time, _ bool) {
// Weed out (some) values that can't possibly be
// [time.RFC3339Nano] timestamps.
if len(text) < len("2006-01-02T15:04:05Z") {
@@ -21,8 +24,8 @@ func maybeTime(text string) (_ time.Time, _ bool) {
// Slow path.
var buf [len(time.RFC3339Nano)]byte
date, err := time.Parse(time.RFC3339Nano, text)
if err == nil && text == string(date.AppendFormat(buf[:0], time.RFC3339Nano)) {
date, err := time.Parse(time.RFC3339Nano, string(text))
if err == nil && bytes.Equal(text, date.AppendFormat(buf[:0], time.RFC3339Nano)) {
return date, true
}
return

View File

@@ -22,7 +22,7 @@ func Fuzz_stringOrTime_1(f *testing.F) {
f.Add("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
f.Fuzz(func(t *testing.T, str string) {
v, ok := maybeTime(str)
v, ok := maybeTime([]byte(str))
if ok {
// Make sure times round-trip to the same string:
// https://pkg.go.dev/database/sql#Rows.Scan
@@ -51,7 +51,7 @@ func Fuzz_stringOrTime_2(f *testing.F) {
f.Add(int64(-763421161058), int64(222_222_222)) // twosday, year 22222BC
checkTime := func(t testing.TB, date time.Time) {
v, ok := maybeTime(date.Format(time.RFC3339Nano))
v, ok := maybeTime(date.AppendFormat(nil, time.RFC3339Nano))
if ok {
// Make sure times round-trip to the same time:
if !v.Equal(date) {

View File

@@ -1,7 +1,6 @@
package driver
import (
"context"
"database/sql/driver"
"slices"
"testing"
@@ -56,7 +55,7 @@ func Fuzz_notWhitespace(f *testing.F) {
t.SkipNow()
}
c, err := db.Conn(context.Background())
c, err := db.Conn(t.Context())
if err != nil {
t.Fatal(err)
}

View File

@@ -1,6 +1,6 @@
# Embeddable Wasm build of SQLite
This folder includes an embeddable Wasm build of SQLite 3.49.1 for use with
This folder includes an embeddable Wasm build of SQLite 3.50.4 for use with
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
The following optional features are compiled in:

Binary file not shown.

View File

@@ -53,7 +53,7 @@ func Test_bcw2(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if version != "3.50.0" {
if version != "3.51.0" {
t.Error(version)
}
}

View File

@@ -7,17 +7,18 @@ ROOT=../../
BINARYEN="$ROOT/tools/binaryen/bin"
WASI_SDK="$ROOT/tools/wasi-sdk/bin"
trap 'rm -rf build/ sqlite/ bcw2.tmp' EXIT
trap 'rm -rf sqlite/ build/ bcw2.tmp' EXIT
mkdir -p sqlite/
mkdir -p build/ext/
cp "$ROOT"/sqlite3/*.[ch] build/
cp "$ROOT"/sqlite3/*.patch build/
cd sqlite/
# https://sqlite.org/src/info/c09656c62155a6e8
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=c09656c6 | tar xz
# https://sqlite.org/src/info/0e862bc9ed7aa9ae
curl -#L https://github.com/sqlite/sqlite/archive/0b99392.tar.gz | tar xz --strip-components=1
# curl -#L https://sqlite.org/src/tarball/sqlite.tar.gz?r=0e862bc9ed | tar xz --strip-components=1
cd sqlite
cat ../repro.patch | patch -p0 --no-backup-if-mismatch
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c "OPTS=-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT -DSQLITE_ENABLE_ORDERED_SET_AGGREGATES"
else
@@ -44,13 +45,14 @@ 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" \
-o bcw2.wasm build/main.c \
-I"$ROOT/sqlite3/libc" -I"build" \
-mexec-model=reactor \
-msimd128 -mmutable-globals -mmultivalue \
-mbulk-memory -mreference-types \
-mnontrapping-fptoint -msign-ext \
-fno-stack-protector -fno-stack-clash-protection \
-mmutable-globals -mnontrapping-fptoint \
-msimd128 -mbulk-memory -msign-ext \
-mreference-types -mmultivalue \
-mno-extended-const \
-fno-stack-protector \
-Wl,--stack-first \
-Wl,--import-undefined \
-Wl,--initial-memory=327680 \
@@ -60,8 +62,9 @@ cd ~-
$(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
"$BINARYEN/wasm-opt" -g bcw2.tmp -o bcw2.wasm \
--low-memory-unused --gufa --generate-global-effects --converge -O3 \
--enable-mutable-globals --enable-nontrapping-float-to-int \
--enable-simd --enable-bulk-memory --enable-sign-ext \
--enable-reference-types --enable-multivalue \
--strip --strip-producers

View File

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

View File

@@ -1,12 +1,12 @@
github.com/ncruces/go-sqlite3 v0.23.1 h1:zGAd76q+Tr18z/xKGatUlzBQdjR3J+rexfANUcjAgkY=
github.com/ncruces/go-sqlite3 v0.23.1/go.mod h1:Xg3FyAZl25HcBSFmcbymdfoTqD7jRnBUmv1jSrbIjdE=
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/ncruces/sort v0.1.5 h1:fiFWXXAqKI8QckPf/6hu/bGFwcEPrirIOFaJqWujs4k=
github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
github.com/ncruces/sort v0.1.6 h1:TrsJfGRH1AoWoaeB4/+gCohot9+cA6u/INaH5agIhNk=
github.com/ncruces/sort v0.1.6/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
github.com/tetratelabs/wazero v1.10.0 h1:CXP3zneLDl6J4Zy8N/J+d5JsWKfrjE6GtvVK1fpnDlk=
github.com/tetratelabs/wazero v1.10.0/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=

View File

@@ -1,23 +0,0 @@
# https://sqlite.org/src/vpatch?from=67809715977a5bad&to=3f57584710d61174
--- tool/mkpragmatab.tcl
+++ tool/mkpragmatab.tcl
@@ -526,14 +526,17 @@
puts $fd [format {#define PragFlg_%-10s 0x%02x /* %s */} \
$f $fv $flagMeaning($f)]
set fv [expr {$fv*2}]
}
-# Sort the column lists so that longer column lists occur first
+# Sort the column lists so that longer column lists occur first.
+# In the event of a tie, sort column lists lexicographically.
#
proc colscmp {a b} {
- return [expr {[llength $b] - [llength $a]}]
+ set rc [expr {[llength $b] - [llength $a]}]
+ if {$rc} {return $rc}
+ return [string compare $a $b]
}
set cols_list [lsort -command colscmp $cols_list]
# Generate the array of column names used by pragmas that act like
# queries.

View File

@@ -12,12 +12,13 @@ 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" \
-I"$ROOT/sqlite3/libc" -I"$ROOT/sqlite3" \
-mexec-model=reactor \
-msimd128 -mmutable-globals -mmultivalue \
-mbulk-memory -mreference-types \
-mnontrapping-fptoint -msign-ext \
-fno-stack-protector -fno-stack-clash-protection \
-mmutable-globals -mnontrapping-fptoint \
-msimd128 -mbulk-memory -msign-ext \
-mreference-types -mmultivalue \
-mno-extended-const \
-fno-stack-protector \
-Wl,--stack-first \
-Wl,--import-undefined \
-Wl,--initial-memory=327680 \
@@ -26,8 +27,9 @@ trap 'rm -f sqlite3.tmp' EXIT
$(awk '{print "-Wl,--export="$0}' exports.txt)
"$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 \
--enable-simd --enable-mutable-globals --enable-multivalue \
--enable-bulk-memory --enable-reference-types \
--enable-nontrapping-float-to-int --enable-sign-ext
"$BINARYEN/wasm-opt" -g sqlite3.tmp -o sqlite3.wasm \
--low-memory-unused --gufa --generate-global-effects --converge -O3 \
--enable-mutable-globals --enable-nontrapping-float-to-int \
--enable-simd --enable-bulk-memory --enable-sign-ext \
--enable-reference-types --enable-multivalue \
--strip --strip-producers

View File

@@ -59,13 +59,14 @@ sqlite3_db_filename
sqlite3_db_name
sqlite3_db_readonly
sqlite3_db_release_memory
sqlite3_db_status
sqlite3_db_status64
sqlite3_declare_vtab
sqlite3_errcode
sqlite3_errmsg
sqlite3_error_offset
sqlite3_errstr
sqlite3_exec
sqlite3_exec_go
sqlite3_expanded_sql
sqlite3_file_control
sqlite3_filename_database
@@ -97,6 +98,7 @@ sqlite3_result_error_toobig
sqlite3_result_int64
sqlite3_result_null
sqlite3_result_pointer_go
sqlite3_result_subtype
sqlite3_result_text_go
sqlite3_result_value
sqlite3_result_zeroblob64
@@ -125,6 +127,7 @@ sqlite3_value_int64
sqlite3_value_nochange
sqlite3_value_numeric_type
sqlite3_value_pointer_go
sqlite3_value_subtype
sqlite3_value_text
sqlite3_value_type
sqlite3_vtab_collation

View File

@@ -17,5 +17,7 @@ import (
var binary string
func init() {
sqlite3.Binary = unsafe.Slice(unsafe.StringData(binary), len(binary))
if sqlite3.Binary == nil {
sqlite3.Binary = unsafe.Slice(unsafe.StringData(binary), len(binary))
}
}

View File

@@ -19,7 +19,7 @@ func Test_init(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if version != "3.49.1" {
if version != "3.51.0" {
t.Error(version)
}
}

Binary file not shown.

View File

@@ -2,7 +2,6 @@ package sqlite3
import (
"errors"
"strconv"
"strings"
"github.com/ncruces/go-sqlite3/internal/util"
@@ -12,7 +11,7 @@ import (
//
// https://sqlite.org/c3ref/errcode.html
type Error struct {
str string
sys error
msg string
sql string
code res_t
@@ -29,28 +28,34 @@ func (e *Error) Code() ErrorCode {
//
// https://sqlite.org/rescode.html
func (e *Error) ExtendedCode() ExtendedErrorCode {
return ExtendedErrorCode(e.code)
return xErrorCode(e.code)
}
// Error implements the error interface.
func (e *Error) Error() string {
var b strings.Builder
b.WriteString("sqlite3: ")
if e.str != "" {
b.WriteString(e.str)
} else {
b.WriteString(strconv.Itoa(int(e.code)))
}
b.WriteString(util.ErrorCodeString(e.code))
if e.msg != "" {
b.WriteString(": ")
b.WriteString(e.msg)
}
if e.sys != nil {
b.WriteString(": ")
b.WriteString(e.sys.Error())
}
return b.String()
}
// Unwrap returns the underlying operating system error
// that caused the I/O error or failure to open a file.
//
// https://sqlite.org/c3ref/system_errno.html
func (e *Error) Unwrap() error {
return e.sys
}
// Is tests whether this error matches a given [ErrorCode] or [ExtendedErrorCode].
//
// It makes it possible to do:
@@ -83,7 +88,7 @@ func (e *Error) As(err any) bool {
// Temporary returns true for [BUSY] errors.
func (e *Error) Temporary() bool {
return e.Code() == BUSY
return e.Code() == BUSY || e.Code() == INTERRUPT
}
// Timeout returns true for [BUSY_TIMEOUT] errors.
@@ -98,22 +103,31 @@ func (e *Error) SQL() string {
// Error implements the error interface.
func (e ErrorCode) Error() string {
return util.ErrorCodeString(uint32(e))
return util.ErrorCodeString(e)
}
// As converts this error to an [ExtendedErrorCode].
func (e ErrorCode) As(err any) bool {
c, ok := err.(*xErrorCode)
if ok {
*c = xErrorCode(e)
}
return ok
}
// Temporary returns true for [BUSY] errors.
func (e ErrorCode) Temporary() bool {
return e == BUSY
return e == BUSY || e == INTERRUPT
}
// ExtendedCode returns the extended error code for this error.
func (e ErrorCode) ExtendedCode() ExtendedErrorCode {
return ExtendedErrorCode(e)
return xErrorCode(e)
}
// Error implements the error interface.
func (e ExtendedErrorCode) Error() string {
return util.ErrorCodeString(uint32(e))
return util.ErrorCodeString(e)
}
// Is tests whether this error matches a given [ErrorCode].
@@ -133,7 +147,7 @@ func (e ExtendedErrorCode) As(err any) bool {
// Temporary returns true for [BUSY] errors.
func (e ExtendedErrorCode) Temporary() bool {
return ErrorCode(e) == BUSY
return ErrorCode(e) == BUSY || ErrorCode(e) == INTERRUPT
}
// Timeout returns true for [BUSY_TIMEOUT] errors.
@@ -158,14 +172,10 @@ func errorCode(err error, def ErrorCode) (msg string, code res_t) {
return code.msg, res_t(code.code)
}
var ecode ErrorCode
var xcode xErrorCode
switch {
case errors.As(err, &xcode):
if errors.As(err, &xcode) {
code = res_t(xcode)
case errors.As(err, &ecode):
code = res_t(ecode)
default:
} else {
code = res_t(def)
}
return err.Error(), code

View File

@@ -43,7 +43,7 @@ func TestError(t *testing.T) {
if !errors.Is(err, xErrorCode(0x8080)) {
t.Errorf("want true")
}
if s := err.Error(); s != "sqlite3: 32896" {
if s := err.Error(); s != "sqlite3: unknown error" {
t.Errorf("got %q", s)
}
if ok := errors.As(err.ExtendedCode(), &ecode); !ok || ecode != ErrorCode(0x80) {
@@ -83,7 +83,7 @@ func TestError_Temporary(t *testing.T) {
}
}
{
err := ExtendedErrorCode(tt.code)
err := xErrorCode(tt.code)
if got := err.Temporary(); got != tt.want {
t.Errorf("ExtendedErrorCode.Temporary(%d) = %v, want %v", tt.code, got, tt.want)
}
@@ -115,7 +115,7 @@ func TestError_Timeout(t *testing.T) {
}
}
{
err := ExtendedErrorCode(tt.code)
err := xErrorCode(tt.code)
if got := err.Timeout(); got != tt.want {
t.Errorf("Error.Timeout(%d) = %v, want %v", tt.code, got, tt.want)
}
@@ -156,12 +156,12 @@ func Test_ExtendedErrorCode_Error(t *testing.T) {
defer db.Close()
// Test all extended error codes.
for i := 0; i == int(ExtendedErrorCode(i)); i++ {
for i := 0; i == int(xErrorCode(i)); i++ {
want := "sqlite3: "
ptr := ptr_t(db.call("sqlite3_errstr", stk_t(i)))
want += util.ReadString(db.mod, ptr, _MAX_NAME)
got := ExtendedErrorCode(i).Error()
got := xErrorCode(i).Error()
if got != want {
t.Fatalf("got %q, want %q, with %d", got, want, i)
}

View File

@@ -30,7 +30,7 @@ you can load into your database connections.
- [`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.
provides [statistics](https://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)
@@ -38,11 +38,11 @@ you can load into your database connections.
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
maps multidimensional data to one dimension.
### Pakages
### Packages
These packages may also be useful to work with SQLite:
- [`github.com/ncruces/decimal`](https://pkg.go.dev/github.com/ncruces/decimal)
decimal arithmetic.
- [`github.com/ncruces/julianday`](https://pkg.go.dev/github.com/ncruces/julianday)
Julian day math.
Julian day math.

View File

@@ -59,7 +59,8 @@ func (c *cursor) Next() error {
}
func (c *cursor) RowID() (int64, error) {
return int64(c.rowID), nil
// One-based RowID for consistency with carray and other tables.
return int64(c.rowID) + 1, nil
}
func (c *cursor) Column(ctx sqlite3.Context, n int) error {

View File

@@ -88,9 +88,9 @@ func Example() {
func Test_cursor_Column(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, array.Register)
db, err := driver.Open(dsn, array.Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -1,6 +1,7 @@
package blobio_test
import (
"database/sql"
"io"
"log"
"os"
@@ -34,7 +35,8 @@ func Example() {
const message = "Hello BLOB!"
// Create the BLOB.
r, err := db.Exec(`INSERT INTO test VALUES (?)`, sqlite3.ZeroBlob(len(message)))
r, err := db.Exec(`INSERT INTO test VALUES (:data)`,
sql.Named("data", sqlite3.ZeroBlob(len(message))))
if err != nil {
log.Fatal(err)
}
@@ -45,15 +47,19 @@ func Example() {
}
// Write the BLOB.
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', ?, 0, ?)`,
id, message)
_, err = db.Exec(`SELECT writeblob('main', 'test', 'col', :rowid, :offset, :message)`,
sql.Named("rowid", id),
sql.Named("offset", 0),
sql.Named("message", message))
if err != nil {
log.Fatal(err)
}
// Read the BLOB.
_, err = db.Exec(`SELECT readblob('main', 'test', 'col', ?, 0, ?)`,
id, sqlite3.Pointer(os.Stdout))
_, err = db.Exec(`SELECT readblob('main', 'test', 'col', :rowid, :offset, :writer)`,
sql.Named("rowid", id),
sql.Named("offset", 0),
sql.Named("writer", sqlite3.Pointer(os.Stdout)))
if err != nil {
log.Fatal(err)
}
@@ -64,7 +70,7 @@ func Example() {
func TestMain(m *testing.M) {
sqlite3.AutoExtension(blobio.Register)
sqlite3.AutoExtension(array.Register)
m.Run()
os.Exit(m.Run())
}
func Test_readblob(t *testing.T) {
@@ -138,18 +144,16 @@ func Test_readblob(t *testing.T) {
}
}
if stmt.Step() {
got := stmt.ColumnText(0)
if got != tt.want1 {
t.Errorf("got %q", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnText(0); got != tt.want1 {
t.Errorf("got %q", got)
}
if stmt.Step() {
got := stmt.ColumnText(0)
if got != tt.want2 {
t.Errorf("got %q", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnText(0); got != tt.want2 {
t.Errorf("got %q", got)
}
err = stmt.Err()

View File

@@ -268,13 +268,13 @@ func (b *bloom) Open() (sqlite3.VTabCursor, error) {
type cursor struct {
*bloom
arg *sqlite3.Value
arg sqlite3.Value
eof bool
}
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
c.eof = false
c.arg = &arg[0]
c.arg = arg[0]
blob := arg[0].RawBlob()
f, err := c.db.OpenBlob(c.schema, c.storage, "data", 1, false)
@@ -312,7 +312,7 @@ func (c *cursor) Column(ctx sqlite3.Context, n int) error {
case 0:
ctx.ResultBool(true)
case 1:
ctx.ResultValue(*c.arg)
ctx.ResultValue(c.arg)
}
return nil
}

View File

@@ -14,7 +14,7 @@ import (
func TestMain(m *testing.M) {
sqlite3.AutoExtension(bloom.Register)
m.Run()
os.Exit(m.Run())
}
func TestRegister(t *testing.T) {

View File

@@ -154,6 +154,7 @@ func (c *closure) BestIndex(idx *sqlite3.IndexInfo) error {
return sqlite3.CONSTRAINT
}
idx.IdxFlags = sqlite3.INDEX_SCAN_HEX
idx.EstimatedCost = cost
idx.IdxNum = plan
return nil

View File

@@ -4,6 +4,7 @@ import (
_ "embed"
"fmt"
"log"
"os"
"testing"
"github.com/ncruces/go-sqlite3"
@@ -14,7 +15,7 @@ import (
func TestMain(m *testing.M) {
sqlite3.AutoExtension(closure.Register)
m.Run()
os.Exit(m.Run())
}
func Example() {

View File

@@ -3,6 +3,7 @@ package csv_test
import (
"fmt"
"log"
"os"
"testing"
"github.com/ncruces/go-sqlite3"
@@ -56,7 +57,7 @@ func Example() {
func TestMain(m *testing.M) {
sqlite3.AutoExtension(csv.Register)
m.Run()
os.Exit(m.Run())
}
func TestRegister(t *testing.T) {
@@ -146,20 +147,21 @@ func TestAffinity(t *testing.T) {
}
defer stmt.Close()
if stmt.Step() {
if got := stmt.ColumnText(0); got != "1" {
t.Errorf("got %q want 1", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else 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() {
t.Fatal(stmt.Err())
} else 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)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnText(0); got != "e" {
t.Errorf("got %q want e", got)
}
}

View File

@@ -18,7 +18,7 @@ func Register(db *sqlite3.Conn) error {
return RegisterFS(db, nil)
}
// Register registers SQL functions readfile, lsmode,
// RegisterFS 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) error {
@@ -42,7 +42,7 @@ func lsmode(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultText(fs.FileMode(arg[0].Int()).String())
}
func readfile(fsys fs.FS) func(ctx sqlite3.Context, arg ...sqlite3.Value) {
func readfile(fsys fs.FS) sqlite3.ScalarFunction {
return func(ctx sqlite3.Context, arg ...sqlite3.Value) {
var err error
var data []byte

View File

@@ -17,9 +17,9 @@ import (
func Test_lsmode(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, fileio.Register)
db, err := driver.Open(dsn, fileio.Register)
if err != nil {
t.Fatal(err)
}
@@ -53,9 +53,9 @@ func Test_readfile(t *testing.T) {
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
t.Run("", func(t *testing.T) {
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, func(c *sqlite3.Conn) error {
db, err := driver.Open(dsn, func(c *sqlite3.Conn) error {
fileio.RegisterFS(c, fsys)
return nil
})

View File

@@ -63,12 +63,12 @@ func (d fsdir) Open() (sqlite3.VTabCursor, error) {
type cursor struct {
fsdir
base string
resume func() (entry, bool)
cancel func()
curr entry
eof bool
rowID int64
base string
next func() (entry, bool)
stop func()
curr entry
eof bool
rowID int64
}
type entry struct {
@@ -78,8 +78,8 @@ type entry struct {
}
func (c *cursor) Close() error {
if c.cancel != nil {
c.cancel()
if c.stop != nil {
c.stop()
}
return nil
}
@@ -102,7 +102,7 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
c.base = base
}
c.resume, c.cancel = iter.Pull(func(yield func(entry) bool) {
c.next, c.stop = 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
@@ -121,7 +121,7 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
}
func (c *cursor) Next() error {
curr, ok := c.resume()
curr, ok := c.next()
c.curr = curr
c.eof = !ok
c.rowID++

View File

@@ -21,9 +21,9 @@ func Test_fsdir(t *testing.T) {
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
t.Run("", func(t *testing.T) {
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, func(c *sqlite3.Conn) error {
db, err := driver.Open(dsn, func(c *sqlite3.Conn) error {
fileio.RegisterFS(c, fsys)
return nil
})

View File

@@ -15,9 +15,9 @@ import (
func Test_writefile(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -4,6 +4,7 @@ import (
_ "crypto/md5"
_ "crypto/sha1"
_ "crypto/sha256"
_ "crypto/sha3"
_ "crypto/sha512"
"testing"
@@ -11,7 +12,6 @@ import (
_ "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"
@@ -21,7 +21,7 @@ import (
func TestRegister(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
tests := []struct {
name string
@@ -55,7 +55,7 @@ func TestRegister(t *testing.T) {
{"blake2b('', 256)", "0E5751C026E543B2E8AB2EB06099DAA1D1E5DF47778F7787FAAB45CDF12FE3A8"},
}
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}

113
ext/ipaddr/ipaddr.go Normal file
View File

@@ -0,0 +1,113 @@
// Package ipaddr provides functions to manipulate IPs and CIDRs.
//
// It provides the following functions:
// - ipcontains(prefix, ip)
// - ipoverlaps(prefix1, prefix2)
// - ipfamily(ip/prefix)
// - iphost(ip/prefix)
// - ipmasklen(prefix)
// - ipnetwork(prefix)
package ipaddr
import (
"errors"
"net/netip"
"github.com/ncruces/go-sqlite3"
)
// Register IP/CIDR functions for a database connection.
func Register(db *sqlite3.Conn) error {
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
return errors.Join(
db.CreateFunction("ipcontains", 2, flags, contains),
db.CreateFunction("ipoverlaps", 2, flags, overlaps),
db.CreateFunction("ipfamily", 1, flags, family),
db.CreateFunction("iphost", 1, flags, host),
db.CreateFunction("ipmasklen", 1, flags, masklen),
db.CreateFunction("ipnetwork", 1, flags, network))
}
func contains(ctx sqlite3.Context, arg ...sqlite3.Value) {
prefix, err := netip.ParsePrefix(arg[0].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
addr, err := netip.ParseAddr(arg[1].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
ctx.ResultBool(prefix.Contains(addr))
}
func overlaps(ctx sqlite3.Context, arg ...sqlite3.Value) {
prefix1, err := netip.ParsePrefix(arg[0].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
prefix2, err := netip.ParsePrefix(arg[0].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
ctx.ResultBool(prefix1.Overlaps(prefix2))
}
func family(ctx sqlite3.Context, arg ...sqlite3.Value) {
addr, err := addr(arg[0].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
switch {
case addr.Is4():
ctx.ResultInt(4)
case addr.Is6():
ctx.ResultInt(6)
}
}
func host(ctx sqlite3.Context, arg ...sqlite3.Value) {
addr, err := addr(arg[0].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
buf, _ := addr.MarshalText()
ctx.ResultRawText(buf)
}
func masklen(ctx sqlite3.Context, arg ...sqlite3.Value) {
prefix, err := netip.ParsePrefix(arg[0].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
ctx.ResultInt(prefix.Bits())
}
func network(ctx sqlite3.Context, arg ...sqlite3.Value) {
prefix, err := netip.ParsePrefix(arg[0].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
buf, _ := prefix.Masked().MarshalText()
ctx.ResultRawText(buf)
}
func addr(text string) (netip.Addr, error) {
addr, err := netip.ParseAddr(text)
if err != nil {
if prefix, err := netip.ParsePrefix(text); err == nil {
return prefix.Addr(), nil
}
if addrpt, err := netip.ParseAddrPort(text); err == nil {
return addrpt.Addr(), nil
}
}
return addr, err
}

88
ext/ipaddr/ipaddr_test.go Normal file
View File

@@ -0,0 +1,88 @@
package ipaddr_test
import (
"testing"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/ext/ipaddr"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func TestRegister(t *testing.T) {
t.Parallel()
dsn := memdb.TestDB(t)
db, err := driver.Open(dsn, ipaddr.Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var got string
err = db.QueryRow(`SELECT ipfamily('::1')`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != "6" {
t.Fatalf("got %s", got)
}
err = db.QueryRow(`SELECT ipfamily('[::1]:80')`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != "6" {
t.Fatalf("got %s", got)
}
err = db.QueryRow(`SELECT ipfamily('192.168.1.5/24')`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != "4" {
t.Fatalf("got %s", got)
}
err = db.QueryRow(`SELECT iphost('192.168.1.5/24')`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != "192.168.1.5" {
t.Fatalf("got %s", got)
}
err = db.QueryRow(`SELECT ipmasklen('192.168.1.5/24')`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != "24" {
t.Fatalf("got %s", got)
}
err = db.QueryRow(`SELECT ipnetwork('192.168.1.5/24')`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != "192.168.1.0/24" {
t.Fatalf("got %s", got)
}
err = db.QueryRow(`SELECT ipcontains('192.168.1.0/24', '192.168.1.5')`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != "1" {
t.Fatalf("got %s", got)
}
err = db.QueryRow(`SELECT ipoverlaps('192.168.1.0/24', '192.168.1.5/32')`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != "1" {
t.Fatalf("got %s", got)
}
}

View File

@@ -67,9 +67,9 @@ func Example() {
func Test_lines(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, lines.Register)
db, err := driver.Open(dsn, lines.Register)
if err != nil {
log.Fatal(err)
}
@@ -98,9 +98,9 @@ func Test_lines(t *testing.T) {
func Test_lines_error(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, lines.Register)
db, err := driver.Open(dsn, lines.Register)
if err != nil {
log.Fatal(err)
}
@@ -123,9 +123,9 @@ func Test_lines_error(t *testing.T) {
func Test_lines_read(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, lines.Register)
db, err := driver.Open(dsn, lines.Register)
if err != nil {
log.Fatal(err)
}
@@ -155,9 +155,9 @@ func Test_lines_read(t *testing.T) {
func Test_lines_test(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, lines.Register)
db, err := driver.Open(dsn, lines.Register)
if err != nil {
log.Fatal(err)
}

View File

@@ -3,6 +3,7 @@ package pivot_test
import (
"fmt"
"log"
"os"
"strings"
"testing"
@@ -85,7 +86,7 @@ func Example() {
func TestMain(m *testing.M) {
sqlite3.AutoExtension(pivot.Register)
m.Run()
os.Exit(m.Run())
}
func TestRegister(t *testing.T) {
@@ -140,10 +141,10 @@ func TestRegister(t *testing.T) {
}
defer stmt.Close()
if stmt.Step() {
if got := stmt.ColumnInt(0); got != 3 {
t.Errorf("got %d, want 3", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnInt(0); got != 3 {
t.Errorf("got %d, want 3", got)
}
err = db.Exec(`ALTER TABLE v_x RENAME TO v_y`)

View File

@@ -111,22 +111,24 @@ loop2:
return glob.String()
}
func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
func load(ctx sqlite3.Context, arg []sqlite3.Value, i int) (*regexp.Regexp, error) {
re, ok := ctx.GetAuxData(i).(*regexp.Regexp)
if !ok {
r, err := regexp.Compile(expr)
if err != nil {
return nil, err
re, ok = arg[i].Pointer().(*regexp.Regexp)
if !ok {
r, err := regexp.Compile(arg[i].Text())
if err != nil {
return nil, err
}
re = r
}
re = r
ctx.SetAuxData(0, r)
ctx.SetAuxData(i, re)
}
return re, nil
}
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
_ = arg[1] // bounds check
re, err := load(ctx, 0, arg[0].Text())
re, err := load(ctx, arg, 0)
if err != nil {
ctx.ResultError(err)
return // notest
@@ -136,18 +138,17 @@ func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
func regexLike(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 1, arg[1].Text())
re, err := load(ctx, arg, 1)
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())
re, err := load(ctx, arg, 1)
if err != nil {
ctx.ResultError(err)
return // notest
@@ -162,7 +163,7 @@ func regexCount(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 1, arg[1].Text())
re, err := load(ctx, arg, 1)
if err != nil {
ctx.ResultError(err)
return // notest
@@ -187,7 +188,7 @@ func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
func regexInstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 1, arg[1].Text())
re, err := load(ctx, arg, 1)
if err != nil {
ctx.ResultError(err)
return // notest
@@ -215,16 +216,14 @@ func regexInstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
func regexReplace(ctx sqlite3.Context, arg ...sqlite3.Value) {
_ = arg[2] // bounds check
re, err := load(ctx, 1, arg[1].Text())
re, err := load(ctx, arg, 1)
if err != nil {
ctx.ResultError(err)
return // notest
}
text := arg[0].RawText()
repl := arg[2].RawText()
text := arg[0].RawText()
var pos, n int
if len(arg) > 3 {
pos = arg[3].Int()

View File

@@ -6,6 +6,7 @@ import (
"strings"
"testing"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
@@ -14,9 +15,9 @@ import (
func TestRegister(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}
@@ -37,7 +38,7 @@ func TestRegister(t *testing.T) {
{`regexp_instr('Hello', '.', 6)`, ""},
{`regexp_substr('Hello', 'el.')`, "ell"},
{`regexp_replace('Hello', 'llo', 'll')`, "Hell"},
// https://www.postgresql.org/docs/current/functions-matching.html
// https://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"},
@@ -79,9 +80,9 @@ func TestRegister(t *testing.T) {
func TestRegister_errors(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}
@@ -104,6 +105,27 @@ func TestRegister_errors(t *testing.T) {
}
}
func TestRegister_pointer(t *testing.T) {
t.Parallel()
dsn := memdb.TestDB(t)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var got int
err = db.QueryRow(`SELECT regexp_count('ABCABCAXYaxy', ?, 1)`,
sqlite3.Pointer(regexp.MustCompile(`(?i)A.`))).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != 4 {
t.Errorf("got %d, want %d", got, 4)
}
}
func TestGlobPrefix(t *testing.T) {
tests := []struct {
re string

View File

@@ -2,9 +2,8 @@
package serdes
import (
"io"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/util/vfsutil"
"github.com/ncruces/go-sqlite3/vfs"
)
@@ -14,16 +13,16 @@ func init() {
vfs.Register(vfsName, sliceVFS{})
}
var fileToOpen = make(chan *sliceFile, 1)
var fileToOpen = make(chan *[]byte, 1)
// Serialize backs up a database into a byte slice.
//
// https://sqlite.org/c3ref/serialize.html
func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) {
var file sliceFile
var file []byte
fileToOpen <- &file
err := db.Backup(schema, "file:serdes.db?vfs="+vfsName)
return file.data, err
err := db.Backup(schema, "file:serdes.db?nolock=1&vfs="+vfsName)
return file, err
}
// Deserialize restores a database from a byte slice,
@@ -41,8 +40,8 @@ func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) {
// ["memdb"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb
// ["reader"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs
func Deserialize(db *sqlite3.Conn, schema string, data []byte) error {
fileToOpen <- &sliceFile{data}
return db.Restore(schema, "file:serdes.db?vfs="+vfsName)
fileToOpen <- &data
return db.Restore(schema, "file:serdes.db?immutable=1&vfs="+vfsName)
}
type sliceVFS struct{}
@@ -53,14 +52,14 @@ func (sliceVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, e
}
select {
case file := <-fileToOpen:
return file, flags | vfs.OPEN_MEMORY, nil
return (*vfsutil.SliceFile)(file), flags | vfs.OPEN_MEMORY, nil
default:
return nil, flags, sqlite3.MISUSE
}
}
func (sliceVFS) Delete(name string, dirSync bool) error {
// notest // OPEN_MEMORY
// notest // no journals to delete
return sqlite3.IOERR_DELETE
}
@@ -71,70 +70,3 @@ func (sliceVFS) Access(name string, flag vfs.AccessFlag) (bool, error) {
func (sliceVFS) FullPathname(name string) (string, error) {
return name, nil
}
type sliceFile struct{ data []byte }
func (f *sliceFile) ReadAt(b []byte, off int64) (n int, err error) {
if d := f.data; off < int64(len(d)) {
n = copy(b, d[off:])
}
if n == 0 {
err = io.EOF
}
return
}
func (f *sliceFile) WriteAt(b []byte, off int64) (n int, err error) {
if d := f.data; off > int64(len(d)) {
f.data = append(d, make([]byte, off-int64(len(d)))...)
}
d := append(f.data[:off], b...)
if len(d) > len(f.data) {
f.data = d
}
return len(b), nil
}
func (f *sliceFile) Size() (int64, error) {
return int64(len(f.data)), nil
}
func (f *sliceFile) Truncate(size int64) error {
if d := f.data; size < int64(len(d)) {
f.data = d[:size]
}
return nil
}
func (f *sliceFile) SizeHint(size int64) error {
if d := f.data; size > int64(len(d)) {
f.data = append(d, make([]byte, size-int64(len(d)))...)
}
return nil
}
func (*sliceFile) Close() error { return nil }
func (*sliceFile) Sync(flag vfs.SyncFlag) error { return nil }
func (*sliceFile) Lock(lock vfs.LockLevel) error { return nil }
func (*sliceFile) Unlock(lock vfs.LockLevel) error { return nil }
func (*sliceFile) CheckReservedLock() (bool, error) {
// notest // OPEN_MEMORY
return false, nil
}
func (*sliceFile) SectorSize() int {
// notest // IOCAP_POWERSAFE_OVERWRITE
return 0
}
func (*sliceFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
return vfs.IOCAP_ATOMIC |
vfs.IOCAP_SAFE_APPEND |
vfs.IOCAP_SEQUENTIAL |
vfs.IOCAP_POWERSAFE_OVERWRITE |
vfs.IOCAP_SUBPAGE_READ
}

View File

@@ -1,6 +1,7 @@
package serdes_test
import (
_ "embed"
"errors"
"io"
"net/http"
@@ -11,7 +12,30 @@ import (
"github.com/ncruces/go-sqlite3/ext/serdes"
)
func TestDeserialize(t *testing.T) {
//go:embed testdata/wal.db
var walDB []byte
func Test_wal(t *testing.T) {
db, err := sqlite3.Open("testdata/wal.db")
if err != nil {
t.Fatal(err)
}
defer db.Close()
data, err := serdes.Serialize(db, "main")
if err != nil {
t.Fatal(err)
}
compareDBs(t, data, walDB)
err = serdes.Deserialize(db, "temp", walDB)
if err != nil {
t.Fatal(err)
}
}
func Test_northwind(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
@@ -37,10 +61,14 @@ func TestDeserialize(t *testing.T) {
t.Fatal(err)
}
if len(input) != len(output) {
compareDBs(t, input, output)
}
func compareDBs(t *testing.T, a, b []byte) {
if len(a) != len(b) {
t.Fatal("lengths are different")
}
for i := range input {
for i := range a {
// These may be different.
switch {
case 24 <= i && i < 28:
@@ -53,14 +81,14 @@ func TestDeserialize(t *testing.T) {
// SQLite version that wrote the file.
continue
}
if input[i] != output[i] {
t.Errorf("difference at %d: %d %d", i, input[i], output[i])
if a[i] != b[i] {
t.Errorf("difference at %d: %d %d", i, a[i], b[i])
}
}
}
func httpGet() ([]byte, error) {
res, err := http.Get("https://raw.githubusercontent.com/jpwhite3/northwind-SQLite3/refs/heads/main/dist/northwind.db")
res, err := http.Get("https://github.com/jpwhite3/northwind-SQLite3/raw/refs/heads/main/dist/northwind.db")
if err != nil {
return nil, err
}

BIN
ext/serdes/testdata/wal.db vendored Normal file

Binary file not shown.

View File

@@ -159,8 +159,6 @@ func (c *cursor) Close() error {
}
func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
c.arg = arg
c.rowID = 0
err := errors.Join(
c.stmt.Reset(),
c.stmt.ClearBindings())
@@ -187,6 +185,8 @@ func (c *cursor) Filter(idxNum int, idxStr string, arg ...sqlite3.Value) error {
return err
}
}
c.arg = append(c.arg[:0], arg...)
c.rowID = 0
return c.Next()
}

View File

@@ -3,6 +3,7 @@ package statement_test
import (
"fmt"
"log"
"os"
"testing"
"github.com/ncruces/go-sqlite3"
@@ -50,7 +51,7 @@ func Example() {
func TestMain(m *testing.M) {
sqlite3.AutoExtension(statement.Register)
m.Run()
os.Exit(m.Run())
}
func TestRegister(t *testing.T) {
@@ -91,7 +92,9 @@ func TestRegister(t *testing.T) {
}
defer stmt.Close()
if stmt.Step() {
if !stmt.Step() {
t.Fatal(stmt.Err())
} else {
x := stmt.ColumnInt(0)
y := stmt.ColumnInt(1)
hypot := stmt.ColumnInt(2)

View File

@@ -1,6 +1,6 @@
# ANSI SQL Aggregate Functions
https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
https://oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
## Built in aggregates

View File

@@ -7,7 +7,7 @@ const (
some
)
func newBoolean(kind int) func() sqlite3.AggregateFunction {
func newBoolean(kind int) sqlite3.AggregateConstructor {
return func() sqlite3.AggregateFunction { return &boolean{kind: kind} }
}

View File

@@ -37,7 +37,9 @@ func TestRegister_boolean(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if !stmt.Step() {
t.Fatal(stmt.Err())
} else {
if got := stmt.ColumnBool(0); got != true {
t.Errorf("got %v, want true", got)
}

View File

@@ -19,8 +19,8 @@ type mode struct {
func (m mode) Value(ctx sqlite3.Context) {
var (
max = 0
typ = sqlite3.NULL
max uint
i64 int64
f64 float64
str string
@@ -32,7 +32,6 @@ func (m mode) Value(ctx sqlite3.Context) {
i64 = k
}
}
f64 = float64(i64)
for k, v := range m.reals {
if v > max || v == max && k < f64 {
typ = sqlite3.FLOAT
@@ -66,33 +65,45 @@ func (m mode) Value(ctx sqlite3.Context) {
}
}
func (b *mode) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
func (m *mode) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
switch arg[0].Type() {
case sqlite3.INTEGER:
b.ints.add(arg[0].Int64())
if m.reals == nil {
m.ints.add(arg[0].Int64())
break
}
fallthrough
case sqlite3.FLOAT:
b.reals.add(arg[0].Float())
m.reals.add(arg[0].Float())
for k, v := range m.ints {
m.reals[float64(k)] += v
}
m.ints = nil
case sqlite3.TEXT:
b.texts.add(arg[0].Text())
m.texts.add(arg[0].Text())
case sqlite3.BLOB:
b.blobs.add(string(arg[0].RawBlob()))
m.blobs.add(string(arg[0].RawBlob()))
}
}
func (b *mode) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
func (m *mode) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
switch arg[0].Type() {
case sqlite3.INTEGER:
b.ints.del(arg[0].Int64())
if m.reals == nil {
m.ints.del(arg[0].Int64())
break
}
fallthrough
case sqlite3.FLOAT:
b.reals.del(arg[0].Float())
m.reals.del(arg[0].Float())
case sqlite3.TEXT:
b.texts.del(arg[0].Text())
m.texts.del(arg[0].Text())
case sqlite3.BLOB:
b.blobs.del(string(arg[0].RawBlob()))
m.blobs.del(string(arg[0].RawBlob()))
}
}
type counter[T comparable] map[T]int
type counter[T comparable] map[T]uint
func (c *counter[T]) add(k T) {
if (*c) == nil {
@@ -102,11 +113,9 @@ func (c *counter[T]) add(k T) {
}
func (c counter[T]) del(k T) {
switch n := c[k]; n {
default:
c[k] = n - 1
case 1:
if n := c[k]; n == 1 {
delete(c, k)
case 0:
} else {
c[k] = n - 1
}
}

View File

@@ -21,10 +21,10 @@ func TestRegister_mode(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnInt(0); got != 3 {
t.Errorf("got %v, want 3", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnInt(0); got != 3 {
t.Errorf("got %v, want 3", got)
}
stmt.Close()
@@ -32,10 +32,10 @@ func TestRegister_mode(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnInt(0); got != 1 {
t.Errorf("got %v, want 1", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnInt(0); got != 1 {
t.Errorf("got %v, want 1", got)
}
stmt.Close()
@@ -43,10 +43,10 @@ func TestRegister_mode(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 2.5 {
t.Errorf("got %v, want 2.5", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnFloat(0); got != 2.5 {
t.Errorf("got %v, want 2.5", got)
}
stmt.Close()
@@ -54,21 +54,22 @@ func TestRegister_mode(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnText(0); got != "red" {
t.Errorf("got %q, want red", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnText(0); got != "red" {
t.Errorf("got %q, want red", got)
}
stmt.Close()
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES (X'cafebabe'), ('green'), ('blue'), (X'cafebabe'))`)
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnText(0); got != "\xca\xfe\xba\xbe" {
t.Errorf("got %q, want cafebabe", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnText(0); got != "\xca\xfe\xba\xbe" {
t.Errorf("got %q, want cafebabe", got)
}
stmt.Close()
@@ -82,4 +83,20 @@ func TestRegister_mode(t *testing.T) {
for stmt.Step() {
}
stmt.Close()
stmt, _, err = db.Prepare(`SELECT mode(column1) FROM (VALUES (?), (?), (?), (?), (?))`)
if err != nil {
t.Fatal(err)
}
stmt.BindInt(1, 1)
stmt.BindInt(2, 1)
stmt.BindInt(3, 2)
stmt.BindFloat(4, 2)
stmt.BindFloat(5, 2)
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnInt(0); got != 2 {
t.Errorf("got %v, want 2", got)
}
stmt.Close()
}

View File

@@ -21,7 +21,7 @@ const (
percentile_disc
)
func newPercentile(kind int) func() sqlite3.AggregateFunction {
func newPercentile(kind int) sqlite3.AggregateConstructor {
return func() sqlite3.AggregateFunction { return &percentile{kind: kind} }
}

View File

@@ -38,7 +38,9 @@ func TestRegister_percentile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if !stmt.Step() {
t.Fatal(stmt.Err())
} else {
if got := stmt.ColumnFloat(0); got != 10 {
t.Errorf("got %v, want 10", got)
}
@@ -65,30 +67,30 @@ func TestRegister_percentile(t *testing.T) {
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() {
t.Fatal(stmt.Err())
} else 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() {
t.Fatal(stmt.Err())
} else 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() {
t.Fatal(stmt.Err())
} else 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() {
t.Fatal(stmt.Err())
} else 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)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnFloat(0); got != 16 {
t.Errorf("got %v, want 16", got)
}
stmt.Close()
@@ -103,7 +105,9 @@ func TestRegister_percentile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if !stmt.Step() {
t.Fatal(stmt.Err())
} else {
if got := stmt.ColumnFloat(0); got != 4 {
t.Errorf("got %v, want 4", got)
}
@@ -134,7 +138,9 @@ func TestRegister_percentile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if !stmt.Step() {
t.Fatal(stmt.Err())
} else {
if got := stmt.ColumnType(0); got != sqlite3.NULL {
t.Error("want NULL")
}

View File

@@ -47,7 +47,7 @@
//
// [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
// [ANSI SQL Aggregate Functions]: https://oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html
package stats
import (
@@ -58,8 +58,11 @@ import (
// Register registers statistics functions.
func Register(db *sqlite3.Conn) error {
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
const order = sqlite3.SELFORDER1 | flags
const (
flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
json = sqlite3.RESULT_SUBTYPE | flags
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)),
@@ -81,7 +84,7 @@ func Register(db *sqlite3.Conn) error {
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("regr_json", 2, json, 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)),
@@ -130,7 +133,7 @@ func special(kind int, n int64) (null, zero bool) {
}
}
func newVariance(kind int) func() sqlite3.AggregateFunction {
func newVariance(kind int) sqlite3.AggregateConstructor {
return func() sqlite3.AggregateFunction { return &variance{kind: kind} }
}
@@ -178,7 +181,7 @@ func (fn *variance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
}
func newCovariance(kind int) func() sqlite3.AggregateFunction {
func newCovariance(kind int) sqlite3.AggregateConstructor {
return func() sqlite3.AggregateFunction { return &covariance{kind: kind} }
}
@@ -227,6 +230,7 @@ func (fn *covariance) Value(ctx sqlite3.Context) {
case regr_json:
var buf [128]byte
ctx.ResultRawText(fn.regr_json(buf[:0]))
ctx.ResultSubtype('J')
return
}
ctx.ResultFloat(r)
@@ -254,7 +258,7 @@ func (fn *covariance) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
}
func newMoments(kind int) func() sqlite3.AggregateFunction {
func newMoments(kind int) sqlite3.AggregateConstructor {
return func() sqlite3.AggregateFunction { return &momentfn{kind: kind} }
}

View File

@@ -2,6 +2,7 @@ package stats_test
import (
"math"
"os"
"testing"
"github.com/ncruces/go-sqlite3"
@@ -12,7 +13,7 @@ import (
func TestMain(m *testing.M) {
sqlite3.AutoExtension(stats.Register)
m.Run()
os.Exit(m.Run())
}
func TestRegister_variance(t *testing.T) {
@@ -33,10 +34,10 @@ func TestRegister_variance(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != sqlite3.NULL {
t.Errorf("got %v, want NULL", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
} else if got := stmt.ColumnType(0); got != sqlite3.NULL {
t.Errorf("got %v, want NULL", got)
}
stmt.Close()
@@ -56,7 +57,9 @@ func TestRegister_variance(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if !stmt.Step() {
t.Fatal(stmt.Err())
} else {
if got := stmt.ColumnFloat(0); got != 40 {
t.Errorf("got %v, want 40", got)
}
@@ -130,7 +133,9 @@ func TestRegister_covariance(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if !stmt.Step() {
t.Fatal(stmt.Err())
} else {
if got := stmt.ColumnInt(0); got != 0 {
t.Errorf("got %v, want 0", got)
}
@@ -155,49 +160,50 @@ func TestRegister_covariance(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt.Step() {
if got := stmt.ColumnFloat(0); got != 0.9881049293224639 {
t.Errorf("got %v, want 0.9881049293224639", got)
}
if got := stmt.ColumnFloat(1); got != 21.25 {
t.Errorf("got %v, want 21.25", got)
}
if got := stmt.ColumnFloat(2); got != 17 {
t.Errorf("got %v, want 17", got)
}
if got := stmt.ColumnFloat(3); got != 4.2 {
t.Errorf("got %v, want 4.2", got)
}
if got := stmt.ColumnFloat(4); got != 75 {
t.Errorf("got %v, want 75", got)
}
if got := stmt.ColumnFloat(5); got != 14.8 {
t.Errorf("got %v, want 14.8", got)
}
if got := stmt.ColumnFloat(6); got != 500 {
t.Errorf("got %v, want 500", got)
}
if got := stmt.ColumnFloat(7); got != 85 {
t.Errorf("got %v, want 85", got)
}
if got := stmt.ColumnFloat(8); got != 0.17 {
t.Errorf("got %v, want 0.17", got)
}
if got := stmt.ColumnFloat(9); got != -8.55 {
t.Errorf("got %v, want -8.55", got)
}
if got := stmt.ColumnFloat(10); got != 0.9763513513513513 {
t.Errorf("got %v, want 0.9763513513513513", got)
}
if got := stmt.ColumnInt(11); got != 5 {
t.Errorf("got %v, want 5", got)
}
var a map[string]float64
if err := stmt.ColumnJSON(12, &a); err != nil {
t.Error(err)
} else if got := a["count"]; got != 5 {
t.Errorf("got %v, want 5", got)
}
if !stmt.Step() {
t.Fatal(stmt.Err())
}
if got := stmt.ColumnFloat(0); got != 0.9881049293224639 {
t.Errorf("got %v, want 0.9881049293224639", got)
}
if got := stmt.ColumnFloat(1); got != 21.25 {
t.Errorf("got %v, want 21.25", got)
}
if got := stmt.ColumnFloat(2); got != 17 {
t.Errorf("got %v, want 17", got)
}
if got := stmt.ColumnFloat(3); got != 4.2 {
t.Errorf("got %v, want 4.2", got)
}
if got := stmt.ColumnFloat(4); got != 75 {
t.Errorf("got %v, want 75", got)
}
if got := stmt.ColumnFloat(5); got != 14.8 {
t.Errorf("got %v, want 14.8", got)
}
if got := stmt.ColumnFloat(6); got != 500 {
t.Errorf("got %v, want 500", got)
}
if got := stmt.ColumnFloat(7); got != 85 {
t.Errorf("got %v, want 85", got)
}
if got := stmt.ColumnFloat(8); got != 0.17 {
t.Errorf("got %v, want 0.17", got)
}
if got := stmt.ColumnFloat(9); got != -8.55 {
t.Errorf("got %v, want -8.55", got)
}
if got := stmt.ColumnFloat(10); got != 0.9763513513513513 {
t.Errorf("got %v, want 0.9763513513513513", got)
}
if got := stmt.ColumnInt(11); got != 5 {
t.Errorf("got %v, want 5", got)
}
var a map[string]float64
if err := stmt.ColumnJSON(12, &a); err != nil {
t.Error(err)
} else if got := a["count"]; got != 5 {
t.Errorf("got %v, want 5", got)
}
stmt.Close()
@@ -247,7 +253,9 @@ func Benchmark_average(b *testing.B) {
b.Fatal(err)
}
if stmt.Step() {
if !stmt.Step() {
b.Fatal(stmt.Err())
} else {
want := float64(b.N) / 2
if got := stmt.ColumnFloat(0); got != want {
b.Errorf("got %v, want %v", got, want)
@@ -281,7 +289,9 @@ func Benchmark_variance(b *testing.B) {
b.Fatal(err)
}
if stmt.Step() && b.N > 100 {
if !stmt.Step() {
b.Fatal(stmt.Err())
} else if b.N > 100 {
want := float64(b.N*b.N) / 12
if got := stmt.ColumnFloat(0); want > (got-want)*float64(b.N) {
b.Errorf("got %v, want %v", got, want)

View File

@@ -1,21 +1,22 @@
// Package unicode provides an alternative to the SQLite ICU extension.
//
// Like the [ICU extension], it provides Unicode aware:
// - upper() and lower() functions,
// - LIKE and REGEXP operators,
// - collation sequences.
// - upper() and lower() functions
// - LIKE and REGEXP operators
// - collation sequences
//
// The implementation is not 100% compatible with the [ICU extension]:
// - upper() and lower() use [strings.ToUpper], [strings.ToLower] and [cases];
// - the LIKE operator follows [strings.EqualFold] rules;
// - the REGEXP operator uses Go [regexp/syntax];
// - collation sequences use [collate].
// Like PostgreSQL, it also provides:
// - initcap()
// - casefold()
// - normalize()
// - unaccent()
//
// It also provides (approximately) from PostgreSQL:
// - casefold(),
// - initcap(),
// - normalize(),
// - unaccent().
// The implementations are not 100% compatible:
// - upper(), lower(), initcap() casefold() use [strings.ToUpper], [strings.ToLower], [strings.Title] and [cases]
// - normalize(), unaccent() use [transform] and [unicode.Mn]
// - the LIKE operator follows [strings.EqualFold] rules
// - the REGEXP operator uses Go [regexp/syntax]
// - collation sequences use [collate]
//
// Expect subtle differences (e.g.) in the handling of Turkish case folding.
//
@@ -27,6 +28,7 @@ import (
"errors"
"regexp"
"strings"
"sync"
"unicode"
"unicode/utf8"
@@ -41,7 +43,7 @@ import (
"github.com/ncruces/go-sqlite3/internal/util"
)
// Set RegisterLike to false to not register a Unicode aware LIKE operator.
// RegisterLike must be set 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
@@ -113,9 +115,8 @@ func upper(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultError(err)
return // notest
}
c := cases.Upper(t)
ctx.SetAuxData(1, c)
cs = c
cs = cases.Upper(t)
ctx.SetAuxData(1, cs)
}
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
}
@@ -132,9 +133,8 @@ func lower(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultError(err)
return // notest
}
c := cases.Lower(t)
ctx.SetAuxData(1, c)
cs = c
cs = cases.Lower(t)
ctx.SetAuxData(1, cs)
}
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
}
@@ -151,9 +151,8 @@ func initcap(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultError(err)
return // notest
}
c := cases.Title(t)
ctx.SetAuxData(1, c)
cs = c
cs = cases.Title(t)
ctx.SetAuxData(1, cs)
}
ctx.ResultRawText(cs.Bytes(arg[0].RawText()))
}
@@ -162,8 +161,16 @@ func casefold(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultRawText(cases.Fold().Bytes(arg[0].RawText()))
}
var unaccentPool = sync.Pool{
New: func() any {
return transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
},
}
func unaccent(ctx sqlite3.Context, arg ...sqlite3.Value) {
unaccent := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
unaccent := unaccentPool.Get().(transform.Transformer)
defer unaccentPool.Put(unaccent)
res, _, err := transform.Bytes(unaccent, arg[0].RawText())
if err != nil {
ctx.ResultError(err) // notest
@@ -200,13 +207,16 @@ func normalize(ctx sqlite3.Context, arg ...sqlite3.Value) {
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 // notest
re, ok = arg[0].Pointer().(*regexp.Regexp)
if !ok {
r, err := regexp.Compile(arg[0].Text())
if err != nil {
ctx.ResultError(err)
return // notest
}
re = r
}
re = r
ctx.SetAuxData(0, r)
ctx.SetAuxData(0, re)
}
ctx.ResultBool(re.Match(arg[1].RawText()))
}

View File

@@ -26,11 +26,10 @@ func TestRegister(t *testing.T) {
}
defer stmt.Close()
if stmt.Step() {
return stmt.ColumnText(0)
if !stmt.Step() {
t.Fatal(stmt.Err())
}
t.Fatal(stmt.Err())
return ""
return stmt.ColumnText(0)
}
Register(db)

View File

@@ -17,17 +17,18 @@ import (
// 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.
// - uuid([ version [, domain/namespace, [ id/data ]]]):
// to generate a UUID as a string
// - uuid_str(u):
// to convert a UUID into a well-formed UUID string
// - uuid_blob(u):
// to convert a UUID into a 16-byte blob
// - uuid_extract_version(u):
// to extract the version of a RFC 4122 UUID
// - uuid_extract_timestamp(u):
// to extract the timestamp of a version 1/2/6/7 UUID
// - gen_random_uuid(u):
// to generate a version 4 (random) UUID
func Register(db *sqlite3.Conn) error {
const flags = sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
return errors.Join(
@@ -38,7 +39,8 @@ func Register(db *sqlite3.Conn) error {
db.CreateFunction("uuid_str", 1, flags, toString),
db.CreateFunction("uuid_blob", 1, flags, toBlob),
db.CreateFunction("uuid_extract_version", 1, flags, version),
db.CreateFunction("uuid_extract_timestamp", 1, flags, timestamp))
db.CreateFunction("uuid_extract_timestamp", 1, flags, timestamp),
db.CreateFunction("gen_random_uuid", 0, sqlite3.INNOCUOUS, generate))
}
func generate(ctx sqlite3.Context, arg ...sqlite3.Value) {
@@ -192,7 +194,7 @@ func timestamp(ctx sqlite3.Context, arg ...sqlite3.Value) {
switch u.Version() {
case 1, 2, 6, 7:
ctx.ResultTime(
time.Unix(u.Time().UnixTime()),
time.Unix(u.Time().UnixTime()).UTC(),
sqlite3.TimeFormatDefault)
}
}

View File

@@ -14,9 +14,9 @@ import (
func Test_generate(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}
@@ -153,9 +153,9 @@ func Test_generate(t *testing.T) {
func Test_convert(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -19,9 +19,9 @@ func Register(db *sqlite3.Conn) error {
}
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"))
var x [24]int64
if n := len(arg); n < 2 || n > 24 {
ctx.ResultError(util.ErrorString("zorder: needs between 2 and 24 dimensions"))
return
}
for i := range arg {
@@ -29,17 +29,15 @@ func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
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 63 {
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"))
ctx.ResultError(util.ErrorString("zorder: argument out of range"))
return
}
}
@@ -51,6 +49,19 @@ func unzorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
n := arg[1].Int64()
z := arg[0].Int64()
if n < 2 || n > 24 {
ctx.ResultError(util.ErrorString("unzorder: needs between 2 and 24 dimensions"))
return
}
if i < 0 || i >= n {
ctx.ResultError(util.ErrorString("unzorder: index out of range"))
return
}
if z < 0 {
ctx.ResultError(util.ErrorString("unzorder: argument out of range"))
return
}
var k int
var x int64
for j := i; j < 63; j += n {

View File

@@ -12,11 +12,11 @@ import (
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func TestRegister_zorder(t *testing.T) {
func Test_zorder(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, zorder.Register)
db, err := driver.Open(dsn, zorder.Register)
if err != nil {
t.Fatal(err)
}
@@ -57,11 +57,11 @@ func TestRegister_zorder(t *testing.T) {
}
}
func TestRegister_unzorder(t *testing.T) {
func Test_unzorder(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, zorder.Register)
db, err := driver.Open(dsn, zorder.Register)
if err != nil {
t.Fatal(err)
}
@@ -85,11 +85,11 @@ func TestRegister_unzorder(t *testing.T) {
}
}
func TestRegister_error(t *testing.T) {
func Test_zorder_error(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, zorder.Register)
db, err := driver.Open(dsn, zorder.Register)
if err != nil {
t.Fatal(err)
}
@@ -103,7 +103,7 @@ func TestRegister_error(t *testing.T) {
var buf strings.Builder
buf.WriteString("SELECT zorder(0")
for i := 1; i < 80; i++ {
for i := 1; i < 25; i++ {
buf.WriteByte(',')
buf.WriteString(strconv.Itoa(0))
}
@@ -113,3 +113,30 @@ func TestRegister_error(t *testing.T) {
t.Error("want error")
}
}
func Test_unzorder_error(t *testing.T) {
t.Parallel()
dsn := memdb.TestDB(t)
db, err := driver.Open(dsn, zorder.Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var got int64
err = db.QueryRow(`SELECT unzorder(-1, 2, 0)`).Scan(&got)
if err == nil {
t.Error("want error")
}
err = db.QueryRow(`SELECT unzorder(0, 2, 2)`).Scan(&got)
if err == nil {
t.Error("want error")
}
err = db.QueryRow(`SELECT unzorder(0, 25, 2)`).Scan(&got)
if err == nil {
t.Error("want error")
}
}

149
func.go
View File

@@ -3,7 +3,9 @@ package sqlite3
import (
"context"
"io"
"iter"
"sync"
"sync/atomic"
"github.com/tetratelabs/wazero/api"
@@ -45,7 +47,7 @@ func (c Conn) AnyCollationNeeded() error {
// 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 {
func (c *Conn) CreateCollation(name string, fn CollatingFunction) error {
var funcPtr ptr_t
defer c.arena.mark()()
namePtr := c.arena.string(name)
@@ -57,6 +59,10 @@ func (c *Conn) CreateCollation(name string, fn func(a, b []byte) int) error {
return c.error(rc)
}
// CollatingFunction is the type of a collation callback.
// Implementations must not retain a or b.
type CollatingFunction func(a, b []byte) int
// CreateFunction defines a new scalar SQL function.
//
// https://sqlite.org/c3ref/create_function.html
@@ -77,34 +83,67 @@ func (c *Conn) CreateFunction(name string, nArg int, flag FunctionFlag, fn Scala
// Implementations must not retain arg.
type ScalarFunction func(ctx Context, arg ...Value)
// CreateWindowFunction defines a new aggregate or aggregate window SQL function.
// If fn returns a [WindowFunction], then an aggregate window function is created.
// If fn returns an [io.Closer], it will be called to free resources.
// CreateAggregateFunction defines a new aggregate SQL function.
//
// https://sqlite.org/c3ref/create_function.html
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn func() AggregateFunction) error {
func (c *Conn) CreateAggregateFunction(name string, nArg int, flag FunctionFlag, fn AggregateSeqFunction) error {
var funcPtr ptr_t
defer c.arena.mark()()
namePtr := c.arena.string(name)
call := "sqlite3_create_aggregate_function_go"
if fn != nil {
agg := fn()
if c, ok := agg.(io.Closer); ok {
if err := c.Close(); err != nil {
return err
funcPtr = util.AddHandle(c.ctx, AggregateConstructor(func() AggregateFunction {
var a aggregateFunc
coro := func(yieldCoro func(struct{}) bool) {
seq := func(yieldSeq func([]Value) bool) {
for yieldSeq(a.arg) {
if !yieldCoro(struct{}{}) {
break
}
}
}
fn(&a.ctx, seq)
}
}
if _, ok := agg.(WindowFunction); ok {
call = "sqlite3_create_window_function_go"
}
funcPtr = util.AddHandle(c.ctx, fn)
a.next, a.stop = iter.Pull(coro)
return &a
}))
}
rc := res_t(c.call(call,
rc := res_t(c.call("sqlite3_create_aggregate_function_go",
stk_t(c.handle), stk_t(namePtr), stk_t(nArg),
stk_t(flag), stk_t(funcPtr)))
return c.error(rc)
}
// AggregateSeqFunction is the type of an aggregate SQL function.
// Implementations must not retain the slices yielded by seq.
type AggregateSeqFunction func(ctx *Context, seq iter.Seq[[]Value])
// CreateWindowFunction defines a new aggregate or aggregate window SQL function.
// If fn returns a [WindowFunction], an aggregate window function is created.
// If fn returns an [io.Closer], it will be called to free resources.
//
// https://sqlite.org/c3ref/create_function.html
func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn AggregateConstructor) error {
var funcPtr ptr_t
defer c.arena.mark()()
namePtr := c.arena.string(name)
if fn != nil {
funcPtr = util.AddHandle(c.ctx, AggregateConstructor(func() AggregateFunction {
agg := fn()
if win, ok := agg.(WindowFunction); ok {
return win
}
return agg
}))
}
rc := res_t(c.call("sqlite3_create_window_function_go",
stk_t(c.handle), stk_t(namePtr), stk_t(nArg),
stk_t(flag), stk_t(funcPtr)))
return c.error(rc)
}
// AggregateConstructor is a an [AggregateFunction] constructor.
type AggregateConstructor func() AggregateFunction
// AggregateFunction is the interface an aggregate function should implement.
//
// https://sqlite.org/appfunc.html
@@ -153,26 +192,24 @@ func collationCallback(ctx context.Context, mod api.Module, pArg, pDB ptr_t, eTe
}
func compareCallback(ctx context.Context, mod api.Module, pApp ptr_t, nKey1 int32, pKey1 ptr_t, nKey2 int32, pKey2 ptr_t) uint32 {
fn := util.GetHandle(ctx, pApp).(func(a, b []byte) int)
fn := util.GetHandle(ctx, pApp).(CollatingFunction)
return uint32(fn(util.View(mod, pKey1, int64(nKey1)), util.View(mod, pKey2, int64(nKey2))))
}
func funcCallback(ctx context.Context, mod api.Module, pCtx, pApp ptr_t, nArg int32, pArg ptr_t) {
args := getFuncArgs()
defer putFuncArgs(args)
db := ctx.Value(connKey{}).(*Conn)
args := callbackArgs(db, nArg, pArg)
defer returnArgs(args)
fn := util.GetHandle(db.ctx, pApp).(ScalarFunction)
callbackArgs(db, args[:nArg], pArg)
fn(Context{db, pCtx}, args[:nArg]...)
fn(Context{db, pCtx}, *args...)
}
func stepCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp ptr_t, nArg int32, pArg ptr_t) {
args := getFuncArgs()
defer putFuncArgs(args)
db := ctx.Value(connKey{}).(*Conn)
callbackArgs(db, args[:nArg], pArg)
args := callbackArgs(db, nArg, pArg)
defer returnArgs(args)
fn, _ := callbackAggregate(db, pAgg, pApp)
fn.Step(Context{db, pCtx}, args[:nArg]...)
fn.Step(Context{db, pCtx}, *args...)
}
func valueCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp ptr_t, final int32) {
@@ -196,12 +233,11 @@ func valueCallback(ctx context.Context, mod api.Module, pCtx, pAgg, pApp ptr_t,
}
func inverseCallback(ctx context.Context, mod api.Module, pCtx, pAgg ptr_t, nArg int32, pArg ptr_t) {
args := getFuncArgs()
defer putFuncArgs(args)
db := ctx.Value(connKey{}).(*Conn)
callbackArgs(db, args[:nArg], pArg)
args := callbackArgs(db, nArg, pArg)
defer returnArgs(args)
fn := util.GetHandle(db.ctx, pAgg).(WindowFunction)
fn.Inverse(Context{db, pCtx}, args[:nArg]...)
fn.Inverse(Context{db, pCtx}, *args...)
}
func callbackAggregate(db *Conn, pAgg, pApp ptr_t) (AggregateFunction, ptr_t) {
@@ -211,7 +247,7 @@ func callbackAggregate(db *Conn, pAgg, pApp ptr_t) (AggregateFunction, ptr_t) {
}
// We need to create the aggregate.
fn := util.GetHandle(db.ctx, pApp).(func() AggregateFunction)()
fn := util.GetHandle(db.ctx, pApp).(AggregateConstructor)()
if pAgg != 0 {
handle := util.AddHandle(db.ctx, fn)
util.Write32(db.mod, pAgg, handle)
@@ -220,25 +256,54 @@ func callbackAggregate(db *Conn, pAgg, pApp ptr_t) (AggregateFunction, ptr_t) {
return fn, 0
}
func callbackArgs(db *Conn, arg []Value, pArg ptr_t) {
for i := range arg {
arg[i] = Value{
var (
valueArgsPool sync.Pool
valueArgsLen atomic.Int32
)
func callbackArgs(db *Conn, nArg int32, pArg ptr_t) *[]Value {
arg, ok := valueArgsPool.Get().(*[]Value)
if !ok || cap(*arg) < int(nArg) {
max := valueArgsLen.Or(nArg) | nArg
lst := make([]Value, max)
arg = &lst
}
lst := (*arg)[:nArg]
for i := range lst {
lst[i] = Value{
c: db,
handle: util.Read32[ptr_t](db.mod, pArg+ptr_t(i)*ptrlen),
}
}
*arg = lst
return arg
}
var funcArgsPool sync.Pool
func putFuncArgs(p *[_MAX_FUNCTION_ARG]Value) {
funcArgsPool.Put(p)
func returnArgs(p *[]Value) {
valueArgsPool.Put(p)
}
func getFuncArgs() *[_MAX_FUNCTION_ARG]Value {
if p := funcArgsPool.Get(); p == nil {
return new([_MAX_FUNCTION_ARG]Value)
} else {
return p.(*[_MAX_FUNCTION_ARG]Value)
type aggregateFunc struct {
next func() (struct{}, bool)
stop func()
ctx Context
arg []Value
}
func (a *aggregateFunc) Step(ctx Context, arg ...Value) {
a.ctx = ctx
a.arg = append(a.arg[:0], arg...)
if _, more := a.next(); !more {
a.stop()
}
}
func (a *aggregateFunc) Value(ctx Context) {
a.ctx = ctx
a.stop()
}
func (a *aggregateFunc) Close() error {
a.stop()
return nil
}

57
func_seq_test.go Normal file
View File

@@ -0,0 +1,57 @@
package sqlite3_test
import (
"fmt"
"iter"
"log"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
)
func ExampleConn_CreateAggregateFunction() {
db, err := sqlite3.Open(":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Exec(`CREATE TABLE test (col)`)
if err != nil {
log.Fatal(err)
}
err = db.Exec(`INSERT INTO test VALUES (1), (2), (3)`)
if err != nil {
log.Fatal(err)
}
err = db.CreateAggregateFunction("seq_avg", 1, sqlite3.DETERMINISTIC|sqlite3.INNOCUOUS,
func(ctx *sqlite3.Context, seq iter.Seq[[]sqlite3.Value]) {
count := 0
total := 0.0
for arg := range seq {
total += arg[0].Float()
count++
}
ctx.ResultFloat(total / float64(count))
})
if err != nil {
log.Fatal(err)
}
stmt, _, err := db.Prepare(`SELECT seq_avg(col) FROM test`)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
for stmt.Step() {
fmt.Println(stmt.ColumnFloat(0))
}
if err := stmt.Err(); err != nil {
log.Fatal(err)
}
// Output:
// 2
}

17
go.mod
View File

@@ -1,23 +1,22 @@
module github.com/ncruces/go-sqlite3
go 1.23.0
toolchain go1.24.0
go 1.24.0
require (
github.com/ncruces/julianday v1.0.0
github.com/ncruces/sort v0.1.5
github.com/tetratelabs/wazero v1.9.0
golang.org/x/crypto v0.35.0
golang.org/x/sys v0.30.0
github.com/ncruces/sort v0.1.6
github.com/ncruces/wbt v0.2.0
github.com/tetratelabs/wazero v1.10.0
golang.org/x/sys v0.38.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.11.0 // test
golang.org/x/text v0.22.0 // ext/unicode
golang.org/x/crypto v0.43.0 // vfs/adiantum vfs/xts
golang.org/x/sync v0.17.0 // test
golang.org/x/text v0.30.0 // ext/unicode
lukechampine.com/adiantum v1.1.1 // vfs/adiantum
)

26
go.sum
View File

@@ -4,19 +4,21 @@ 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.5 h1:fiFWXXAqKI8QckPf/6hu/bGFwcEPrirIOFaJqWujs4k=
github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
github.com/ncruces/sort v0.1.6 h1:TrsJfGRH1AoWoaeB4/+gCohot9+cA6u/INaH5agIhNk=
github.com/ncruces/sort v0.1.6/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
github.com/ncruces/wbt v0.2.0 h1:Q9zlKOBSZc7Yy/R2cGa35g6RKUUE3BjNIW3tfGC4F04=
github.com/ncruces/wbt v0.2.0/go.mod h1:DtF92amvMxH69EmBFUSFWRDAlo6hOEfoNQnClxj9C/c=
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.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
github.com/tetratelabs/wazero v1.10.0 h1:CXP3zneLDl6J4Zy8N/J+d5JsWKfrjE6GtvVK1fpnDlk=
github.com/tetratelabs/wazero v1.10.0/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=

View File

@@ -209,8 +209,12 @@ func (d *ddl) renameTable(dst, src string) error {
return nil
}
func compileConstraintRegexp(name string) *regexp.Regexp {
return regexp.MustCompile("^(?i:CONSTRAINT)\\s+[\"`]?" + regexp.QuoteMeta(name) + "[\"`\\s]")
}
func (d *ddl) addConstraint(name string, sql string) {
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
reg := compileConstraintRegexp(name)
for i := 0; i < len(d.fields); i++ {
if reg.MatchString(d.fields[i]) {
@@ -223,7 +227,7 @@ func (d *ddl) addConstraint(name string, sql string) {
}
func (d *ddl) removeConstraint(name string) bool {
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
reg := compileConstraintRegexp(name)
for i := 0; i < len(d.fields); i++ {
if reg.MatchString(d.fields[i]) {
@@ -236,7 +240,7 @@ func (d *ddl) removeConstraint(name string) bool {
//lint:ignore U1000 ignore unused code.
func (d *ddl) hasConstraint(name string) bool {
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
reg := compileConstraintRegexp(name)
for _, f := range d.fields {
if reg.MatchString(f) {

View File

@@ -95,7 +95,7 @@ func parseAllColumns(in string) ([]string, error) {
}
return nil, fmt.Errorf("unexpected token: %s", string(s[i]))
case parseAllColumnsState_State_End:
break
continue // avoid SA4011
}
}
if state != parseAllColumnsState_State_End {

View File

@@ -313,6 +313,41 @@ func TestRemoveConstraint(t *testing.T) {
success: true,
expect: []string{"`id` integer NOT NULL"},
},
{
name: "lowercase",
fields: []string{"`id` integer NOT NULL", "constraint `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
cName: "fk_users_notes",
success: true,
expect: []string{"`id` integer NOT NULL"},
},
{
name: "mixed_case",
fields: []string{"`id` integer NOT NULL", "cOnsTraiNT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
cName: "fk_users_notes",
success: true,
expect: []string{"`id` integer NOT NULL"},
},
{
name: "newline",
fields: []string{"`id` integer NOT NULL", "CONSTRAINT `fk_users_notes`\nFOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
cName: "fk_users_notes",
success: true,
expect: []string{"`id` integer NOT NULL"},
},
{
name: "lots_of_newlines",
fields: []string{"`id` integer NOT NULL", "constraint \n fk_users_notes \n FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
cName: "fk_users_notes",
success: true,
expect: []string{"`id` integer NOT NULL"},
},
{
name: "no_backtick",
fields: []string{"`id` integer NOT NULL", "CONSTRAINT fk_users_notes FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))"},
cName: "fk_users_notes",
success: true,
expect: []string{"`id` integer NOT NULL"},
},
{
name: "check",
fields: []string{"CONSTRAINT `name_checker` CHECK (`name` <> 'thetadev')", "`id` integer NOT NULL"},

View File

@@ -1,19 +1,17 @@
module github.com/ncruces/go-sqlite3/gormlite
go 1.23.0
toolchain go1.24.0
go 1.24.0
require (
github.com/ncruces/go-sqlite3 v0.23.1
gorm.io/gorm v1.25.12
github.com/ncruces/go-sqlite3 v0.30.1
gorm.io/gorm v1.31.1
)
require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
github.com/tetratelabs/wazero v1.10.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.30.0 // indirect
)

View File

@@ -2,15 +2,15 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/ncruces/go-sqlite3 v0.23.1 h1:zGAd76q+Tr18z/xKGatUlzBQdjR3J+rexfANUcjAgkY=
github.com/ncruces/go-sqlite3 v0.23.1/go.mod h1:Xg3FyAZl25HcBSFmcbymdfoTqD7jRnBUmv1jSrbIjdE=
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
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.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
github.com/tetratelabs/wazero v1.10.0 h1:CXP3zneLDl6J4Zy8N/J+d5JsWKfrjE6GtvVK1fpnDlk=
github.com/tetratelabs/wazero v1.10.0/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

View File

@@ -339,7 +339,7 @@ func (m _Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) {
indexes := make([]gorm.Index, 0)
err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
rst := make([]*_Index, 0)
if err := m.DB.Debug().Raw("SELECT * FROM PRAGMA_index_list(?)", stmt.Table).Scan(&rst).Error; err != nil { // alias `PRAGMA index_list(?)`
if err := m.DB.Raw("SELECT * FROM PRAGMA_index_list(?)", stmt.Table).Scan(&rst).Error; err != nil { // alias `PRAGMA index_list(?)`
return err
}
for _, index := range rst {

View File

@@ -1,4 +1,3 @@
// Package gormlite provides a GORM driver for SQLite.
package gormlite
import (
@@ -52,7 +51,9 @@ func (dialector _Dialector) Initialize(db *gorm.DB) (err error) {
})
for k, v := range dialector.ClauseBuilders() {
db.ClauseBuilders[k] = v
if _, ok := db.ClauseBuilders[k]; !ok {
db.ClauseBuilders[k] = v
}
}
return
}

View File

@@ -14,10 +14,10 @@ import (
)
func TestDialector(t *testing.T) {
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
// Custom connection with a custom function called "my_custom_function".
db, err := driver.Open(tmp, func(conn *sqlite3.Conn) error {
db, err := driver.Open(dsn, func(conn *sqlite3.Conn) error {
return conn.CreateFunction("my_custom_function", 0, sqlite3.DETERMINISTIC,
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultText("my-result")
@@ -36,14 +36,14 @@ func TestDialector(t *testing.T) {
}{
{
description: "Default driver",
dialector: Open(tmp),
dialector: Open(dsn),
openSuccess: true,
query: "SELECT 1",
querySuccess: true,
},
{
description: "Custom function",
dialector: Open(tmp),
dialector: Open(dsn),
openSuccess: true,
query: "SELECT my_custom_function()",
querySuccess: false,

View File

@@ -7,7 +7,7 @@ rm -rf gorm/ tests/
go work use -r .
go test
git clone --branch v1.25.12 --filter=blob:none https://github.com/go-gorm/gorm.git
git clone --branch v1.31.1 --filter=blob:none https://github.com/go-gorm/gorm.git
mv gorm/tests tests
rm -rf gorm/

View File

@@ -7,13 +7,14 @@ diff --git a/tests/.gitignore b/tests/.gitignore
diff --git a/tests/tests_test.go b/tests/tests_test.go
--- a/tests/tests_test.go
+++ b/tests/tests_test.go
@@ -7,9 +7,11 @@ import (
@@ -8,9 +8,11 @@ import (
"path/filepath"
"time"
+ _ "github.com/ncruces/go-sqlite3/embed"
+ sqlite "github.com/ncruces/go-sqlite3/gormlite"
+
"gorm.io/driver/gaussdb"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
- "gorm.io/driver/sqlite"

View File

@@ -3,13 +3,12 @@ set -euo pipefail
cd -P -- "$(dirname -- "$0")"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/ddlmod.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/ddlmod_test.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/ddlmod_parse_all_columns.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/ddlmod_parse_all_columns_test.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/error_translator.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/migrator.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/sqlite.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/sqlite_test.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.5.7/sqlite_test.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod_test.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod_parse_all_columns.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/ddlmod_parse_all_columns_test.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/error_translator.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/migrator.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/sqlite.go"
curl -#OL "https://github.com/go-gorm/sqlite/raw/v1.6.0/sqlite_test.go"
curl -#L "https://github.com/glebarez/sqlite/raw/v1.11.0/sqlite_error_translator_test.go" > error_translator_test.go

View File

@@ -9,24 +9,31 @@ import (
"golang.org/x/sys/unix"
)
func NewMemory(_, max uint64) experimental.LinearMemory {
func NewMemory(cap, max uint64) experimental.LinearMemory {
// Round up to the page size.
rnd := uint64(unix.Getpagesize() - 1)
max = (max + rnd) &^ rnd
res := (max + rnd) &^ rnd
if max > math.MaxInt {
// This ensures int(max) overflows to a negative value,
if res > math.MaxInt {
// This ensures int(res) overflows to a negative value,
// and unix.Mmap returns EINVAL.
max = math.MaxUint64
res = math.MaxUint64
}
// Reserve max bytes of address space, to ensure we won't need to move it.
com := res
prot := unix.PROT_READ | unix.PROT_WRITE
if cap < max { // Commit memory only if cap=max.
com = 0
prot = unix.PROT_NONE
}
// Reserve res bytes of address space, to ensure we won't need to move it.
// A protected, private, anonymous mapping should not commit memory.
b, err := unix.Mmap(-1, 0, int(max), unix.PROT_NONE, unix.MAP_PRIVATE|unix.MAP_ANON)
b, err := unix.Mmap(-1, 0, int(res), prot, unix.MAP_PRIVATE|unix.MAP_ANON)
if err != nil {
panic(err)
}
return &mmappedMemory{buf: b[:0]}
return &mmappedMemory{buf: b[:com]}
}
// The slice covers the entire mmapped memory:
@@ -40,9 +47,11 @@ func (m *mmappedMemory) Reallocate(size uint64) []byte {
com := uint64(len(m.buf))
res := uint64(cap(m.buf))
if com < size && size <= res {
// Round up to the page size.
// Grow geometrically, round up to the page size.
rnd := uint64(unix.Getpagesize() - 1)
new := (size + rnd) &^ rnd
new := com + com>>3
new = min(max(size, new), res)
new = (new + rnd) &^ rnd
// Commit additional memory up to new bytes.
err := unix.Mprotect(m.buf[com:new], unix.PROT_READ|unix.PROT_WRITE)
@@ -50,8 +59,7 @@ func (m *mmappedMemory) Reallocate(size uint64) []byte {
return nil
}
// Update committed memory.
m.buf = m.buf[:new]
m.buf = m.buf[:new] // Update committed memory.
}
// Limit returned capacity because bytes beyond
// len(m.buf) have not yet been committed.

View File

@@ -9,20 +9,26 @@ import (
"golang.org/x/sys/windows"
)
func NewMemory(_, max uint64) experimental.LinearMemory {
func NewMemory(cap, max uint64) experimental.LinearMemory {
// Round up to the page size.
rnd := uint64(windows.Getpagesize() - 1)
max = (max + rnd) &^ rnd
res := (max + rnd) &^ rnd
if max > math.MaxInt {
// This ensures uintptr(max) overflows to a large value,
if res > math.MaxInt {
// This ensures uintptr(res) overflows to a large value,
// and windows.VirtualAlloc returns an error.
max = math.MaxUint64
res = math.MaxUint64
}
// Reserve max bytes of address space, to ensure we won't need to move it.
// This does not commit memory.
r, err := windows.VirtualAlloc(0, uintptr(max), windows.MEM_RESERVE, windows.PAGE_READWRITE)
com := res
kind := windows.MEM_COMMIT
if cap < max { // Commit memory only if cap=max.
com = 0
kind = windows.MEM_RESERVE
}
// Reserve res bytes of address space, to ensure we won't need to move it.
r, err := windows.VirtualAlloc(0, uintptr(res), uint32(kind), windows.PAGE_READWRITE)
if err != nil {
panic(err)
}
@@ -30,8 +36,9 @@ func NewMemory(_, max uint64) experimental.LinearMemory {
mem := virtualMemory{addr: r}
// SliceHeader, although deprecated, avoids a go vet warning.
sh := (*reflect.SliceHeader)(unsafe.Pointer(&mem.buf))
sh.Cap = int(max)
sh.Data = r
sh.Len = int(com)
sh.Cap = int(res)
return &mem
}
@@ -47,9 +54,11 @@ func (m *virtualMemory) Reallocate(size uint64) []byte {
com := uint64(len(m.buf))
res := uint64(cap(m.buf))
if com < size && size <= res {
// Round up to the page size.
// Grow geometrically, round up to the page size.
rnd := uint64(windows.Getpagesize() - 1)
new := (size + rnd) &^ rnd
new := com + com>>3
new = min(max(size, new), res)
new = (new + rnd) &^ rnd
// Commit additional memory up to new bytes.
_, err := windows.VirtualAlloc(m.addr, uintptr(new), windows.MEM_COMMIT, windows.PAGE_READWRITE)
@@ -57,8 +66,7 @@ func (m *virtualMemory) Reallocate(size uint64) []byte {
return nil
}
// Update committed memory.
m.buf = m.buf[:new]
m.buf = m.buf[:new] // Update committed memory.
}
// Limit returned capacity because bytes beyond
// len(m.buf) have not yet been committed.

View File

@@ -39,6 +39,9 @@ const (
ERROR_MISSING_COLLSEQ = ERROR | (1 << 8)
ERROR_RETRY = ERROR | (2 << 8)
ERROR_SNAPSHOT = ERROR | (3 << 8)
ERROR_RESERVESIZE = ERROR | (4 << 8)
ERROR_KEY = ERROR | (5 << 8)
ERROR_UNABLE = ERROR | (6 << 8)
IOERR_READ = IOERR | (1 << 8)
IOERR_SHORT_READ = IOERR | (2 << 8)
IOERR_WRITE = IOERR | (3 << 8)
@@ -73,6 +76,8 @@ const (
IOERR_DATA = IOERR | (32 << 8)
IOERR_CORRUPTFS = IOERR | (33 << 8)
IOERR_IN_PAGE = IOERR | (34 << 8)
IOERR_BADKEY = IOERR | (35 << 8)
IOERR_CODEC = IOERR | (36 << 8)
LOCKED_SHAREDCACHE = LOCKED | (1 << 8)
LOCKED_VTAB = LOCKED | (2 << 8)
BUSY_RECOVERY = BUSY | (1 << 8)

View File

@@ -33,8 +33,12 @@ func AssertErr() ErrorString {
return ErrorString(msg)
}
func ErrorCodeString(rc uint32) string {
switch rc {
type errorCode interface {
~uint8 | ~uint16 | ~uint32 | ~int32
}
func ErrorCodeString[T errorCode](rc T) string {
switch uint32(rc) {
case ABORT_ROLLBACK:
return "sqlite3: abort due to ROLLBACK"
case ROW:
@@ -42,7 +46,7 @@ func ErrorCodeString(rc uint32) string {
case DONE:
return "sqlite3: no more rows available"
}
switch rc & 0xff {
switch uint8(rc) {
case OK:
return "sqlite3: not an error"
case ERROR:
@@ -75,7 +79,7 @@ func ErrorCodeString(rc uint32) string {
return "sqlite3: unable to open database file"
case PROTOCOL:
return "sqlite3: locking protocol"
case FORMAT:
case EMPTY:
break
case SCHEMA:
return "sqlite3: database schema has changed"
@@ -91,7 +95,7 @@ func ErrorCodeString(rc uint32) string {
break
case AUTH:
return "sqlite3: authorization denied"
case EMPTY:
case FORMAT:
break
case RANGE:
return "sqlite3: column index out of range"

View File

@@ -20,20 +20,6 @@ func ExportFuncVI[T0 i32](mod wazero.HostModuleBuilder, name string, fn func(con
Export(name)
}
type funcVII[T0, T1 i32] func(context.Context, api.Module, T0, T1)
func (fn funcVII[T0, T1]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[1] // prevent bounds check on every slice access
fn(ctx, mod, T0(stack[0]), T1(stack[1]))
}
func ExportFuncVII[T0, T1 i32](mod wazero.HostModuleBuilder, name string, fn func(context.Context, api.Module, T0, T1)) {
mod.NewFunctionBuilder().
WithGoModuleFunction(funcVII[T0, T1](fn),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, nil).
Export(name)
}
type funcVIII[T0, T1, T2 i32] func(context.Context, api.Module, T0, T1, T2)
func (fn funcVIII[T0, T1, T2]) Call(ctx context.Context, mod api.Module, stack []uint64) {

View File

@@ -1,3 +1,5 @@
//go:build !goexperiment.jsonv2
package util
import (

52
internal/util/json_v2.go Normal file
View File

@@ -0,0 +1,52 @@
//go:build goexperiment.jsonv2
package util
import (
"encoding/json/v2"
"math"
"strconv"
"time"
"unsafe"
)
type JSON struct{ Value any }
func (j JSON) Scan(value any) error {
var buf []byte
switch v := value.(type) {
case []byte:
buf = v
case string:
buf = unsafe.Slice(unsafe.StringData(v), len(v))
case int64:
buf = strconv.AppendInt(nil, v, 10)
case float64:
buf = AppendNumber(nil, v)
case time.Time:
buf = append(buf, '"')
buf = v.AppendFormat(buf, time.RFC3339Nano)
buf = append(buf, '"')
case nil:
buf = []byte("null")
default:
panic(AssertErr())
}
return json.Unmarshal(buf, j.Value)
}
func AppendNumber(dst []byte, f float64) []byte {
switch {
case math.IsNaN(f):
dst = append(dst, "null"...)
case math.IsInf(f, 1):
dst = append(dst, "9.0e999"...)
case math.IsInf(f, -1):
dst = append(dst, "-9.0e999"...)
default:
return strconv.AppendFloat(dst, f, 'g', -1, 64)
}
return dst
}

View File

@@ -135,11 +135,10 @@ func ReadString(mod api.Module, ptr Ptr_t, maxlen int64) string {
panic(RangeErr)
}
}
if i := bytes.IndexByte(buf, 0); i < 0 {
panic(NoNulErr)
} else {
if i := bytes.IndexByte(buf, 0); i >= 0 {
return string(buf[:i])
}
panic(NoNulErr)
}
func WriteBytes(mod api.Module, ptr Ptr_t, b []byte) {

View File

@@ -1,12 +1,10 @@
package util
import (
"context"
"os"
"reflect"
"unsafe"
"github.com/tetratelabs/wazero/api"
"golang.org/x/sys/windows"
)
@@ -16,14 +14,21 @@ type MappedRegion struct {
addr uintptr
}
func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32) (*MappedRegion, error) {
h, err := windows.CreateFileMapping(windows.Handle(f.Fd()), nil, windows.PAGE_READWRITE, 0, 0, nil)
func MapRegion(f *os.File, offset int64, size int32) (*MappedRegion, error) {
maxSize := offset + int64(size)
h, err := windows.CreateFileMapping(
windows.Handle(f.Fd()), nil, windows.PAGE_READWRITE,
uint32(maxSize>>32), uint32(maxSize), nil)
if h == 0 {
return nil, err
}
const allocationGranularity = 64 * 1024
align := offset % allocationGranularity
offset -= align
a, err := windows.MapViewOfFile(h, windows.FILE_MAP_WRITE,
uint32(offset>>32), uint32(offset), uintptr(size))
uint32(offset>>32), uint32(offset), uintptr(size)+uintptr(align))
if a == 0 {
windows.CloseHandle(h)
return nil, err
@@ -32,9 +37,9 @@ func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, si
ret := &MappedRegion{Handle: h, addr: a}
// SliceHeader, although deprecated, avoids a go vet warning.
sh := (*reflect.SliceHeader)(unsafe.Pointer(&ret.Data))
sh.Data = a + uintptr(align)
sh.Len = int(size)
sh.Cap = int(size)
sh.Data = a
return ret, nil
}

View File

@@ -12,6 +12,7 @@ type ConnKey struct{}
type moduleKey struct{}
type moduleState struct {
sysError error
mmapState
handleState
}
@@ -23,3 +24,20 @@ func NewContext(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, moduleKey{}, state)
return ctx
}
func GetSystemError(ctx context.Context) error {
// Test needed to simplify testing.
s, ok := ctx.Value(moduleKey{}).(*moduleState)
if ok {
return s.sysError
}
return nil
}
func SetSystemError(ctx context.Context, err error) {
// Test needed to simplify testing.
s, ok := ctx.Value(moduleKey{}).(*moduleState)
if ok {
s.sysError = err
}
}

View File

@@ -1,11 +1,3 @@
package util
type Pointer[T any] struct{ Value T }
func (p Pointer[T]) unwrap() any { return p.Value }
type PointerUnwrap interface{ unwrap() any }
func UnwrapPointer(p PointerUnwrap) any {
return p.unwrap()
}
type Pointer struct{ Value any }

View File

@@ -1,13 +0,0 @@
package util
import (
"math"
"testing"
)
func TestUnwrapPointer(t *testing.T) {
p := Pointer[float64]{Value: math.Pi}
if got := UnwrapPointer(p); got != math.Pi {
t.Errorf("want π, got %v", got)
}
}

83
json.go
View File

@@ -1,6 +1,13 @@
//go:build !goexperiment.jsonv2
package sqlite3
import "github.com/ncruces/go-sqlite3/internal/util"
import (
"encoding/json"
"strconv"
"github.com/ncruces/go-sqlite3/internal/util"
)
// JSON returns a value that can be used as an argument to
// [database/sql.DB.Exec], [database/sql.Row.Scan] and similar methods to
@@ -10,3 +17,77 @@ import "github.com/ncruces/go-sqlite3/internal/util"
func JSON(value any) any {
return util.JSON{Value: value}
}
// ResultJSON sets the result of the function to the JSON encoding of value.
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultJSON(value any) {
err := json.NewEncoder(callbackWriter(func(p []byte) (int, error) {
ctx.ResultRawText(p[:len(p)-1]) // remove the newline
return 0, nil
})).Encode(value)
if err != nil {
ctx.ResultError(err)
return // notest
}
}
// BindJSON binds the JSON encoding of value to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindJSON(param int, value any) error {
return json.NewEncoder(callbackWriter(func(p []byte) (int, error) {
return 0, s.BindRawText(param, p[:len(p)-1]) // remove the newline
})).Encode(value)
}
// ColumnJSON parses the JSON-encoded value of the result column
// and stores it in the value pointed to by ptr.
// The leftmost column of the result set has the index 0.
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnJSON(col int, ptr any) error {
var data []byte
switch s.ColumnType(col) {
case NULL:
data = []byte("null")
case TEXT:
data = s.ColumnRawText(col)
case BLOB:
data = s.ColumnRawBlob(col)
case INTEGER:
data = strconv.AppendInt(nil, s.ColumnInt64(col), 10)
case FLOAT:
data = util.AppendNumber(nil, s.ColumnFloat(col))
default:
panic(util.AssertErr())
}
return json.Unmarshal(data, ptr)
}
// JSON parses a JSON-encoded value
// and stores the result in the value pointed to by ptr.
func (v Value) JSON(ptr any) error {
var data []byte
switch v.Type() {
case NULL:
data = []byte("null")
case TEXT:
data = v.RawText()
case BLOB:
data = v.RawBlob()
case INTEGER:
data = strconv.AppendInt(nil, v.Int64(), 10)
case FLOAT:
data = util.AppendNumber(nil, v.Float())
default:
panic(util.AssertErr())
}
return json.Unmarshal(data, ptr)
}
type callbackWriter func(p []byte) (int, error)
func (fn callbackWriter) Write(p []byte) (int, error) { return fn(p) }

113
json_v2.go Normal file
View File

@@ -0,0 +1,113 @@
//go:build goexperiment.jsonv2
package sqlite3
import (
"encoding/json/v2"
"strconv"
"github.com/ncruces/go-sqlite3/internal/util"
)
// JSON returns a value that can be used as an argument to
// [database/sql.DB.Exec], [database/sql.Row.Scan] and similar methods to
// store value as JSON, or decode JSON into value.
// JSON should NOT be used with [Stmt.BindJSON], [Stmt.ColumnJSON],
// [Value.JSON], or [Context.ResultJSON].
func JSON(value any) any {
return util.JSON{Value: value}
}
// ResultJSON sets the result of the function to the JSON encoding of value.
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultJSON(value any) {
w := bytesWriter{sqlite: ctx.c.sqlite}
if err := json.MarshalWrite(&w, value); err != nil {
ctx.c.free(w.ptr)
ctx.ResultError(err)
return // notest
}
ctx.c.call("sqlite3_result_text_go",
stk_t(ctx.handle), stk_t(w.ptr), stk_t(len(w.buf)))
}
// BindJSON binds the JSON encoding of value to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindJSON(param int, value any) error {
w := bytesWriter{sqlite: s.c.sqlite}
if err := json.MarshalWrite(&w, value); err != nil {
s.c.free(w.ptr)
return err
}
rc := res_t(s.c.call("sqlite3_bind_text_go",
stk_t(s.handle), stk_t(param),
stk_t(w.ptr), stk_t(len(w.buf))))
return s.c.error(rc)
}
// ColumnJSON parses the JSON-encoded value of the result column
// and stores it in the value pointed to by ptr.
// The leftmost column of the result set has the index 0.
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnJSON(col int, ptr any) error {
var data []byte
switch s.ColumnType(col) {
case NULL:
data = []byte("null")
case TEXT:
data = s.ColumnRawText(col)
case BLOB:
data = s.ColumnRawBlob(col)
case INTEGER:
data = strconv.AppendInt(nil, s.ColumnInt64(col), 10)
case FLOAT:
data = util.AppendNumber(nil, s.ColumnFloat(col))
default:
panic(util.AssertErr())
}
return json.Unmarshal(data, ptr)
}
// JSON parses a JSON-encoded value
// and stores the result in the value pointed to by ptr.
func (v Value) JSON(ptr any) error {
var data []byte
switch v.Type() {
case NULL:
data = []byte("null")
case TEXT:
data = v.RawText()
case BLOB:
data = v.RawBlob()
case INTEGER:
data = strconv.AppendInt(nil, v.Int64(), 10)
case FLOAT:
data = util.AppendNumber(nil, v.Float())
default:
panic(util.AssertErr())
}
return json.Unmarshal(data, ptr)
}
type bytesWriter struct {
*sqlite
buf []byte
ptr ptr_t
}
func (b *bytesWriter) Write(p []byte) (n int, err error) {
if len(p) > cap(b.buf)-len(b.buf) {
want := int64(len(b.buf)) + int64(len(p))
grow := int64(cap(b.buf))
grow += grow >> 1
want = max(want, grow)
b.ptr = b.realloc(b.ptr, want)
b.buf = util.View(b.mod, b.ptr, want)[:len(b.buf)]
}
b.buf = append(b.buf, p...)
return len(p), nil
}

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