Compare commits

..

279 Commits

Author SHA1 Message Date
Fedor Indutny
f2dd94be8f 9.0.13 2025-02-13 16:31:08 -08:00
Fedor Indutny
34dced2d27 fix windows build 2025-02-13 16:31:02 -08:00
Fedor Indutny
6dcd4fa333 9.0.12 2025-02-13 14:37:29 -08:00
Fedor Indutny
2a6d9a51fe Compatibility with pnpm 2025-02-13 14:37:15 -08:00
Fedor Indutny
3a3cad1d03 9.0.11 2025-02-05 09:57:56 -08:00
Fedor Indutny
0000015ba0 deps: bump extension to fix compilation issues 2025-02-05 09:57:37 -08:00
Fedor Indutny
551fc92c17 9.0.10 2025-01-28 13:47:27 -08:00
Fedor Indutny
8c74ab6e62 deps: use sha512-asm after all 2025-01-28 13:47:19 -08:00
Fedor Indutny
cd50a61cb0 9.0.9 2025-01-02 12:15:23 -08:00
Fedor Indutny
841b625bd4 deps: update extension to 0.2.1 2025-01-02 12:15:10 -08:00
Fedor Indutny
aedb91ab92 9.0.8 2024-10-28 15:25:39 -07:00
ayumi-signal
9abaedb382 Fix build on windows arm64 2024-10-28 15:18:54 -07:00
Fedor Indutny
46134ef439 9.0.7 2024-10-18 15:59:58 -07:00
Fedor Indutny
a610cb4899 9.0.7-rc.4 2024-10-18 15:37:59 -07:00
Fedor Indutny
c10c625237 Add back libraries on Windows 2024-10-18 15:37:52 -07:00
Fedor Indutny
6cf974078d 9.0.7-rc.3 2024-10-18 15:20:00 -07:00
Fedor Indutny
13fecd9041 Fix build on non-macos 2024-10-18 15:19:52 -07:00
Fedor Indutny
2c5d0d3e8a 9.0.7-rc.2 2024-10-18 15:15:54 -07:00
Fedor Indutny
fc60881b64 Fix build 2024-10-18 15:15:42 -07:00
Fedor Indutny
896c585802 9.0.7-rc.1 2024-10-18 14:47:57 -07:00
Fedor Indutny
3a959e1c31 Use signal-sqlcipher-extension 2024-10-18 14:47:29 -07:00
Fedor Indutny
00731353b9 9.0.6 2024-10-16 12:31:20 -07:00
Fedor Indutny
19577c3691 Use smaller fts5 extension 2024-10-16 12:31:10 -07:00
Fedor Indutny
a3b182985c Fix build against Electron 2024-10-16 12:17:59 -07:00
Fedor Indutny
fc026bf4b6 Match MACOSX_DEPLOYMENT_TARGET to Electron 2024-10-16 12:07:13 -07:00
Fedor Indutny
5c0f7371f7 Use -flto for sqlcipher as well 2024-10-16 12:04:10 -07:00
Fedor Indutny
844b3893ca 9.0.5 2024-10-16 11:36:39 -07:00
Fedor Indutny
e3ef568532 Minimize use of ifdefs 2024-10-16 11:35:46 -07:00
Fedor Indutny
eed787dec6 9.0.4 2024-10-16 10:12:33 -07:00
Fedor Indutny
078808e7c2 Fully remove Deserialize 2024-10-16 10:12:23 -07:00
Fedor Indutny
75babc345a 9.0.3 2024-10-16 09:39:47 -07:00
Fedor Indutny
f66b8c6dd1 Use SQLCIPHER_CRYPTO_CC on macOS 2024-10-16 09:39:46 -07:00
Fedor Indutny
01d99874a6 Omit features we don't use 2024-10-16 09:39:46 -07:00
Fedor Indutny
0da082ad0d Change compile flags on macOS 2024-10-16 09:39:46 -07:00
Fedor Indutny
28d1929af0 Remove loadExtension method 2024-10-16 09:39:46 -07:00
Fedor Indutny
89ff55bf0b Collapse Data namespaces together 2024-10-16 09:39:46 -07:00
Fedor Indutny
312269c8c7 Use v8::LocalVector for object construction 2024-10-16 09:39:46 -07:00
Fedor Indutny
99400c7bdc Move away from lzz 2024-10-16 09:39:46 -07:00
Fedor Indutny
6439afa9d5 Revert all changes since v8.8.5 to reapply cleanly 2024-10-16 09:39:01 -07:00
Fedor Indutny
850afac9a5 9.0.2 2024-10-15 17:57:02 -07:00
Fedor Indutny
27576d4b2e Return back -O3 2024-10-15 17:56:54 -07:00
Fedor Indutny
00b49de136 9.0.1 2024-10-15 17:29:34 -07:00
Fedor Indutny
6c22e97453 Actually use LocalVector 2024-10-15 17:29:33 -07:00
Fedor Indutny
c3a02fb0eb 9.0.0 2024-10-15 17:21:34 -07:00
Fedor Indutny
e64c401b72 Some further optimizations 2024-10-15 17:20:39 -07:00
Fedor Indutny
ca1e8a28dd Omit even more things 2024-10-15 17:09:34 -07:00
Fedor Indutny
f6eaf92037 Omit things we don't use 2024-10-15 17:08:43 -07:00
Fedor Indutny
2f59f4f15b V8_HAS_LOCAL_VECTOR 2024-10-15 17:00:04 -07:00
Fedor Indutny
b24625232b Move away from lzz 2024-10-15 16:48:05 -07:00
Fedor Indutny
1931cc95f2 8.8.5 2024-10-12 21:17:32 -07:00
Fedor Indutny
3bf6cd2b9f Cache keys between rows in .all() 2024-10-12 21:15:57 -07:00
Fedor Indutny
2a3355445f 8.8.4 2024-10-12 13:17:19 -07:00
Fedor Indutny
20e778de1f contruct flat rows slightly faster 2024-10-12 13:17:16 -07:00
Fedor Indutny
f668e9b8d9 8.8.3 2024-10-12 11:32:04 -07:00
Fedor Indutny
0e26efeeb6 Revert "contruct flat rows slightly faster"
This reverts commit 843689519c.
2024-10-12 11:31:58 -07:00
Fedor Indutny
b82abd0fab 8.8.2 2024-10-12 11:26:34 -07:00
Fedor Indutny
843689519c contruct flat rows slightly faster 2024-10-12 11:26:08 -07:00
Fedor Indutny
e1693c48d0 8.8.1 2024-08-22 15:27:43 -07:00
Fedor Indutny
4917cd51d7 Support Electron 23 2024-08-22 15:27:35 -07:00
Fedor Indutny
99cede2168 Build using c++20 for Electron 32 2024-08-22 14:42:17 -07:00
Fedor Indutny
4bd283e08a 8.8.0 2024-08-20 19:18:48 -07:00
Fedor Indutny
c418b73871 Update to sqlcipher 4.6.1 2024-08-20 19:18:35 -07:00
Fedor Indutny
d161201f25 8.7.1 2024-02-22 13:03:56 -08:00
Fedor Indutny
903bcd399a src: final fixes for Electron 29 2024-02-22 13:03:47 -08:00
Fedor Indutny
53f4c04f91 8.7.0 2024-02-20 18:31:45 -08:00
Fedor Indutny
dc1bb76b97 src: final fixes for electron 29 2024-02-20 18:31:34 -08:00
Fedor Indutny
3626b4c8d7 8.6.1 2024-02-20 18:25:14 -08:00
Fedor Indutny
0fd1a2745b src: fix for Electron 29 2024-02-20 18:25:01 -08:00
Fedor Indutnyy
98bd9063a2 8.6.0 2023-11-29 21:58:34 -08:00
Fedor Indutnyy
1a19f1fc6a api: Database#signalTokenize() 2023-11-29 21:46:06 -08:00
Fedor Indutnyy
e8ab1dc902 8.5.2 2023-09-18 11:01:41 -07:00
Fedor Indutnyy
81f37a2f04 deps: cherry-pick FTS5 fix from upstream
Ref: https://sqlite.org/src/info/cc0f82a480a400c6
2023-09-18 10:59:41 -07:00
Fedor Indutnyy
f1e1811589 8.5.1 2023-08-31 14:46:26 -07:00
Fedor Indutnyy
d467d0fc5e deps: link with ntdll 2023-08-31 14:45:14 -07:00
Fedor Indutnyy
07b59659ce 8.5.0 2023-08-31 12:18:26 -07:00
Fedor Indutnyy
23dfe23e16 deps: update to sqlcipher 4.5.5 2023-08-31 12:18:04 -07:00
Fedor Indutnyy
5ea24501fc 8.4.3 2023-02-10 10:53:58 -08:00
Fedor Indutnyy
fe42144e18 deps: update tokenizer to 0.2.1 2023-02-10 10:53:49 -08:00
Fedor Indutnyy
b1df4a4e5f 8.4.2 2023-02-09 09:58:35 -08:00
Fedor Indutnyy
0ff0819c0d deps: update sqlcipher to 4.5.3-fts 2023-02-09 09:58:15 -08:00
Fedor Indutnyy
9a92ca84ca 8.4.1 2023-01-31 13:19:37 -08:00
Fedor Indutnyy
f328d9b496 deps: add missing libs for the latest rust 2023-01-31 13:19:27 -08:00
Fedor Indutnyy
79ba68c957 8.4.0 2023-01-31 12:18:38 -08:00
Fedor Indutnyy
d48edee39a deps: update tokenizer to 0.2.0 2023-01-31 12:18:30 -08:00
Fedor Indutnyy
26ae9e0fc7 8.3.1 2023-01-26 14:58:37 -08:00
Fedor Indutnyy
2e432b0aad rename tokenizer to signal_tokenizer 2023-01-26 14:58:27 -08:00
Fedor Indutnyy
583db4c9ee 8.3.0 2023-01-25 23:08:01 -08:00
Fedor Indutnyy
bfcdb16e52 8.3.0-rc.4 2023-01-25 21:56:28 -08:00
Fedor Indutnyy
ce0f0aa094 deps: try different library placement 2023-01-25 21:52:50 -08:00
Fedor Indutnyy
211b4bceec 8.3.0-rc.3 2023-01-25 21:25:18 -08:00
Fedor Indutnyy
50230955c0 gyp: copy windows lib properly 2023-01-25 21:25:05 -08:00
Fedor Indutnyy
d4af9a32c1 test: rename 2023-01-25 20:05:41 -08:00
Fedor Indutnyy
56d9872ae5 8.3.0-rc.2 2023-01-25 19:56:46 -08:00
Fedor Indutnyy
dbe34b75a4 deps: fix gyp bug 2023-01-25 19:56:40 -08:00
Fedor Indutnyy
0a1cf9cae6 8.3.0-rc.1 2023-01-25 19:37:13 -08:00
Fedor Indutnyy
ac7338ad39 src: use our custom icu_tokenizer 2023-01-25 19:36:39 -08:00
Fedor Indutnyy
0807c512f3 8.2.1 2023-01-25 15:07:07 -08:00
Fedor Indutnyy
dc255273fe lib: fix build on windows, better API name 2023-01-25 15:06:02 -08:00
Fedor Indutnyy
6d8b420079 8.2.0 2023-01-25 14:15:53 -08:00
Fedor Indutny
05a8cc2be6
lib: better tokenizer API 2023-01-25 14:15:29 -08:00
Fedor Indutnyy
4830a01ea6 fix minor style issue 2023-01-24 18:11:26 -08:00
Fedor Indutnyy
3b92255205 8.2.0-rc.1 2023-01-24 18:03:15 -08:00
Fedor Indutnyy
a2b01b922a feat: js tokenizer 2023-01-24 18:02:39 -08:00
Fedor Indutnyy
5054b50288 8.1.1 2023-01-18 15:07:29 -08:00
Fedor Indutnyy
6beeec2f07 deps: slight build optimizations 2023-01-18 15:05:28 -08:00
Fedor Indutnyy
ca250488c4 8.1.0 2023-01-18 13:05:08 -08:00
Fedor Indutnyy
7331a93430 api: add setLogHandler 2023-01-18 13:03:53 -08:00
Fedor Indutnyy
835217678a 8.0.4 2022-12-15 17:20:32 -08:00
Fedor Indutny
1bf85ef39e
Fix crashes on Electron 22 2022-12-15 17:20:13 -08:00
Fedor Indutnyy
22ef5d8cc6 8.0.3 2022-12-14 11:32:16 -08:00
Fedor Indutnyy
ce2676e8d8 Fix windows builds 2022-12-14 11:32:04 -08:00
Fedor Indutnyy
c821d92663 8.0.2 2022-12-14 10:58:49 -08:00
Fedor Indutnyy
cb49b9118d 8.0.1 2022-12-14 10:58:33 -08:00
Fedor Indutnyy
87e3a97254 8.0.1-rc.1 2022-12-14 10:46:49 -08:00
Fedor Indutnyy
00d21ca645 Embed types 2022-12-14 10:46:49 -08:00
Fedor Indutnyy
3a340883dd Merge remote-tracking branch 'upstream/master' into better-sqlcipher 2022-12-14 10:39:13 -08:00
Fedor Indutnyy
bd45ac7818 Move build artifact to CDN 2022-12-14 10:32:47 -08:00
mceachen
2194095aa1 8.0.1 2022-12-02 01:53:33 +00:00
qier222
f0e622dd8c
Add arm64 prebuilds for macOS (#859)
Co-authored-by: Matthew McEachen <matthew@photostructure.com>
2022-12-01 17:42:41 -08:00
Mahesh Bandara Wijerathna
5eab05e68d
Add support for electron v22 prebuilds (#915) 2022-12-01 17:27:52 -08:00
Mahesh Bandara Wijerathna
b992d041cb
Add support for electron v21 prebuilds (#909) 2022-11-25 17:21:37 -08:00
Joshua Wise
ae12001cdf updated recommended Node.js versions in docs 2022-11-21 19:18:06 -06:00
Joshua Wise
dc76c77326 8.0.0 2022-11-21 18:29:03 -06:00
Joshua Wise
36cf1c47bd upgraded to SQLite version 3.40.0 2022-11-21 17:51:44 -06:00
Danilo Bargen
1b32770139
Sort defines alphabetically (#878)
When modifying the defines, this makes it less likely to accidentally
add a define twice.

This also includes a fix for the documentation patching, which
previously ignored the HAVE_* variables and duplicated them on every
call to `download.sh`.
2022-11-21 17:48:17 -06:00
neoxpert
ba9fcecb87
Support build for electron v20 (#870)
* support prebuild for electron v20

* Remove unsupported versions of Node and Electron

(see https://www.electronjs.org/docs/latest/tutorial/electron-timelines and https://github.com/nodejs/Release/#end-of-life-releases)

* enforce the use of node-gyp 8.4.1

builds for electron 20 fail with prebuild 11.0.4 internal node-gyp version 6.1.0

* conditional compile with CreationContext for NodeJs < 16

* use c++17 to fix Mac M1 and Linux x64 builds

* add c++17 flag for MSVC

Co-authored-by: Matthew McEachen <matthew@photostructure.com>
2022-11-21 17:31:03 -06:00
John Haugeland
54e6cbadfb
Please consider setting pragma WAL in your introductory documentation (#831)
The performance difference can be several orders of magnitude on even simple tables, and most users won't think to go looking for this.

I've included lines that clarify, and link to your existing documentation on the topic.

Thank you for an excellent tool.
2022-11-21 17:18:48 -06:00
Fedor Indutnyy
afdbd49cf1 Fix build with Electron 21-x-y 2022-11-17 16:31:17 -08:00
Fedor Indutnyy
c4ab3697af Update sqlcipher to support LIMIT in DELETE 2022-11-12 16:36:45 -08:00
Fedor Indutnyy
5caf211b72 Revert "Optimize build side on macOS"
This reverts commit dab57610e8.
2022-11-03 20:49:51 -07:00
Fedor Indutnyy
f51f6fe710 Revert "Optimize linux build size"
This reverts commit fc70f3d416.
2022-11-03 20:49:44 -07:00
Fedor Indutnyy
fc70f3d416 Optimize linux build size 2022-11-03 19:54:44 -07:00
Fedor Indutnyy
dab57610e8 Optimize build side on macOS 2022-11-03 19:49:33 -07:00
Fedor Indutnyy
1cfaa8c9c6 Update sqlcipher to 4.5.2 2022-11-03 19:30:11 -07:00
Joshua Wise
d66747ef56
Update SQLite to version 3.39.4 (#896)
Co-authored-by: mceachen <mceachen@users.noreply.github.com>
2022-10-27 14:47:27 -07:00
Nathan Hammond
6a4a80090c
Remove npm run from install lifecycle script. (#894)
This ensures compatibility with all package manager installation choices.

Closes #893.
2022-10-27 14:45:33 -07:00
Ryusei Yamaguchi
15652ad800
Fix virtual table limit offset (#873)
* Don't care limit and offset constraints of user-defined virtual tables

* Add a test case for #872
2022-10-12 10:36:44 -05:00
Fedor Indutnyy
a92f637708 Fix linux builds 2022-08-25 11:28:35 -07:00
Fedor Indutnyy
52a3df2a31 7.5.1 2022-08-25 09:57:21 -07:00
Fedor Indutnyy
65d5e336e2 Fix electron 20 build errors 2022-08-25 09:56:32 -07:00
mceachen
0c42307437 7.6.2 2022-07-15 22:49:58 +00:00
Mahesh Bandara Wijerathna
4c6a583c94
Add support for electron v19 prebuilds (#834) 2022-07-15 12:59:42 -07:00
Mahesh Bandara Wijerathna
157ea8bd83
Update prebuild dependency to v11.0.4 (#845) 2022-07-15 12:59:30 -07:00
mceachen
e7d1b668f2 7.6.1 2022-07-13 23:58:41 +00:00
Joshua Wise
de2b2e95d0
Update SQLite to version 3.39.1 (#841)
Co-authored-by: mceachen <mceachen@users.noreply.github.com>
2022-07-13 16:56:56 -07:00
JoshuaWise
793c6b2f84 7.6.0 2022-07-09 05:32:33 +00:00
Joshua Wise
3c9ac3b6c6
Update SQLite to version 3.39.0 (#828)
Co-authored-by: mceachen <matthew@photostructure.com>
2022-06-25 11:40:15 -07:00
Matthew McEachen
f52b3b00cf
Update troubleshooting.md (#814)
Updated the instructions for Windows builds: things should "just work" if they click "Automatically install the necessary tools" and follow the instructions.
2022-05-18 09:36:48 -07:00
Patrick Martin
f639d61200
Update troubleshooting.md (#752)
As I just learned, having spaces in the path of the directory which you are running `npm i better-sqlite3` can cause issues with installation, presumably because, according to the error message, it does not insert escapes for characters like spaces.
2022-05-16 10:11:16 -07:00
Joshua Wise
28667b11bd Merge branch 'master' of github.com:JoshuaWise/better-sqlite3 2022-05-16 01:44:30 -05:00
Joshua Wise
e4a1442f51 added instructions on creating releases 2022-05-16 01:44:21 -05:00
JoshuaWise
8fd426f3e5 7.5.3 2022-05-16 06:31:18 +00:00
Joshua Wise
2083409858 fixed bump-version workflow 2022-05-16 01:29:57 -05:00
Joshua Wise
e56fae6954 added a workflow for bumping versions 2022-05-16 01:18:46 -05:00
Joshua Wise
dfbedccf83 updated contribution rules 2022-05-16 00:42:14 -05:00
Joshua Wise
a39e0acd84 updated contribution rules 2022-05-16 00:41:46 -05:00
Joshua Wise
499f748ae0 updated contribution rules 2022-05-16 00:36:39 -05:00
Joshua Wise
25e1c11650 updated contribution rules 2022-05-16 00:36:15 -05:00
Joshua Wise
55fec9ec6a updated contribution rules 2022-05-16 00:32:18 -05:00
Joshua Wise
3cb78917ec updated contribution rules 2022-05-16 00:31:57 -05:00
Joshua Wise
76ab48f0b0 updated contribution rules 2022-05-16 00:31:29 -05:00
Joshua Wise
c39c7deaf8 updated contribution rules 2022-05-16 00:30:29 -05:00
Joshua Wise
223598bf0e updated contribution rules 2022-05-16 00:29:42 -05:00
Joshua Wise
a7b1d94c87 updated contribution rules 2022-05-16 00:29:04 -05:00
Joshua Wise
9130cc8aee updated contribution rules 2022-05-16 00:21:34 -05:00
Joshua Wise
b1144a8232 updated contribution rules 2022-05-16 00:21:05 -05:00
Joshua Wise
1f58728bde updated contribution rules 2022-05-16 00:16:41 -05:00
Joshua Wise
48fdf9a3a4 updated contribution rules 2022-05-16 00:15:35 -05:00
Joshua Wise
9cd009ba04 updated CODEOWNERS 2022-05-16 00:05:36 -05:00
Joshua Wise
9a98290bb4 fixed download script for linux 2022-05-15 23:45:24 -05:00
Joshua Wise
81fee0a8d7 added code of conduct and contribution rules 2022-05-15 22:55:40 -05:00
Joshua Wise
6da9e569db 7.5.2 2022-05-15 11:02:48 -05:00
Matthew McEachen
7aec2ac297
Upgrade SQLite to 3.38.5. Update dependencies. (#808)
* pull in 3.38.5

* Update dependencies with ncu -u

* downgrade mocha to 8, as mocha v10 dropped support for node 10.
2022-05-15 10:58:28 -05:00
Mahesh Bandara Wijerathna
e886811f77 Add Node v18 prebuild support 2022-05-13 10:39:24 -07:00
Fedor Indutnyy
587721adcf deps: add prebuilt OpenSSL-linux-arm64 2022-05-09 14:11:47 -07:00
Fedor Indutnyy
3c4a7eebba Merge remote-tracking branch 'upstream/master' 2022-04-07 16:46:10 -07:00
Fedor Indutny
bd26a8145b sqlcipher support 2022-04-07 16:44:24 -07:00
Joshua Wise
feba6d6cc3 7.5.1 2022-04-06 23:52:15 -05:00
Joshua Wise
05da7712eb replaced github token with PAT in update-sqlite workflow 2022-04-06 23:48:49 -05:00
Mahesh Bandara Wijerathna
7d37224630
Add update-sqlite workflow (#750)
Automates the update process of SQLite.
2022-04-06 23:47:54 -05:00
jochenschmich-aeberle
fbde33b1fc Add support for electron v18 prebuilds (#790)
Co-authored-by: Joshua Wise <8255757+JoshuaWise@users.noreply.github.com>
2022-04-06 23:29:04 -05:00
Joshua Wise
b544892631 attempt at fixing windows CI 2022-04-06 23:29:04 -05:00
Joshua Wise
d09615c936 upgraded to SQLite version 3.38.2, and removed symlinks during build phase 2022-04-06 22:21:36 -05:00
Mahesh Bandara Wijerathna
5e07181b11
Add Linux prebuilt binaries for armv7 and arm64 and other improvements (#758)
* Add support for alpine prebuilt binaries on `armv7`

This job now uses the `checkout` action making it much cleaner and reliable.

* Add support for Linux prebuilt binaries on `armv7` & `arm64`
2022-04-06 21:19:02 -05:00
Mahesh Bandara Wijerathna
fa58644ca1 Add support for electron v17 prebuilds 2022-01-31 21:55:27 -08:00
Joshua Wise
8c7c8e5025 tweak to api docs 2022-01-18 20:39:55 -06:00
Joshua Wise
98095c4edb 7.5.0 2022-01-18 20:28:14 -06:00
Joshua Wise
e9ee374077 fixed test 2022-01-18 20:15:14 -06:00
Joshua Wise
963327a801 fixed strange windows build issue 2022-01-18 20:01:48 -06:00
Joshua Wise
9ad75ac5c8 added temporary console.logs for debugging CI 2022-01-18 19:53:09 -06:00
Joshua Wise
032746ba1d added temporary console.logs for debugging CI 2022-01-18 19:46:11 -06:00
Joshua Wise
9f6050a562 implemented "nativeBinding" database option 2022-01-18 17:45:57 -06:00
Joshua Wise
539ff32b96 implemented statement.busy getter 2022-01-18 16:19:51 -06:00
Joshua Wise
6ab63c1f63 fixed obscure bug 2022-01-18 16:01:01 -06:00
Joshua Wise
fc565653d6 made SqliteError code property enumerable 2022-01-18 15:37:47 -06:00
Joshua Wise
8e11207eec updated docs 2022-01-18 15:34:02 -06:00
Joshua Wise
da19254fc2 added example for adding compile-time options to a custom amalgamation 2022-01-18 15:32:29 -06:00
Joshua Wise
0f871ba54a added documentation for SqliteError, and improved error message for binding non-plain objects 2022-01-18 15:23:36 -06:00
Joshua Wise
12d0083151 dont show diffs for sqlite3 source files or files generated by lzz 2022-01-18 14:40:10 -06:00
Joshua Wise
1e7e1d090f upgraded SQLite to 3.37.2, and removed tar dependency 2022-01-18 14:36:46 -06:00
Joshua Wise
230ea65ed0 7.4.6 2021-12-29 18:30:35 -05:00
Joshua Wise
5bbd71d585 upgraded to SQLite 3.37.0 2021-12-29 18:30:29 -05:00
Joshua Wise
ccc9460ed0 tweak to README.md 2021-12-29 18:26:39 -05:00
John Haugeland
e488ff47b9
Add an example using import in Typescript (#676)
* Add an example using `import` in Typescript

It's much harder to track down the expected syntax than one would want.  Could we just toss a note in?

* Update README.md
2021-12-29 17:14:06 -06:00
Mahesh Bandara Wijerathna
7dd0a395fd
Add workflow_dispatch trigger for tests (#731) 2021-12-29 17:11:04 -06:00
Mahesh Bandara Wijerathna
b67b9696b1
Use github context properties in arm64-alpine prebuild job (#729) 2021-12-29 17:10:08 -06:00
Joshua Wise
fedcca3e31 7.4.5 2021-11-19 16:36:34 -06:00
Antti
ac4d76a04a
add arm64 prebuilds for alpine (#714)
* add arm64 prebuilds for alpine

* run arm64 build inside emulated arm64 docker container
2021-11-19 16:10:16 -06:00
Mahesh Bandara Wijerathna
bd2a69e3e6
Update prebuild-install to v7.0.0 (#724) 2021-11-19 16:02:09 -06:00
Mahesh Bandara Wijerathna
6c97553971 Add electron v16 prebuild support 2021-11-15 22:23:35 -08:00
Joshua Wise
1ac101d7de 7.4.4 2021-10-25 08:32:43 -05:00
Joshua Wise
22c2ab8199 reverted mocha version 2021-10-25 08:31:23 -05:00
Joshua Wise
5858f6c8c0 Merge branch 'master' of github.com:JoshuaWise/better-sqlite3 2021-10-25 08:29:14 -05:00
Matthew McEachen
ad9b28e502
Upgrade depencies (#702)
Addresses #692 and #694
2021-10-25 08:29:03 -05:00
Oliver Kamer
8a70a92430
Prebuild for alpine (#641)
* prebuild node for alpine

This should speed up CI in docker containers a lot

* Fix the build for alpine

Much simpler and should work

* hopefully final fix

I'm not good at the github workflow stuff

Co-authored-by: Joshua Wise <8255757+JoshuaWise@users.noreply.github.com>
2021-10-25 08:27:05 -05:00
Ameen Rashad Vazhayil
ff15af59da
ci: add prebuild for electron 32 bit windows (#584)
There is a many leading electron applications which is supports windows 32bit version. So it will be good to have a prebuild for that.

Co-authored-by: Joshua Wise <8255757+JoshuaWise@users.noreply.github.com>
2021-10-25 08:20:55 -05:00
Joshua Wise
28195b3ca5 added CODEOWNERS 2021-10-25 08:07:50 -05:00
Kunal Dabir
b433c8a2ee
add electron 14 and 15 (#705) 2021-10-25 07:24:09 -05:00
Jan Alexander Steffens
23c56aa77e
Encapsulate the internal SQLite (#684)
Add linker flags to
 - prevent the dynamic linker from using an external SQLite for this
   library (`-Bsymbolic`) and
 - prevent the dynamic linker from using the internal SQLite for other
   libraries (`--exclude-libs ALL`).

This is just a (conflict-free) cherry-pick of a Signal-specific fix in
https://github.com/signalapp/better-sqlite3/pull/3 that seems OK to
upstream further.
2021-08-23 13:41:58 -05:00
Joshua Wise
1fa3c77e90 7.4.3 2021-07-19 00:13:43 -05:00
Joshua Wise
b3169ef6d6 fixed bug in node 14 where empty buffers are bound as null 2021-07-19 00:13:37 -05:00
Joshua Wise
3467abdd4c 7.4.2 2021-07-18 23:45:16 -05:00
Joshua Wise
4d316a5dfc added SQLITE_ENABLE_MATH_FUNCTIONS 2021-07-18 23:44:34 -05:00
Joshua Wise
d094aac3f5 updated tests 2021-07-18 21:47:44 -05:00
Joshua Wise
be7770e054 upgraded to sqlite 3.36 2021-07-18 21:27:11 -05:00
Joshua Wise
115258a106 fixed typo in docs 2021-06-05 21:30:05 -05:00
Joshua Wise
309708be17 7.4.1 2021-06-05 21:27:11 -05:00
Matthew Rathbone
ed3d203086
Fixes GLIBC 2.29 issues, electron issues, and more (#631)
* Update build.yml

* everything on 18.04
2021-06-05 21:24:09 -05:00
Joshua Wise
ea5725b5fd added support for node v16 2021-06-05 21:23:55 -05:00
Joshua Wise
a65bec1ba2 improved documentation 2021-06-05 21:19:08 -05:00
Alexander Prinzhorn
4fe0ae3240
Document empty string -> tmp db (#629) 2021-06-05 21:10:10 -05:00
Paul Rosania
bd52e0408b
Generate prebuilds for Electron 13 (#638) 2021-06-05 21:09:19 -05:00
Alexander Prinzhorn
02c9c250bc
This comes up frequently (#625)
See https://github.com/JoshuaWise/better-sqlite3/issues/609#issuecomment-829013505
2021-05-19 13:47:09 -05:00
Joshua Wise
8d06b17720 7.4.0 2021-05-14 09:32:31 -05:00
Joshua Wise
b33c1d50fc added missing C++ header 2021-05-12 23:55:34 -05:00
Joshua Wise
2b8ad64bd3 edited comment 2021-05-12 23:52:15 -05:00
Joshua Wise
843026fe01 fixed bug in edge case 2021-05-12 23:46:33 -05:00
Joshua Wise
43dd2887b6 added test 2021-05-12 21:17:05 -05:00
Joshua Wise
4ee9fc7dee improved code readability 2021-05-12 21:07:44 -05:00
Joshua Wise
174559d0f3 Merge branch 'master' into vtab 2021-05-12 18:40:30 -05:00
Joshua Wise
4789dec29c Merge branch 'after-vtab' into vtab 2021-05-12 17:57:25 -05:00
Joshua Wise
36fab27b55
Table-valued functions (stateless read-only virtual tables) (#617)
* implemented rought draft version of virtual tables

* slightly cleaned up code

* added documentation for virtual tables

* updated api docs

* wrote tests for virtual tables

* made virtual tables strict about preventing missing/extraneous columns from being yielded

* fixed test

* updated api docs

* improved documentation, added tests, and micro-optimizations
2021-05-12 17:51:33 -05:00
Joshua Wise
a72acd5842 improved documentation, added tests, and micro-optimizations 2021-05-12 17:36:11 -05:00
Joshua Wise
1a54e931fd updated api docs 2021-05-11 02:15:16 -05:00
Joshua Wise
0a0bc8b7f5 fixed test 2021-05-11 01:39:32 -05:00
Joshua Wise
38557cf2a4 made virtual tables strict about preventing missing/extraneous columns from being yielded 2021-05-11 01:36:14 -05:00
Joshua Wise
9227a57de3 wrote tests for virtual tables 2021-05-11 01:05:54 -05:00
Joshua Wise
5957a1ac5d updated api docs 2021-05-10 12:49:24 -05:00
Joshua Wise
3768dbedd9 added documentation for virtual tables 2021-05-10 03:55:39 -05:00
Joshua Wise
6e24b2b03b cleaned up C++ code 2021-05-07 19:05:12 -05:00
Joshua Wise
b782198a5d slightly cleaned up code 2021-05-07 02:25:18 -05:00
Joshua Wise
1d8e9f6f86 implemented rought draft version of virtual tables 2021-05-07 01:50:07 -05:00
Joshua Wise
6de7e28b46 updated docs 2021-05-05 08:55:49 -05:00
Joshua Wise
74f9ee7ea9 updated docs 2021-05-05 08:54:30 -05:00
Joshua Wise
696719b8f5 7.3.1 2021-05-04 21:39:37 -05:00
Joshua Wise
877fba8f56 Merge branch 'master' of github.com:JoshuaWise/better-sqlite3 2021-05-04 21:39:28 -05:00
Joshua Wise
0ed6f40e1e prevented db.serialize() from being used while the database is busy or closed 2021-05-04 21:39:01 -05:00
Tobias Nießen
b7a3b2efdc
Fix typo in API docs (#613) 2021-05-04 20:27:47 -05:00
Joshua Wise
3893b33f0b 7.3.0 2021-05-04 20:09:48 -05:00
Joshua Wise
f58d99df91 updated docs 2021-05-04 19:58:57 -05:00
Joshua Wise
0ad86ca5e0 added directOnly option to db.aggregate() 2021-05-04 19:57:29 -05:00
Joshua Wise
260b854e47 added directOnly option to db.function() 2021-05-04 19:52:07 -05:00
Joshua Wise
1a3efc20a9 updated readme 2021-05-04 19:36:13 -05:00
Joshua Wise
e8dcaa4414 updated readme 2021-05-04 19:35:35 -05:00
Joshua Wise
3af2d6b8b6 updated readme 2021-05-04 19:30:45 -05:00
Joshua Wise
dcb58cd070 implemented stmt.readonly property 2021-05-04 19:29:53 -05:00
Joshua Wise
405052f115 removed HAVE_USLEEP 2021-05-04 18:56:46 -05:00
Joshua Wise
83776dcb0d updated readme 2021-05-04 18:52:01 -05:00
Joshua Wise
fcdf57fa14 updated readme 2021-05-04 18:50:08 -05:00
Joshua Wise
b038df60c9 updated readme 2021-05-04 18:49:05 -05:00
Joshua Wise
c004940840 updated readme 2021-05-04 18:48:34 -05:00
Joshua Wise
e208564193 implemented db.serialize() and deserialization 2021-05-04 18:47:20 -05:00
Joshua Wise
61bb6cb842 made timing test even more lenient 2021-05-04 02:36:42 -05:00
Joshua Wise
a7db75c1ce made timing test more lenient 2021-05-04 02:33:39 -05:00
Joshua Wise
418ba29b0c added HAVE_USLEEP and HAVE_STDINT_H compile-time options 2021-05-04 01:45:03 -05:00
Joshua Wise
898946e580 7.2.0 2021-05-04 01:12:33 -05:00
Joshua Wise
579e3b4b4b * added support for the RETURNING clause
* statements that return data can now be executed via .run()
2021-05-04 01:06:46 -05:00
Joshua Wise
4e68294e1c 7.1.5 2021-04-14 17:57:38 -05:00
Joshua Wise
e8057ec4ab fixed database subclassing 2021-04-14 17:53:42 -05:00
66 changed files with 5788 additions and 4785 deletions

4
.gitattributes vendored
View File

@ -1,2 +1,2 @@
*.lzz linguist-language=C++
deps/sqlite3.tar.gz filter=lfs diff=lfs merge=lfs -text
*.c -diff
*.h -diff

9
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,9 @@
* @JoshuaWise
/package.json @JoshuaWise @WiseLibs/better-sqlite3-team
/docs/compilation.md @JoshuaWise @WiseLibs/better-sqlite3-team
/docs/performance.md @JoshuaWise @WiseLibs/better-sqlite3-team
/docs/troubleshooting.md @JoshuaWise @WiseLibs/better-sqlite3-team
/deps/sqlite3/sqlite3.c @JoshuaWise @WiseLibs/better-sqlite3-team
/deps/sqlite3/sqlite3.h @JoshuaWise @WiseLibs/better-sqlite3-team
/deps/sqlite3/sqlite3ext.h @JoshuaWise @WiseLibs/better-sqlite3-team
/.github/workflows/build.yml @JoshuaWise @WiseLibs/better-sqlite3-team

View File

@ -10,6 +10,7 @@ on:
release:
types:
- released
workflow_dispatch: {}
jobs:
@ -17,13 +18,13 @@ jobs:
strategy:
matrix:
os:
- ubuntu-latest
- ubuntu-20.04
- macos-latest
- windows-latest
- windows-2019
node:
- 10
- 12
- 14
- 16
- 18
name: Testing Node ${{ matrix.node }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps:
@ -38,13 +39,13 @@ jobs:
publish:
if: ${{ github.event_name == 'release' }}
name: Publishing to NPM
runs-on: ubuntu-latest
runs-on: ubuntu-18.04
needs: test
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
registry-url: https://registry.npmjs.org
- run: npm publish
env:
@ -54,9 +55,9 @@ jobs:
strategy:
matrix:
os:
- ubuntu-latest
- ubuntu-18.04
- macos-latest
- windows-latest
- windows-2019
name: Prebuild on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: publish
@ -64,7 +65,59 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
- run: npm install --ignore-scripts
- run: npx --no-install prebuild -r node -t 10.20.0 -t 12.0.0 -t 14.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
- run: npx --no-install prebuild -r electron -t 10.0.0 -t 11.0.0 -t 12.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
- run: npx --no-install prebuild -r node -t 14.0.0 -t 16.0.0 -t 18.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
- run: npx --no-install prebuild -r electron -t 16.0.0 -t 17.0.0 -t 18.0.0 -t 19.0.0 -t 20.0.0 -t 21.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
- if: matrix.os == 'windows-2019'
run: npx --no-install prebuild -r electron -t 16.0.0 -t 17.0.0 -t 18.0.0 -t 19.0.0 -t 20.0.0 -t 21.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' --arch ia32 -u ${{ secrets.GITHUB_TOKEN }}
- if: matrix.os == 'macos-latest'
run: npx --no-install prebuild -r electron -t 16.0.0 -t 17.0.0 -t 18.0.0 -t 19.0.0 -t 20.0.0 -t 21.0.0 -t 22.0.0 --include-regex 'better_sqlite3.node$' --arch arm64 -u ${{ secrets.GITHUB_TOKEN }}
prebuild-alpine:
name: Prebuild on alpine
runs-on: ubuntu-latest
container: node:16-alpine
needs: publish
steps:
- uses: actions/checkout@v2
- run: apk add build-base git python3 --update-cache
- run: npm install --ignore-scripts
- run: npx --no-install prebuild -r node -t 14.0.0 -t 16.0.0 -t 18.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
prebuild-alpine-arm:
strategy:
matrix:
arch:
- arm/v7
- arm64
name: Prebuild on alpine (${{ matrix.arch }})
runs-on: ubuntu-latest
needs: publish
steps:
- uses: actions/checkout@v2
- uses: docker/setup-qemu-action@v1
- run: |
docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:16-alpine -c "\
apk add build-base git python3 --update-cache && \
cd /tmp/project && \
npm install --ignore-scripts && \
npx --no-install prebuild -r node -t 14.0.0 -t 16.0.0 -t 18.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}"
prebuild-linux-arm:
strategy:
matrix:
arch:
- arm/v7
- arm64
name: Prebuild on Linux (${{ matrix.arch }})
runs-on: ubuntu-latest
needs: publish
steps:
- uses: actions/checkout@v2
- uses: docker/setup-qemu-action@v1
- run: |
docker run --rm -v $(pwd):/tmp/project --entrypoint /bin/sh --platform linux/${{ matrix.arch }} node:16 -c "\
cd /tmp/project && \
npm install --ignore-scripts && \
npx --no-install prebuild -r node -t 14.0.0 -t 16.0.0 -t 18.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}"

36
.github/workflows/bump-version.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: bump-version
on:
workflow_dispatch:
inputs:
type:
type: choice
description: Type of version bump
required: true
options:
- patch
- minor
- major
jobs:
bump:
name: Bump to a new version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.PAT }}
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 16
- name: Configure user
run: |
git config --local user.name "${{ github.actor }}"
git config --local user.email "${{ github.actor }}@users.noreply.github.com"
- name: Bump the version
run: npm version ${{ github.event.inputs.type }}
- name: Push commit
run: git push origin master:master
- name: Push tag
run: git push origin --tags

48
.github/workflows/update-sqlite.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: update-sqlite
on:
workflow_dispatch:
inputs:
year:
description: SQLite release year
required: true
version:
description: SQLite version (encoded)
required: true
jobs:
download-and-update:
name: Download and update SQLite
runs-on: ubuntu-latest
env:
ENV_YEAR: ${{ github.event.inputs.year }}
ENV_VERSION: ${{ github.event.inputs.version }}
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.PAT }}
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 16
- name: Create new update branch
run: git checkout -b sqlite-update-${{ env.ENV_VERSION }}
- name: Update download script
run: |
sed -Ei "s/YEAR=\"[0-9]+\"/YEAR=\"${{ env.ENV_YEAR }}\"/g" ./deps/download.sh
sed -Ei "s/VERSION=\"[0-9]+\"/VERSION=\"${{ env.ENV_VERSION }}\"/g" ./deps/download.sh
echo "ENV_TRUE_VERSION=$((10#${ENV_VERSION:0:1})).$((10#${ENV_VERSION:1:2})).$((10#${ENV_VERSION:3:2}))" >> $GITHUB_ENV
- name: Download, compile and package SQLite
run: npm run download
- name: Push update branch
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Update SQLite to version ${{ env.ENV_TRUE_VERSION }}
branch: sqlite-update-${{ env.ENV_VERSION }}
- name: Create new PR
uses: repo-sync/pull-request@v2
with:
github_token: ${{ secrets.PAT }}
source_branch: sqlite-update-${{ env.ENV_VERSION }}
pr_title: Update SQLite to version ${{ env.ENV_TRUE_VERSION }}
pr_body: This is an automated pull request, updating SQLite to version \`${{ env.ENV_TRUE_VERSION }}\`.

4
.gitignore vendored
View File

@ -40,3 +40,7 @@ lib/binding
temp/
TODO
.local
# Downloaded artifact
deps/unverified.tmp
deps/sqlcipher.tar.gz

25
ACKNOWLEDGMENTS.md Normal file
View File

@ -0,0 +1,25 @@
# Acknowledgments
## @types/better-sqlite3
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

View File

@ -5,7 +5,7 @@ The fastest and simplest library for SQLite3 in Node.js.
- Full transaction support
- High performance, efficiency, and safety
- Easy-to-use synchronous API *(better concurrency than an asynchronous API... yes, you read that correctly)*
- Support for user-defined functions, aggregates, and extensions
- Support for user-defined functions, aggregates, virtual tables, and extensions
- 64-bit integers *(invisible until you need them)*
- Worker thread support *(for large/slow queries)*
@ -32,9 +32,7 @@ The fastest and simplest library for SQLite3 in Node.js.
npm install better-sqlite3
```
> You must be using Node.js v10.20.1 or above. Prebuilt binaries are available for [LTS versions](https://nodejs.org/en/about/releases/).
> If you have trouble installing, check the [troubleshooting guide](./docs/troubleshooting.md).
> You must be using Node.js v14.21.1 or above. Prebuilt binaries are available for [LTS versions](https://nodejs.org/en/about/releases/). If you have trouble installing, check the [troubleshooting guide](./docs/troubleshooting.md).
## Usage
@ -45,6 +43,20 @@ const row = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
console.log(row.firstName, row.lastName, row.email);
```
Though not required, [it is generally important to set the WAL pragma for performance reasons](https://github.com/WiseLibs/better-sqlite3/blob/master/docs/performance.md).
```js
db.pragma('journal_mode = WAL');
```
##### In ES6 module notation:
```js
import Database from 'better-sqlite3';
const db = new Database('foobar.db', options);
db.pragma('journal_mode = WAL');
```
## Why should I use this instead of [node-sqlite3](https://github.com/mapbox/node-sqlite3)?
- `node-sqlite3` uses asynchronous APIs for tasks that are either CPU-bound or serialized. That's not only bad design, but it wastes tons of resources. It also causes [mutex thrashing](https://en.wikipedia.org/wiki/Resource_contention) which has devastating effects on performance.
@ -71,7 +83,9 @@ For these situations, you should probably use a full-fledged RDBMS such as [Post
- [64-bit integer support](./docs/integer.md)
- [Worker thread support](./docs/threads.md)
- [Unsafe mode (advanced)](./docs/unsafe.md)
- [SQLite3 compilation](./docs/compilation.md)
- [SQLite3 compilation (advanced)](./docs/compilation.md)
- [Contribution rules](./docs/contribution.md)
- [Code of conduct](./docs/conduct.md)
# License

View File

@ -9,9 +9,13 @@
'target_name': 'better_sqlite3',
'dependencies': ['deps/sqlite3.gyp:sqlite3'],
'sources': ['src/better_sqlite3.cpp'],
'cflags': ['-std=c++14'],
'xcode_settings': {
'OTHER_CPLUSPLUSFLAGS': ['-std=c++14', '-stdlib=libc++'],
'cflags_cc': ['-std=c++20'],
'msvs_settings': {
'VCCLCompilerTool': {
'AdditionalOptions': [
'/std:c++20',
],
},
},
'conditions': [
['OS=="linux"', {
@ -22,10 +26,5 @@
}],
],
},
{
'target_name': 'test_extension',
'dependencies': ['deps/sqlite3.gyp:sqlite3'],
'conditions': [['sqlite3 == ""', { 'sources': ['deps/test_extension.c'] }]],
},
],
}

31
deps/common.gypi vendored
View File

@ -13,19 +13,20 @@
},
},
'conditions': [
['target_arch == "x64"', {
'variables': {
'rust_arch%': 'x86_64',
}
}, {
'variables': {
'rust_arch%': 'aarch64',
}
}],
['OS == "win"', {
'defines': ['WIN32'],
'conditions': [
['target_arch == "ia32"', {
'variables': {
'openssl_root%': 'OpenSSL-Win32',
}
}, {
'variables': {
'openssl_root%': 'OpenSSL-Win64',
}
}]
],
'variables': {
'openssl_root%': 'OpenSSL-win-<(target_arch)',
}
}],
],
'configurations': {
@ -45,7 +46,7 @@
'-O0',
],
'xcode_settings': {
'MACOSX_DEPLOYMENT_TARGET': '10.7',
'MACOSX_DEPLOYMENT_TARGET': '10.15',
'GCC_OPTIMIZATION_LEVEL': '0',
'GCC_GENERATE_DEBUGGING_SYMBOLS': 'YES',
},
@ -67,11 +68,15 @@
'-O3',
],
'xcode_settings': {
'MACOSX_DEPLOYMENT_TARGET': '10.7',
'MACOSX_DEPLOYMENT_TARGET': '10.15',
'GCC_OPTIMIZATION_LEVEL': '3',
'GCC_GENERATE_DEBUGGING_SYMBOLS': 'NO',
'DEAD_CODE_STRIPPING': 'YES',
'GCC_INLINES_ARE_PRIVATE_EXTERN': 'YES',
'OTHER_CPLUSPLUSFLAGS': ['-std=c++20', '-stdlib=libc++'],
'GCC_ENABLE_CPP_EXCEPTIONS': 'NO',
'GCC_SYMBOLS_PRIVATE_EXTERN': 'YES', # -fvisibility=hidden
'LLVM_LTO': 'YES',
},
},
},

26
deps/copy.js vendored Normal file
View File

@ -0,0 +1,26 @@
'use strict';
const path = require('path');
const fs = require('fs');
const dest = process.argv[2];
const source = path.resolve(path.sep, process.argv[3] || path.join(__dirname, 'sqlite3'));
const files = [
{ filename: 'sqlite3.c', optional: false },
{ filename: 'sqlite3.h', optional: false },
];
if (process.argv[3]) {
// Support "_HAVE_SQLITE_CONFIG_H" in custom builds.
files.push({ filename: 'config.h', optional: true });
} else {
// Required for some tests.
files.push({ filename: 'sqlite3ext.h', optional: false });
}
for (const { filename, optional } of files) {
if (optional && !fs.existsSync(path.join(source, filename))) {
continue;
}
fs.accessSync(path.join(source, filename));
fs.copyFileSync(path.join(source, filename), path.join(dest, filename));
}

25
deps/defines.gypi vendored
View File

@ -1,28 +1,47 @@
# THIS FILE IS AUTOMATICALLY GENERATED (DO NOT EDIT)
{
'defines': [
'SQLITE_LIKE_DOESNT_MATCH_BLOBS',
'SQLITE_THREADSAFE=2',
'SQLITE_USE_URI=0',
'SQLITE_DEFAULT_MEMSTATUS=0',
'SQLITE_OMIT_AUTOINIT',
'SQLITE_OMIT_DEPRECATED',
'SQLITE_OMIT_DESERIALIZE',
'SQLITE_OMIT_GET_TABLE',
'SQLITE_OMIT_TCL_VARIABLE',
'SQLITE_OMIT_PROGRESS_CALLBACK',
'SQLITE_OMIT_SHARED_CACHE',
'SQLITE_OMIT_UTF16',
'SQLITE_OMIT_COMPLETE',
'SQLITE_OMIT_GET_TABLE',
'SQLITE_OMIT_AUTHORIZATION',
'SQLITE_OMIT_LOAD_EXTENSION',
'SQLITE_TRACE_SIZE_LIMIT=32',
'SQLITE_DEFAULT_CACHE_SIZE=-16000',
'SQLITE_DEFAULT_FOREIGN_KEYS=1',
'SQLITE_DEFAULT_WAL_SYNCHRONOUS=1',
'SQLITE_DQS=0',
'SQLITE_ENABLE_MATH_FUNCTIONS',
'SQLITE_ENABLE_DESERIALIZE',
'SQLITE_ENABLE_COLUMN_METADATA',
'SQLITE_ENABLE_UPDATE_DELETE_LIMIT',
'SQLITE_ENABLE_STAT4',
'SQLITE_ENABLE_FTS5',
'SQLITE_ENABLE_JSON1',
'SQLITE_ENABLE_RTREE',
'SQLITE_INTROSPECTION_PRAGMAS',
'SQLCIPHER_CRYPTO_CUSTOM=signal_crypto_provider_setup',
'HAVE_STDINT_H=1',
'HAVE_INT8_T=1',
'HAVE_INT16_T=1',
'HAVE_INT32_T=1',
'HAVE_UINT8_T=1',
'HAVE_INT8_T=1',
'HAVE_STDINT_H=1',
'HAVE_UINT16_T=1',
'HAVE_UINT32_T=1',
# SQLCipher-specific options
'SQLITE_HAS_CODEC',
'SQLITE_TEMP_STORE=2',

66
deps/download.js vendored Normal file
View File

@ -0,0 +1,66 @@
const https = require('https');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { Transform } = require('stream');
const { pipeline } = require('stream/promises');
const BASE_URI = `https://build-artifacts.signal.org/desktop`;
const HASH = '6253f886c40e49bf892d5cdc92b2eb200b12cd8d80c48ce5b05967cfd01ee8c7';
const SQLCIPHER_VERSION = '4.6.1-signal-patch2';
const EXTENSION_VERSION = '0.2.1-asm2';
const TAG = [SQLCIPHER_VERSION, EXTENSION_VERSION].join('--');
const URL = `${BASE_URI}/sqlcipher-v2-${TAG}-${HASH}.tar.gz`;
const buildFile = process.argv[2];
const targetFile = path.join(__dirname, 'sqlcipher.tar.gz');
const tmpFile = `${targetFile}.tmp`;
async function main() {
if (fs.statSync(targetFile, { throwIfNoEntry: false })) {
const hash = crypto.createHash('sha256');
const existingHash = await pipeline(
fs.createReadStream(targetFile),
hash,
);
if (hash.digest('hex') === HASH) {
console.log('local build artifact is up-to-date');
fs.copyFileSync(targetFile, buildFile);
return;
}
console.log('local build artifact is outdated');
} else {
console.log('local build artifact is absent');
}
download();
}
function download() {
console.log(`downloading ${URL}`);
https.get(URL, async (res) => {
const out = fs.createWriteStream(tmpFile);
const hash = crypto.createHash('sha256');
const t = new Transform({
transform(chunk, encoding, callback) {
hash.write(chunk, encoding);
callback(null, chunk);
}
});
await pipeline(res, t, out);
const actualDigest = hash.digest('hex');
if (actualDigest !== HASH) {
fs.unlinkSync(tmpFile);
throw new Error(`Digest mismatch. Expected ${HASH} got ${actualDigest}`);
}
fs.renameSync(tmpFile, targetFile);
fs.copyFileSync(targetFile, buildFile);
});
}
main();

98
deps/download.sh vendored
View File

@ -1,98 +0,0 @@
#!/usr/bin/env bash
# ===
# This script defines and generates the bundled SQLite3 unit (sqlite3.c).
#
# The following steps are taken:
# 1. populate the shell environment with the defined compile-time options.
# 2. download and extract the SQLite3 source code into a temporary directory.
# 3. run "sh configure" and "make sqlite3.c" within the source directory.
# 4. bundle the generated amalgamation into a tar.gz file (sqlite3.tar.gz).
# 5. export the defined compile-time options to a gyp file (defines.gypi).
# 6. update the docs (../docs/compilation.md) with details of this distribution.
#
# When a user builds better-sqlite3, the following steps are taken:
# 1. node-gyp loads the previously exported compile-time options (defines.gypi).
# 2. the extract.js script unpacks the bundled amalgamation (sqlite3.tar.gz).
# 3. node-gyp compiles the extracted sqlite3.c along with better_sqlite3.cpp.
# 3. node-gyp links the two resulting binaries to generate better_sqlite3.node.
# ===
YEAR="2021"
VERSION="3350200"
DEFINES="
SQLITE_DQS=0
SQLITE_LIKE_DOESNT_MATCH_BLOBS
SQLITE_THREADSAFE=2
SQLITE_USE_URI=0
SQLITE_DEFAULT_MEMSTATUS=0
SQLITE_OMIT_DEPRECATED
SQLITE_OMIT_GET_TABLE
SQLITE_OMIT_TCL_VARIABLE
SQLITE_OMIT_PROGRESS_CALLBACK
SQLITE_OMIT_SHARED_CACHE
SQLITE_TRACE_SIZE_LIMIT=32
SQLITE_DEFAULT_CACHE_SIZE=-16000
SQLITE_DEFAULT_FOREIGN_KEYS=1
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1
SQLITE_ENABLE_COLUMN_METADATA
SQLITE_ENABLE_UPDATE_DELETE_LIMIT
SQLITE_ENABLE_STAT4
SQLITE_ENABLE_FTS3_PARENTHESIS
SQLITE_ENABLE_FTS3
SQLITE_ENABLE_FTS4
SQLITE_ENABLE_FTS5
SQLITE_ENABLE_JSON1
SQLITE_ENABLE_RTREE
SQLITE_ENABLE_GEOPOLY
SQLITE_INTROSPECTION_PRAGMAS
SQLITE_SOUNDEX
"
# ========== START SCRIPT ========== #
echo "setting up environment..."
DEPS="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
TEMP="$DEPS/temp"
rm -rf "$TEMP"
mkdir -p "$TEMP"
export CFLAGS=`echo $(echo "$DEFINES" | sed -e "/^\s*$/d" -e "s/^/-D/")`
echo "downloading source..."
curl -#f "https://www.sqlite.org/$YEAR/sqlite-src-$VERSION.zip" > "$TEMP/source.zip" || exit 1
echo "extracting source..."
unzip "$TEMP/source.zip" -d "$TEMP" > /dev/null || exit 1
cd "$TEMP/sqlite-src-$VERSION" || exit 1
echo "configuring amalgamation..."
sh configure > /dev/null || exit 1
echo "building amalgamation..."
make sqlite3.c > /dev/null || exit 1
echo "generating tarball..."
tar czf "$DEPS/sqlite3.tar.gz" sqlite3.c sqlite3.h sqlite3ext.h || exit 1
echo "updating gyp configs..."
GYP="$DEPS/defines.gypi"
printf "# THIS FILE IS AUTOMATICALLY GENERATED (DO NOT EDIT)\n\n{\n 'defines': [\n" > "$GYP"
printf "$DEFINES" | sed -e "/^\s*$/d" -e "s/\(.*\)/ '\1',/" >> "$GYP"
printf " ],\n}\n" >> "$GYP"
echo "updating docs..."
DOCS="$DEPS/../docs/compilation.md"
MAJOR=`expr "${VERSION:0:1}" + 0`
MINOR=`expr "${VERSION:1:2}" + 0`
PATCH=`expr "${VERSION:3:2}" + 0`
sed -Ei "" -e "s/version [0-9]+\.[0-9]+\.[0-9]+/version $MAJOR.$MINOR.$PATCH/g" "$DOCS"
sed -i "" -e "/^SQLITE_/,\$d" "$DOCS"
printf "$DEFINES" | sed -e "/^\s*$/d" >> "$DOCS"
printf "\`\`\`\n" >> "$DOCS"
echo "cleaning up..."
cd - > /dev/null || exit 1
rm -rf "$TEMP"
echo "done!"

6
deps/extract.js vendored
View File

@ -2,13 +2,13 @@
const path = require('path');
const tar = require('tar');
const dest = process.argv[2];
const source = path.join(__dirname, 'sqlite3.tar.gz');
const source = process.argv[2];
const dest = process.argv[3];
process.on('unhandledRejection', (err) => { throw err; });
/*
This extracts the bundled sqlite3.tar.gz file and places the resulting files
This extracts the bundled sqlcipher.tar.gz file and places the resulting files
into the directory specified by <$2>.
*/

49
deps/sqlite3.gyp vendored
View File

@ -1,7 +1,7 @@
# ===
# This configuration defines options specific to compiling SQLite3 itself.
# Compile-time options are loaded by the auto-generated file "defines.gypi".
# Before SQLite3 is compiled, it gets extracted from "sqlite3.tar.gz".
# Before SQLite3 is compiled, it gets extracted from "sqlcipher.tar.gz".
# The --sqlite3 option can be provided to use a custom amalgamation instead.
# ===
@ -9,12 +9,25 @@
'includes': ['common.gypi'],
'targets': [
{
'target_name': 'locate_sqlite3',
'target_name': 'download_sqlite3',
'type': 'none',
'hard_dependency': 1,
'actions': [{
'action_name': 'download_sqlite3',
'inputs': ['download.js'],
'outputs': [
'<(SHARED_INTERMEDIATE_DIR)/sqlcipher.tar.gz'
],
'action': ['node', 'download.js', '<(SHARED_INTERMEDIATE_DIR)/sqlcipher.tar.gz'],
}],
},
{
'target_name': 'locate_sqlite3',
'type': 'none',
'dependencies': ['download_sqlite3'],
'actions': [{
'action_name': 'extract_sqlite3',
'inputs': ['sqlite3.tar.gz'],
'inputs': ['<(SHARED_INTERMEDIATE_DIR)/sqlcipher.tar.gz'],
'outputs': [
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.c',
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.h',
@ -23,13 +36,11 @@
'conditions': [
['OS == "win"', {
'outputs': [
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/libssl.lib',
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/libcrypto.lib',
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/ossl_static.pdb',
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-pc-windows-msvc/signal_sqlcipher_extension.lib',
],
}],
],
'action': ['node', 'extract.js', '<(SHARED_INTERMEDIATE_DIR)/sqlite3'],
'action': ['node', 'extract.js', '<(SHARED_INTERMEDIATE_DIR)/sqlcipher.tar.gz', '<(SHARED_INTERMEDIATE_DIR)/sqlite3'],
}],
},
{
@ -40,9 +51,7 @@
['OS == "win"', {
'copies': [{
'files': [
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/libssl.lib',
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/libcrypto.lib',
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)/ossl_static.pdb',
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-pc-windows-msvc/signal_sqlcipher_extension.lib',
],
'destination': '<(PRODUCT_DIR)',
}],
@ -56,12 +65,11 @@
'sources': ['<(SHARED_INTERMEDIATE_DIR)/sqlite3/sqlite3.c'],
'include_dirs': [
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/',
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/openssl-include',
],
'direct_dependent_settings': {
'include_dirs': [
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/',
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/openssl-include',
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/include',
],
},
'cflags': ['-std=c99', '-w'],
@ -77,29 +85,28 @@
],
'link_settings': {
'libraries': [
'-llibcrypto.lib',
'-llibssl.lib',
'-lws2_32.lib',
'-lcrypt32.lib'
'-luserenv.lib',
'-lntdll.lib',
'-lbcrypt.lib',
'-lcrypt32.lib',
'-lsignal_sqlcipher_extension.lib'
],
'library_dirs': [
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/>(openssl_root)'
'<(PRODUCT_DIR)',
]
}
},
'OS == "mac"', {
'link_settings': {
'libraries': [
# This statically links libcrypto, whereas -lcrypto would dynamically link it
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/OpenSSL-macOS/libcrypto.a'
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-apple-darwin/libsignal_sqlcipher_extension.a',
]
}
},
{ # Linux
'link_settings': {
'libraries': [
# This statically links libcrypto, whereas -lcrypto would dynamically link it
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/OpenSSL-Linux/libcrypto.a'
'<(SHARED_INTERMEDIATE_DIR)/sqlite3/signal-sqlcipher-extension/>(rust_arch)-unknown-linux-gnu/libsignal_sqlcipher_extension.a',
]
}
}],

BIN
deps/sqlite3.tar.gz (Stored with Git LFS) vendored

Binary file not shown.

16
deps/symlink.js vendored
View File

@ -1,16 +0,0 @@
'use strict';
const path = require('path');
const fs = require('fs');
const dest = process.argv[2];
const source = path.resolve(path.sep, process.argv[3]);
/*
This creates symlinks inside the <$2> directory, linking to the SQLite3
amalgamation files inside the directory specified by the absolute path <$3>.
*/
for (const filename of ['sqlite3.c', 'sqlite3.h']) {
fs.accessSync(path.join(source, filename));
fs.symlinkSync(path.join(source, filename), path.join(dest, filename), 'file');
}

View File

@ -2,6 +2,8 @@
- [class `Database`](#class-database)
- [class `Statement`](#class-statement)
- [class `SqliteError`](#class-sqliteerror)
- [Binding Parameters](#binding-parameters)
# class *Database*
@ -10,8 +12,10 @@
- [Database#transaction()](#transactionfunction---function)
- [Database#pragma()](#pragmastring-options---results)
- [Database#backup()](#backupdestination-options---promise)
- [Database#serialize()](#serializeoptions---buffer)
- [Database#function()](#functionname-options-function---this)
- [Database#aggregate()](#aggregatename-options---this)
- [Database#table()](#tablename-definition---this)
- [Database#loadExtension()](#loadextensionpath-entrypoint---this)
- [Database#exec()](#execstring---this)
- [Database#close()](#close---this)
@ -19,18 +23,22 @@
### new Database(*path*, [*options*])
Creates a new database connection. If the database file does not exist, it is created. This happens synchronously, which means you can start executing queries right away. You can create an [in-memory database](https://www.sqlite.org/inmemorydb.html) by passing `":memory:"` as the first argument.
Creates a new database connection. If the database file does not exist, it is created. This happens synchronously, which means you can start executing queries right away. You can create an [in-memory database](https://www.sqlite.org/inmemorydb.html) by passing `":memory:"` as the first argument. You can create a temporary database by passing an empty string (or by omitting all arguments).
> In-memory databases can also be created by passing a buffer returned by [`.serialize()`](#serializeoptions---buffer), instead of passing a string as the first argument.
Various options are accepted:
- `options.readonly`: open the database connection in readonly mode (default: `false`).
- `options.fileMustExist`: if the database does not exist, an `Error` will be thrown instead of creating a new file. This option does not affect in-memory or readonly database connections (default: `false`).
- `options.fileMustExist`: if the database does not exist, an `Error` will be thrown instead of creating a new file. This option is ignored for in-memory, temporary, or readonly database connections (default: `false`).
- `options.timeout`: the number of milliseconds to wait when executing queries on a locked database, before throwing a `SQLITE_BUSY` error (default: `5000`).
- `options.verbose`: provide a function that gets called with every SQL string executed by the database connection (default: `null`).
- `options.nativeBinding`: if you're using a complicated build system that moves, transforms, or concatenates your JS files, `better-sqlite3` might have trouble locating its native C++ addon (`better_sqlite3.node`). If you get an error that looks like [this](https://github.com/JoshuaWise/better-sqlite3/issues/534#issuecomment-757907190), you can solve it by using this option to provide the file path of `better_sqlite3.node` (relative to the current working directory).
```js
const Database = require('better-sqlite3');
const db = new Database('foobar.db', { verbose: console.log });
@ -90,7 +98,7 @@ If you'd like to manage transactions manually, you're free to do so with regular
Transaction functions do not work with async functions. Technically speaking, async functions always return after the first `await`, which means the transaction will already be committed before any async code executes. Also, because SQLite3 serializes all transactions, it's generally a very bad idea to keep a transaction open across event loop ticks anyways.
It's important to know that SQLite3 may sometimes rollback a transaction without us asking it to. This can happen either because of an [`ON CONFLICT`](https://sqlite.org/lang_conflict.html) clause, the [`RAISE()`](https://www.sqlite.org/lang_createtrigger.html) trigger function, or certain errors such as `SQLITE_FULL` or `SQLITE_BUSY`. In other words, if you catch an SQLite3 error *within* a transaction, you must be aware that any futher SQL that you execute might not be within the same transaction. Usually, the best course of action for such cases is to simply re-throw the error, exiting the transaction function.
It's important to know that SQLite3 may sometimes rollback a transaction without us asking it to. This can happen either because of an [`ON CONFLICT`](https://sqlite.org/lang_conflict.html) clause, the [`RAISE()`](https://www.sqlite.org/lang_createtrigger.html) trigger function, or certain errors such as `SQLITE_FULL` or `SQLITE_BUSY`. In other words, if you catch an SQLite3 error *within* a transaction, you must be aware that any further SQL that you execute might not be within the same transaction. Usually, the best course of action for such cases is to simply re-throw the error, exiting the transaction function.
```js
try {
@ -118,7 +126,7 @@ It's better to use this method instead of normal [prepared statements](#prepares
### .backup(*destination*, [*options*]) -> *promise*
Initiates a [backup](https://www.sqlite.org/backup.html) of the database, returning a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) for when the backup is complete. If the backup fails, the promise will be rejected with an `Error`. You can optionally backup an attached database by setting the `attached` option to the name of the desired attached database.
Initiates a [backup](https://www.sqlite.org/backup.html) of the database, returning a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) for when the backup is complete. If the backup fails, the promise will be rejected with an `Error`. You can optionally backup an attached database instead by setting the `attached` option to the name of the desired attached database. A backup file is just a regular SQLite3 database file. It can be opened by [`new Database()`](#new-databasepath-options) just like any SQLite3 database.
```js
db.backup(`backup-${Date.now()}.db`)
@ -151,6 +159,18 @@ db.backup(`backup-${Date.now()}.db`, {
});
```
### .serialize([*options*]) -> *Buffer*
Returns a [buffer](https://nodejs.org/api/buffer.html#buffer_class_buffer) containing the serialized contents of the database. You can optionally serialize an attached database instead by setting the `attached` option to the name of the desired attached database.
The returned buffer can be written to disk to create a regular SQLite3 database file, or it can be opened directly as an in-memory database by passing it to [`new Database()`](#new-databasepath-options).
```js
const buffer = db.serialize();
db.close();
db = new Database(buffer);
```
### .function(*name*, [*options*], *function*) -> *this*
Registers a user-defined `function` so that it can be used by SQL statements.
@ -167,6 +187,8 @@ By default, user-defined functions have a strict number of arguments (determined
If `options.varargs` is `true`, the registered function can accept any number of arguments.
If `options.directOnly` is `true`, the registered function can only be invoked from top-level SQL, and cannot be used in [VIEWs](https://sqlite.org/lang_createview.html), [TRIGGERs](https://sqlite.org/lang_createtrigger.html), or schema structures such as [CHECK constraints](https://www.sqlite.org/lang_createtable.html#ckconst), [DEFAULT clauses](https://www.sqlite.org/lang_createtable.html#dfltval), etc.
If your function is [deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm), you can set `options.deterministic` to `true`, which may improve performance under some circumstances.
```js
@ -209,7 +231,7 @@ db.prepare('SELECT getAverage(dollars) FROM expenses').pluck().get(); // => 20.2
As shown above, you can use arbitrary JavaScript objects as your aggregation context, as long as a valid SQLite3 value is returned by `result()` in the end. If `step()` doesn't return anything (`undefined`), the aggregate value will not be replaced (be careful of this when using functions that return `undefined` when `null` is desired).
Just like regular [user-defined functions](#functionname-options-function---this), user-defined aggregates can accept multiple arguments. Furthermore, `options.varargs` and `options.deterministic` [are also](#functionname-options-function---this) accepted.
Just like regular [user-defined functions](#functionname-options-function---this), user-defined aggregates can accept multiple arguments. Furthermore, `options.varargs`, `options.directOnly`, and `options.deterministic` [are also](#functionname-options-function---this) accepted.
If you provide an `inverse()` function, the aggregate can be used as a [window function](https://www.sqlite.org/windowfunctions.html). Where `step()` is used to add a row to the current window, `inverse()` is used to remove a row from the current window. When using window functions, `result()` may be invoked multiple times.
@ -229,6 +251,117 @@ db.prepare(`
`).all();
```
### .table(*name*, *definition*) -> *this*
Registers a [virtual table](https://www.sqlite.org/vtab.html). Virtual tables can be queried just like real tables, except their results do not exist in the database file; instead, they are calculated on-the-fly by a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) in JavaScript.
```js
const fs = require('fs');
db.table('filesystem_directory', {
columns: ['filename', 'data'],
rows: function* () {
for (const filename of fs.readdirSync(process.cwd())) {
const data = fs.readFileSync(filename);
yield { filename, data };
}
},
});
const files = db.prepare('SELECT * FROM filesystem_directory').all();
// => [{ filename, data }, { filename, data }]
```
To generate a row in a virtual table, you can either yield an object whose keys correspond to column names, or yield an array whose elements represent columns in the order that they were declared. Every virtual table **must** declare its columns via the `columns` option.
Virtual tables can be used like [table-valued functions](https://www.sqlite.org/vtab.html#tabfunc2); you can pass parameters to them, unlike regular tables.
```js
db.table('regex_matches', {
columns: ['match', 'capture'],
rows: function* (pattern, text) {
const regex = new RegExp(pattern, 'g');
let match;
while (match = regex.exec(text)) {
yield [match[0], match[1]];
}
},
});
const stmt = db.prepare("SELECT * FROM regex('\\$(\\d+)', ?)");
stmt.all('Desks cost $500 and chairs cost $27');
// => [{ match: '$500', capture: '500' }, { match: '$27', capture: '27' }]
```
By default, the number of parameters accepted by a virtual table is inferred by `function.length`, and the parameters are automatically named `$1`, `$2`, etc. However, you can optionally provide an explicit list of parameters via the `parameters` option.
```js
db.table('regex_matches', {
columns: ['match', 'capture'],
parameters: ['pattern', 'text'],
rows: function* (pattern, text) {
...
},
});
```
> In virtual tables, parameters are actually [*hidden columns*](https://www.sqlite.org/vtab.html#hidden_columns_in_virtual_tables), and they can be selected in the result set of a query, just like any other column. That's why it may sometimes be desirable to give them explicit names.
When querying a virtual table, any omitted parameters will be `undefined`. You can use this behavior to implement required parameters and default parameter values.
```js
db.table('sequence', {
columns: ['value'],
parameters: ['length', 'start'],
rows: function* (length, start = 0) {
if (length === undefined) {
throw new TypeError('missing required parameter "length"');
}
const end = start + length;
for (let n = start; n < end; ++n) {
yield { value: n };
}
},
});
db.prepare('SELECT * FROM sequence(10)').pluck().all();
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```
> Note that when using syntax like `start = 0` for default parameter values (shown above), the function's `.length` property does not include the optional parameter, so you need to explicitly declare `parameters` in this case.
Normally, when you register a virtual table, the virtual table *automatically exists* without needing to run a `CREATE VIRTUAL TABLE` statement. However, if you provide a factory function as the second argument (a function that *returns* virtual table definitions), then no virtual table will be created automatically. Instead, you can create multiple similar virtual tables by running [`CREATE VIRTUAL TABLE`](https://sqlite.org/lang_createvtab.html) statements, each with their own module arguments. Think of it like defining a virtual table "class" that can be instantiated by running `CREATE VIRTUAL TABLE` statements.
```js
const fs = require('fs');
db.table('csv', (filename) => {
const firstLine = getFirstLineOfFile(filename);
return {
columns: firstLine.split(','),
rows: function* () {
// This is just an example. Real CSV files are more complicated to parse.
const contents = fs.readFileSync(filename, 'utf8');
for (const line of contents.split('\n')) {
yield line.split(',');
}
},
};
});
db.exec('CREATE VIRTUAL TABLE my_data USING csv(my_data.csv)');
const allData = db.prepare('SELECT * FROM my_data').all();
```
The factory function will be invoked each time a corresponding `CREATE VIRTUAL TABLE` statement runs. The arguments to the factory function correspond to the module arguments passed in the `CREATE VIRTUAL TABLE` statement; always a list of arbitrary strings separated by commas. It's your responsibility to parse and interpret those module arguments. Note that SQLite3 does not allow [bound parameters](#binding-parameters) inside module arguments.
Just like [user-defined functions](#functionname-options-function---this) and [user-defined aggregates](#aggregatename-options---this), virtual tables support `options.directOnly`, which prevents the table from being used inside [VIEWs](https://sqlite.org/lang_createview.html), [TRIGGERs](https://sqlite.org/lang_createtrigger.html), or schema structures such as [CHECK constraints](https://www.sqlite.org/lang_createtable.html#ckconst), [DEFAULT clauses](https://www.sqlite.org/lang_createtable.html#dfltval), etc.
> Some [extensions](#loadextensionpath-entrypoint---this) can provide virtual tables that have write capabilities, but `db.table()` is only capable of creating read-only virtual tables, primarily for the purpose of supporting table-valued functions.
### .loadExtension(*path*, [*entryPoint*]) -> *this*
Loads a compiled [SQLite3 extension](https://sqlite.org/loadext.html) and applies it to the current database connection.
@ -267,7 +400,7 @@ process.on('SIGTERM', () => process.exit(128 + 15));
**.name -> _string_** - The string that was used to open the database connection.
**.memory -> _boolean_** - Whether the database is an in-memory database.
**.memory -> _boolean_** - Whether the database is an in-memory or temporary database.
**.readonly -> _boolean_** - Whether the database connection was created in readonly mode.
@ -288,8 +421,6 @@ An object representing a single SQL statement.
### .run([*...bindParameters*]) -> *object*
**(only on statements that do not return data)*
Executes the prepared statement. When execution completes it returns an `info` object describing any changes made. The `info` object has two properties:
- `info.changes`: the total number of rows that were inserted, updated, or deleted by this operation. Changes made by [foreign key actions](https://www.sqlite.org/foreignkeys.html#fk_actions) or [trigger programs](https://www.sqlite.org/lang_createtrigger.html) do not count.
@ -467,6 +598,18 @@ console.log(cat.name); // => "Joey"
**.reader -> _boolean_** - Whether the prepared statement returns data.
**.readonly -> _boolean_** - Whether the prepared statement is readonly, meaning it does not mutate the database (note that [SQL functions might still change the database indirectly](https://www.sqlite.org/c3ref/stmt_readonly.html) as a side effect, even if the `.readonly` property is `true`).
**.busy -> _boolean_** - Whether the prepared statement is busy executing a query via the [`.iterate()`](#iteratebindparameters---iterator) method.
# class *SqliteError*
Whenever an error occurs within SQLite3, a `SqliteError` object will be thrown. `SqliteError` is a subclass of `Error`. Every `SqliteError` object has a `code` property, which is a string matching one of error codes defined [here](https://sqlite.org/rescode.html) (for example, `"SQLITE_CONSTRAINT"`).
If you receive a `SqliteError`, it probably means you're using SQLite3 incorrectly. The error didn't originate in `better-sqlite3`, so it's probably not an issue with `better-sqlite3`. It's recommended that you learn about the meaning of the error [here](https://sqlite.org/rescode.html), and perhaps learn more about how to use SQLite3 by reading [their docs](https://sqlite.org/docs.html).
> In the unlikely scenario that SQLite3 throws an error that is not recognized by `better-sqlite3` (this would be considered a bug in `better-sqlite3`), the `code` property will be `"UNKNOWN_SQLITE_ERROR_NNNN"`, where `NNNN` is the numeric error code. If this happens to you, please report it as an [issue](https://github.com/JoshuaWise/better-sqlite3/issues).
# Binding Parameters
This section refers to anywhere in the documentation that specifies the optional argument [*`...bindParameters`*].
@ -504,3 +647,13 @@ Below is an example of mixing anonymous parameters with named parameters.
const stmt = db.prepare('INSERT INTO people VALUES (@name, @name, ?)');
stmt.run(45, { name: 'Henry' });
```
Here is how `better-sqlite3` converts values between SQLite3 and JavaScript:
|SQLite3|JavaScript|
|---|---|
|`NULL`|`null`|
|`REAL`|`number`|
|`INTEGER`|`number` [or `BigInt`](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/integer.md#the-bigint-primitive-type)|
|`TEXT`|`string`|
|`BLOB`|[`Buffer`](https://nodejs.org/api/buffer.html#buffer_class_buffer)|

View File

@ -16,7 +16,15 @@ However, if you simply run `npm install` while `better-sqlite3` is listed as a d
}
```
Your amalgamation directory must contain `sqlite3.c` and `sqlite3.h`. Any desired [compile time options](https://www.sqlite.org/compile.html) must be defined directly within `sqlite3.c`.
Your amalgamation directory must contain `sqlite3.c` and `sqlite3.h`. Any desired [compile time options](https://www.sqlite.org/compile.html) must be defined directly within `sqlite3.c`, as shown below.
```c
// These go at the top of the file
#define SQLITE_ENABLE_FTS5 1
#define SQLITE_DEFAULT_CACHE_SIZE 16000
// ... the original content of the file remains below
```
### Step by step example
@ -34,33 +42,42 @@ If you're using a SQLite3 encryption extension that is a drop-in replacement for
# Bundled configuration
By default, this distribution currently uses SQLite3 **version 3.35.2** with the following [compilation options](https://www.sqlite.org/compile.html):
By default, this distribution currently uses SQLite3 **version 3.40.0** with the following [compilation options](https://www.sqlite.org/compile.html):
```
SQLITE_DQS=0
SQLITE_LIKE_DOESNT_MATCH_BLOBS
SQLITE_THREADSAFE=2
SQLITE_USE_URI=0
SQLITE_DEFAULT_MEMSTATUS=0
SQLITE_OMIT_DEPRECATED
SQLITE_OMIT_GET_TABLE
SQLITE_OMIT_TCL_VARIABLE
SQLITE_OMIT_PROGRESS_CALLBACK
SQLITE_OMIT_SHARED_CACHE
SQLITE_TRACE_SIZE_LIMIT=32
HAVE_INT16_T=1
HAVE_INT32_T=1
HAVE_INT8_T=1
HAVE_STDINT_H=1
HAVE_UINT16_T=1
HAVE_UINT32_T=1
HAVE_UINT8_T=1
SQLITE_DEFAULT_CACHE_SIZE=-16000
SQLITE_DEFAULT_FOREIGN_KEYS=1
SQLITE_DEFAULT_MEMSTATUS=0
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1
SQLITE_DQS=0
SQLITE_ENABLE_COLUMN_METADATA
SQLITE_ENABLE_UPDATE_DELETE_LIMIT
SQLITE_ENABLE_STAT4
SQLITE_ENABLE_FTS3_PARENTHESIS
SQLITE_ENABLE_DESERIALIZE
SQLITE_ENABLE_FTS3
SQLITE_ENABLE_FTS3_PARENTHESIS
SQLITE_ENABLE_FTS4
SQLITE_ENABLE_FTS5
SQLITE_ENABLE_JSON1
SQLITE_ENABLE_RTREE
SQLITE_ENABLE_GEOPOLY
SQLITE_ENABLE_JSON1
SQLITE_ENABLE_MATH_FUNCTIONS
SQLITE_ENABLE_RTREE
SQLITE_ENABLE_STAT4
SQLITE_ENABLE_UPDATE_DELETE_LIMIT
SQLITE_INTROSPECTION_PRAGMAS
SQLITE_LIKE_DOESNT_MATCH_BLOBS
SQLITE_OMIT_DEPRECATED
SQLITE_OMIT_GET_TABLE
SQLITE_OMIT_PROGRESS_CALLBACK
SQLITE_OMIT_SHARED_CACHE
SQLITE_OMIT_TCL_VARIABLE
SQLITE_SOUNDEX
SQLITE_THREADSAFE=2
SQLITE_TRACE_SIZE_LIMIT=32
SQLITE_USE_URI=0
```

18
docs/conduct.md Normal file
View File

@ -0,0 +1,18 @@
# Code of conduct
Topics of discussion are expected to be constrained such that all discussion is relevant to the following goals:
- Maintaining `better-sqlite3`'s code, documentation, and build artifacts
- Helping people *get started* in using `better-sqlite3` within their software projects
Other areas of discussion are considered to be off-topic, including but not limited to:
- Politics
- Name-calling, insults
- Help with using SQLite (there's already [very good documentation](https://sqlite.org/docs.html) for that)
- Help with application architecture, and other high-level decisions about software projects
- Attention to personal traits such as race, gender, religion, national origin, sexual orientation, disability, etc.
Repeated offenses against this code of conduct may result in being temporarily banned from the community. Unofficially, the community is expected to maintain a manner of professionalism and to treat others with respect.
Attempting to physically seize, sabotage, or distribute malware through `better-sqlite3` will result in being permanently banned from the community, without warning.

147
docs/contribution.md Normal file
View File

@ -0,0 +1,147 @@
# Contribution
## Introduction and scope
`better-sqlite3` is a low-level Node.js package that provides bindings to [SQLite](https://sqlite.org/index.html). `better-sqlite3` is not an ORM, and does not lend itself to specific types of applications or frameworks.
Anything that SQLite does not directly provide is considered out-of-scope for `better-sqlite3`. Anything that SQLite *does* directly provide *may* be considered in-scope for `better-sqlite3`, with the additional requirements that it:
- can be implemented sensibly and safely (i.e., it cannot lead to [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior))
- is used commonly enough to warrent the extra code complexity that it brings
- cannot be reasonably implemented by a user in JavaScript (e.g., by monkey-patching)
#### Native addons
`better-sqlite3` is a combination of JavaScript and C++. The C++ part is necessary in order to communicate with the [underlying SQLite library](https://sqlite.org/index.html), which is written in C. Node.js supports [C++ addons](https://nodejs.org/api/addons.html) through a build system called [`node-gyp`](https://github.com/nodejs/node-gyp), which is automatically bundled with every installation of [npm](https://docs.npmjs.com/about-npm). On most systems, C++ addons will simply be compiled as part of the installation process when running `npm install`. However, [history has shown](https://github.com/nodejs/node-gyp/issues/629) that Windows users have struggled significantly when trying to build C++ addons for Node.js. This is an issue with Node.js as a whole, and not specific to `better-sqlite3`.
#### Electron
`better-sqlite3` is a Node.js package, *not* an [Electron](https://www.electronjs.org/) package. Electron is considered a third-party platform that is not officially supported. However, many users do find great success in using `better-sqlite3` with Electron, and helpful contributors such as [@mceachen](https://github.com/mceachen) have provided support to the Electron community.
#### TypeScript
Lastly, `better-sqlite3` is a JavaScript package, not a TypeScript package. Type definitions have been generously provided by the community at [`@types/better-sqlite3`](https://www.npmjs.com/package/@types/better-sqlite3), but no official support for TypeScript is currently provided (this may change in the future).
## Principles
Code that gets contributed to `better-sqlite3` must adhere to the following principles, prioritized from first to last:
#### 1) Correctness
The code must behave as expected in all siutations. Often when writing new features, only the nominal case is considered. However, many edge cases exist when you consider race conditions, uncommon states, and improper usage. All possibilities of improper usage must be detected, and an appropriate error must be thrown (never ignored). All possibilities of proper usage must be supported, and must behave as expected.
#### 2) Simplicity
`better-sqlite3`'s public API must be as simple as possible. Rather than calling 3 functions in a specific order, it's simpler for users to call a single function. Rather than providing many similar functions for doing similar things (e.g., "convenience functions"), there should just be one function that is already convenient by design. Sane defaults should be applied when possible. A function's minimal call signature should be as small as possible, with progressively complex customization available when needed. Function names should only be as long as necessary to convey their purpose. For any new feature, it should be easy to showcase code examples that is are so simple that they are self-explanatory.
> This principle only applies to the public API, not necessarily to internal functions.
#### 3) Readability
Code must be written in a way that is intuitive and understandable by other programmers, now and in the future. Some code is naturally complex, and thus should be explained with comments (only when necesary). Code should be written in a style that is similar to existing code.
#### 4) Performance
Code should be written such that it does not use unnecessary computing resources. If a task can be accomplished without copying a potentially large buffer, it should be. If a complex algorithm can generally be avoided with a simple check, it should be. Calls to the operating system or filesystem should be limited to only occur when absolutely necessary. The public API should naturally encourage good performance habits, such as re-using prepared statements.
> It's okay to sacrifice readability for performance if doing so has a clear, measureable benefit to users.
## How to contribute
If you've never written a native addon for Node.js before, you should start by reading the [official documentation](https://nodejs.org/api/addons.html) on the subject.
#### C++
The C++ code in `better-sqlite3` is written using a tool called [`lzz`](https://github.com/WiseLibs/lzz), which alleviates the programmer from needing to write header files. If you plan on changing any C++ code, you'll need to edit `*.lzz` files and then re-compile them into `*.cpp` and `*.hpp` by running `npm run lzz` (while the `lzz` executable is in your PATH). You can learn how to download and install `lzz` [here](https://github.com/WiseLibs/lzz).
#### Style guide
There is currently no linter or style guide associated with `better-sqlite3` (this may change in the future). For now, just try to match the style of existing code as much as possible. Code owners will reject your PR or rewrite your changes if they feel that you've used a coding style that doesn't match the existing code. Although the rules aren't layed out formally, you are expected to adhere to them by using your eyeballs.
#### Testing
All tests are written in JavaScript, and they test `better-sqlite3`'s public API. All new features must be accompinied by a robust set of tests that scrutinize the new feature under all manner of circumstances and edge cases. It's not enough to simply test the "common case". If you write code that detects errors and throws exceptions, those error cases should be tested too, to ensure that all errors are being properly detected. If a new feature interacts with existing features, those interactions must be tested as well.
#### Documentation
All new features must be accompinied by [clear documentation](./api.md). All new methods and classes must be included in the [Table of Contents](./api.md#api), and must include code examples. Documentation must follow the existing formatting:
- Literal values use monospace code formatting
- Examples: `"my string"`, `true`, `false`, `null`, `undefined`, `123`
- Package names and code identifiers use monospace code formatting
- Examples: `better-sqlite3`, `db.myMethod()`, `options.readOnly`, `this`
- Primitive data types are lower-cased, while other data types are capatalized
- Examples: `string`, `number`, `Buffer`, `Database`
- References to other classes or methods must be linked and use monospace code formatting
- Examples: [`.get()`](./api.md#getbindparameters---row), [`new Database()`](./api.md#new-databasepath-options)
- Function signatures are written as: .funcName(*requiredArg*, [*optionalArg*]) -> *returnValue*
- Note that the arguments and return values are *italicized*
- Note that optional arguments are surrounded by square brackets []
- All code blocks should be highlighted using `js` syntax, except for bash commands which don't need highlighting
## Categories of contribution
Depending on the nature of your contribution, it will be held to a different level of scrutiny, from lowest to highest:
#### 1) General maintenance
These changes are self-explanatory. They include:
- Updating the bundled version of SQLite (using [this workflow](https://github.com/WiseLibs/better-sqlite3/actions/workflows/update-sqlite.yml))
- Updating dependencies in `package.json`
- Adding prebuild binaries for a new version of Node.js or Electron
- Adding prebuild binaries for a new architecture or operating system
These kinds of updates happen on a regular basis, and require zero knowledge of `better-sqlite3`'s code. Trusted contributors can merge these changes without approval from the original author.
#### 2) Documentation
Changes to documentation are usually helpful and harmless. However, they should be treated with a higher level of scrutiny because they affect how users learn about and use `better-sqlite3`. Importance is placed on the correctness and truthfuness of documentation. For example, documentation should not "go out of date" based on events outside of our control.
Depending on the type of documentation, trusted contributors might be able to merge these changes without approval from the original author.
#### 3) Minor quality-of-life improvements
These are code changes with a very small blast radius, such as adding a new read-only property to an object, or augmenting a function with a new option that gets passed directly to SQLite. These changes are *probably* harmless, but require additional scrutiny because they must be thoroughly tested and documented. These changes must be completely backwards-compatible, unless they're part of a major version update.
> It's considered a backwards-**incompatible** change for a prebuilt binary to be removed.
#### 4) New features
These are code changes with a substantial blast radius, such as implementing a new class or method. These changes must be completely backwards-compatible, unless they're part of a major version update.
New features are rarely accepted from external contributors because they are rarely held to the extremely high standard that `better-sqlite3` sets for itself. New features must behave correctly in all possible circumstances, including race conditions and edge cases. Likewise, even the most obscure circumstances must have test cases covering them.
When implementing a new feature, ask yourself:
- What could go wrong if I use this feature while executing a [user-defined function](./api.md#functionname-options-function---this)?
- What could go wrong if I use this feature while [iterating](./api.md#iteratebindparameters---iterator) through a prepared statement?
- What could go wrong if I use this feature while the database is [closed](./api.md#close---this)?
- What could go wrong if I use this feature from within the [verbose callback](./api.md#new-databasepath-options)?
- What could go wrong if I use this feature from within a [transaction](./api.md#transactionfunction---function)?
- What could go wrong if I use this feature on a prepared statement that has [bound parameters](./api.md#bindbindparameters---this)?
- What could go wrong if I use this feature within a [worker thread](./threads.md#worker-threads)?
- What could go wrong if I pass the wrong data type?
- What could go wrong if I pass an unexpected value, such as `null`, `undefined`, `""`, `NaN`, a negative/non-integer number, etc.?
- Should the user's [64-bit integer setting](integer.md#the-bigint-primitive-type) affect this feature?
- If this feature accepts a callback function:
- What could go wrong if that callback function throws an exception?
- What could go wrong if that callback function is triggered during one of the above scenarios?
- Could this feature cause memory leaks?
- What if a C++ object gets garbage-collected from JavaScript while it has open handles?
- What if a JavaScript error is thrown within a callback, after I allocated a C++ object?
People love `better-sqlite3` because of its robustness and reliability. Each and every feature of `better-sqlite3` accounts for every single scenario listed above. Additionally, all possible error scenarios are explicitly handled and tested. Any new feature of `better-sqlite3` must be held to the same standard. Currently, no new features are merged without approval from the original author.
## Creating a release
Trusted contributors have the privileges necessary to create a release. Here are the steps to create a release:
1. Run [this workflow](https://github.com/WiseLibs/better-sqlite3/actions/workflows/bump-version.yml) from the `master` branch to create a new version tag
- Select `patch` for bug fixes and general maintenance
- Select `minor` for larger releases with new features
- Select `major` for releases with backwards-incompatible changes
2. [Draft a new release](https://github.com/WiseLibs/better-sqlite3/releases/new), and select the version tag that you just created
3. Leave the "Release title" blank, and click "Auto-generate release notes"
4. Click "Publish release"
5. Wait for the `build` job to complete ([here](https://github.com/WiseLibs/better-sqlite3/actions))

View File

@ -52,4 +52,28 @@ db.prepare('SELECT isInt(?)').pluck().get(10); // => "false"
db.prepare('SELECT isInt(?)').pluck().get(10n); // => "true"
```
Likewise, [user-defined aggregates](./api.md#aggregatename-options---this) and [virtual tables](./api.md#tablename-definition---this) can also receive `BigInts` as arguments:
```js
db.aggregate('addInts', {
safeIntegers: true,
start: 0n,
step: (total, nextValue) => total + nextValue,
});
```
```js
db.table('sequence', {
safeIntegers: true,
columns: ['value'],
parameters: ['length', 'start'],
rows: function* (length, start = 0n) {
const end = start + length;
for (let n = start; n < end; ++n) {
yield { value: n };
}
},
});
```
It's worth noting that REAL (FLOAT) values returned from the database will always be represented as normal numbers.

View File

@ -2,22 +2,40 @@
If you have trouble installing `better-sqlite3`, follow this checklist:
1. Make sure you're using nodejs v10.20.1 or later
## Install a recent Node.js
2. Make sure you have [`node-gyp`](https://github.com/nodejs/node-gyp#installation) globally installed, including all of [its dependencies](https://github.com/nodejs/node-gyp#on-unix). On Windows you may need to [configure some things manually](https://github.com/nodejs/node-gyp#on-windows).
1. Make sure you're using Node.js v14.21.1 or later.
2. If you're on Windows, while installing, be sure to select "Automatically install the necessary tools" on the "Tools for Native Modules" page, and follow the remaining steps, including opening an admin PowerShell and installing visual studio and python. Everything _should_ just work.
3. If you're using [Electron](https://github.com/electron/electron), try running [`electron-rebuild`](https://www.npmjs.com/package/electron-rebuild)
## Install the `node-gyp` toolchain
4. If you're using Windows, follow these steps. Do them **in this order**, and **don't skip steps**.
1. Make sure you have [`node-gyp`](https://github.com/nodejs/node-gyp#installation) globally installed
1. Make sure all [`node-gyp` dependencies are installed](https://github.com/nodejs/node-gyp#on-unix). On Windows you may need to [configure some things manually](https://github.com/nodejs/node-gyp#on-windows). Use `npm ls node-gyp` to make sure none of your local packages installed an outdated version of `node-gyp` that is used over the global one.
1. Install the **latest** of node 10, 12, or 14.
2. Install **latest** Visual Studio Community and Desktop Development with C++ extension.
3. Install **latest** Python.
4. Run following commands:
```
npm config set msvs_version 2019
npm config set msbuild_path "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe"
```
5. Run `npm install`
## No special characters in your project path
1. Make sure there are no spaces in your project path: `node-gyp` may not escape spaces or special characters (like `%` or `$`) properly.
## Electron
1. If you're using [Electron](https://github.com/electron/electron), try running [`electron-rebuild`](https://www.npmjs.com/package/electron-rebuild)
## Windows
If you still have issues on Windows and are on an older version of Node, try these steps:
1. Install the **latest** of node 14, 16, or 18.
1. Install **latest** Visual Studio Community and Desktop Development with C++ extension.
1. Install **latest** Python.
1. Run following commands:
```
npm config set msvs_version 2019
npm config set msbuild_path "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe"
```
1. Delete your `node_modules` subdirectory
1. Delete your `$HOME/.node-gyp` directory
1. Run `npm install`
## If all else fails
If none of these solved your problem, try browsing [previous issues](https://github.com/JoshuaWise/better-sqlite3/issues?q=is%3Aissue) or open a [new issue](https://github.com/JoshuaWise/better-sqlite3/issues/new).

173
index.d.ts vendored Normal file
View File

@ -0,0 +1,173 @@
// Type definitions for better-sqlite3 7.6
// Project: https://github.com/JoshuaWise/better-sqlite3
// Definitions by: Ben Davies <https://github.com/Morfent>
// Mathew Rumsey <https://github.com/matrumz>
// Santiago Aguilar <https://github.com/sant123>
// Alessandro Vergani <https://github.com/loghorn>
// Andrew Kaiser <https://github.com/andykais>
// Mark Stewart <https://github.com/mrkstwrt>
// Florian Stamer <https://github.com/stamerf>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 3.8
/// <reference types="node" />
type VariableArgFunction = (...params: any[]) => any;
type ArgumentTypes<F extends VariableArgFunction> = F extends (...args: infer A) => any ? A : never;
declare namespace BetterSqlite3 {
interface Statement<BindParameters extends any[]> {
database: Database;
source: string;
reader: boolean;
readonly: boolean;
busy: boolean;
run(...params: BindParameters): Database.RunResult;
get(...params: BindParameters): any;
all(...params: BindParameters): any[];
iterate(...params: BindParameters): IterableIterator<any>;
pluck(toggleState?: boolean): this;
expand(toggleState?: boolean): this;
raw(toggleState?: boolean): this;
bind(...params: BindParameters): this;
columns(): ColumnDefinition[];
safeIntegers(toggleState?: boolean): this;
}
interface ColumnDefinition {
name: string;
column: string | null;
table: string | null;
database: string | null;
type: string | null;
}
interface FTS5Tokenizer {
// The resulting array consists of the following triples:
// [..., segment_start_idx, segment_end_idx, segment | null, ...]
//
// `segment` could be `null` or `undefined` if no normalization was
// performed or if the string is unchanged after the normalization.
run(value: string): ReadonlyArray<number | string | undefined | null>;
}
interface FTS5TokenizerConstructor {
new (params: ReadonlyArray<string>): FTS5Tokenizer;
}
interface Transaction<F extends VariableArgFunction> {
(...params: ArgumentTypes<F>): ReturnType<F>;
default(...params: ArgumentTypes<F>): ReturnType<F>;
deferred(...params: ArgumentTypes<F>): ReturnType<F>;
immediate(...params: ArgumentTypes<F>): ReturnType<F>;
exclusive(...params: ArgumentTypes<F>): ReturnType<F>;
}
interface VirtualTableOptions {
rows: () => Generator;
columns: string[];
parameters?: string[] | undefined;
safeIntegers?: boolean | undefined;
directOnly?: boolean | undefined;
}
interface Database {
memory: boolean;
readonly: boolean;
name: string;
open: boolean;
inTransaction: boolean;
prepare<BindParameters extends any[] | {} = any[]>(
source: string,
): BindParameters extends any[] ? Statement<BindParameters> : Statement<[BindParameters]>;
transaction<F extends VariableArgFunction>(fn: F): Transaction<F>;
exec(source: string): this;
pragma(source: string, options?: Database.PragmaOptions): any;
function(name: string, cb: (...params: any[]) => any): this;
function(name: string, options: Database.RegistrationOptions, cb: (...params: any[]) => any): this;
aggregate(name: string, options: Database.AggregateOptions): this;
close(): this;
defaultSafeIntegers(toggleState?: boolean): this;
backup(destinationFile: string, options?: Database.BackupOptions): Promise<Database.BackupMetadata>;
table(name: string, options: VirtualTableOptions): this;
unsafeMode(unsafe?: boolean): this;
createFTS5Tokenizer(name: string, tokenizer: FTS5TokenizerConstructor): void;
signalTokenize(value: string): Array<string>;
}
interface DatabaseConstructor {
new (filename: string, options?: Database.Options): Database;
(filename: string, options?: Database.Options): Database;
prototype: Database;
SqliteError: typeof SqliteError;
setLogHandler(fn: (code: number, value: string) => void): void;
}
}
declare class SqliteError extends Error {
name: string;
message: string;
code: string;
constructor(message: string, code: string);
}
declare namespace Database {
interface RunResult {
changes: number;
lastInsertRowid: number | bigint;
}
interface Options {
readonly?: boolean | undefined;
fileMustExist?: boolean | undefined;
timeout?: number | undefined;
verbose?: ((message?: any, ...additionalArgs: any[]) => void) | undefined;
nativeBinding?: string | undefined;
}
interface SerializeOptions {
attached?: string;
}
interface PragmaOptions {
simple?: boolean | undefined;
}
interface RegistrationOptions {
varargs?: boolean | undefined;
deterministic?: boolean | undefined;
safeIntegers?: boolean | undefined;
directOnly?: boolean | undefined;
}
interface AggregateOptions extends RegistrationOptions {
start?: any;
step: (total: any, next: any) => any;
inverse?: ((total: any, dropped: any) => any) | undefined;
result?: ((total: any) => any) | undefined;
}
interface BackupMetadata {
totalPages: number;
remainingPages: number;
}
interface BackupOptions {
progress: (info: BackupMetadata) => number;
}
type SqliteError = typeof SqliteError;
type Statement<BindParameters extends any[] | {} = any[]> = BindParameters extends any[]
? BetterSqlite3.Statement<BindParameters>
: BetterSqlite3.Statement<[BindParameters]>;
type ColumnDefinition = BetterSqlite3.ColumnDefinition;
type Transaction<T extends VariableArgFunction = VariableArgFunction> = BetterSqlite3.Transaction<T>;
type FTS5Tokenizer = BetterSqlite3.FTS5Tokenizer;
type Database = BetterSqlite3.Database;
}
declare const Database: BetterSqlite3.DatabaseConstructor;
export = Database;

View File

@ -2,15 +2,12 @@
const fs = require('fs');
const path = require('path');
const util = require('./util');
const SqliteError = require('./sqlite-error');
const {
Database: CPPDatabase,
setErrorConstructor,
setCorruptionLogger,
} = require('bindings')('better_sqlite3.node');
let DEFAULT_ADDON;
function Database(filenameGiven, options) {
if (new.target !== Database) {
if (new.target == null) {
return new Database(filenameGiven, options);
}
@ -31,12 +28,27 @@ function Database(filenameGiven, options) {
const fileMustExist = util.getBooleanOption(options, 'fileMustExist');
const timeout = 'timeout' in options ? options.timeout : 5000;
const verbose = 'verbose' in options ? options.verbose : null;
const nativeBindingPath = 'nativeBinding' in options ? options.nativeBinding : null;
// Validate interpreted options
if (readonly && anonymous) throw new TypeError('In-memory/temporary databases cannot be readonly');
if (!Number.isInteger(timeout) || timeout < 0) throw new TypeError('Expected the "timeout" option to be a positive integer');
if (timeout > 0x7fffffff) throw new RangeError('Option "timeout" cannot be greater than 2147483647');
if (verbose != null && typeof verbose !== 'function') throw new TypeError('Expected the "verbose" option to be a function');
if (nativeBindingPath != null && typeof nativeBindingPath !== 'string') throw new TypeError('Expected the "nativeBinding" option to be a string');
// Load the native addon
let addon;
if (nativeBindingPath == null) {
addon = DEFAULT_ADDON || (DEFAULT_ADDON = require('bindings')('better_sqlite3.node'));
} else {
addon = require(path.resolve(nativeBindingPath).replace(/(\.node)?$/, '.node'));
}
if (!addon.isInitialized) {
addon.setErrorConstructor(SqliteError);
addon.setLogHandler(logHandlerWrap);
addon.isInitialized = true;
}
// Make sure the specified directory exists
if (!anonymous && !fs.existsSync(path.dirname(filename))) {
@ -44,11 +56,20 @@ function Database(filenameGiven, options) {
}
Object.defineProperties(this, {
[util.cppdb]: { value: new CPPDatabase(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null) },
[util.cppdb]: { value: new addon.Database(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null) },
...wrappers.getters,
});
}
let logHandler;
function logHandlerWrap(code, warning) {
if (logHandler) {
logHandler(code, warning);
}
}
function noop() {}
const wrappers = require('./methods/wrappers');
Database.prototype.prepare = wrappers.prepare;
Database.prototype.transaction = require('./methods/transaction');
@ -56,13 +77,18 @@ Database.prototype.pragma = require('./methods/pragma');
Database.prototype.backup = require('./methods/backup');
Database.prototype.function = require('./methods/function');
Database.prototype.aggregate = require('./methods/aggregate');
Database.prototype.loadExtension = wrappers.loadExtension;
Database.prototype.table = require('./methods/table');
Database.prototype.createFTS5Tokenizer = require('./methods/createFTS5Tokenizer');
Database.prototype.exec = wrappers.exec;
Database.prototype.close = wrappers.close;
Database.prototype.defaultSafeIntegers = wrappers.defaultSafeIntegers;
Database.prototype.unsafeMode = wrappers.unsafeMode;
Database.prototype.signalTokenize = wrappers.signalTokenize;
Database.prototype[util.inspect] = require('./methods/inspect');
Database.setCorruptionLogger = setCorruptionLogger;
// Static
Database.setLogHandler = function setLogHandler(fn) {
logHandler = fn;
}
module.exports = Database;
setErrorConstructor(require('./sqlite-error'));

View File

@ -14,6 +14,7 @@ module.exports = function defineAggregate(name, options) {
const result = getFunctionOption(options, 'result', false);
const safeIntegers = 'safeIntegers' in options ? +getBooleanOption(options, 'safeIntegers') : 2;
const deterministic = getBooleanOption(options, 'deterministic');
const directOnly = getBooleanOption(options, 'directOnly');
const varargs = getBooleanOption(options, 'varargs');
let argCount = -1;
@ -24,7 +25,7 @@ module.exports = function defineAggregate(name, options) {
if (argCount > 100) throw new RangeError('User-defined functions cannot have more than 100 arguments');
}
this[cppdb].aggregate(start, step, inverse, result, name, argCount, safeIntegers, deterministic);
this[cppdb].aggregate(start, step, inverse, result, name, argCount, safeIntegers, deterministic, directOnly);
return this;
};

View File

@ -0,0 +1,24 @@
'use strict';
const { cppdb } = require('../util');
module.exports = function createFTS5Tokenizer(name, factory) {
// Validate arguments
if (typeof name !== 'string') throw new TypeError('Expected first argument to be a string');
if (!name) throw new TypeError('Virtual table module name cannot be an empty string');
if (typeof factory !== 'function') throw new TypeError('Expected second argument to be a constructor');
this[cppdb].createFTS5Tokenizer(name, function create(params) {
const instance = new factory(params);
function run(str) {
if (!instance.run) {
// This will throw in C++
return;
}
return instance.run(str);
}
return run;
});
return this;
};

View File

@ -15,6 +15,7 @@ module.exports = function defineFunction(name, options, fn) {
// Interpret options
const safeIntegers = 'safeIntegers' in options ? +getBooleanOption(options, 'safeIntegers') : 2;
const deterministic = getBooleanOption(options, 'deterministic');
const directOnly = getBooleanOption(options, 'directOnly');
const varargs = getBooleanOption(options, 'varargs');
let argCount = -1;
@ -25,6 +26,6 @@ module.exports = function defineFunction(name, options, fn) {
if (argCount > 100) throw new RangeError('User-defined functions cannot have more than 100 arguments');
}
this[cppdb].function(fn, name, argCount, safeIntegers, deterministic);
this[cppdb].function(fn, name, argCount, safeIntegers, deterministic, directOnly);
return this;
};

189
lib/methods/table.js Normal file
View File

@ -0,0 +1,189 @@
'use strict';
const { cppdb } = require('../util');
module.exports = function defineTable(name, factory) {
// Validate arguments
if (typeof name !== 'string') throw new TypeError('Expected first argument to be a string');
if (!name) throw new TypeError('Virtual table module name cannot be an empty string');
// Determine whether the module is eponymous-only or not
let eponymous = false;
if (typeof factory === 'object' && factory !== null) {
eponymous = true;
factory = defer(parseTableDefinition(factory, 'used', name));
} else {
if (typeof factory !== 'function') throw new TypeError('Expected second argument to be a function or a table definition object');
factory = wrapFactory(factory);
}
this[cppdb].table(factory, name, eponymous);
return this;
};
function wrapFactory(factory) {
return function virtualTableFactory(moduleName, databaseName, tableName, ...args) {
const thisObject = {
module: moduleName,
database: databaseName,
table: tableName,
};
// Generate a new table definition by invoking the factory
const def = apply.call(factory, thisObject, args);
if (typeof def !== 'object' || def === null) {
throw new TypeError(`Virtual table module "${moduleName}" did not return a table definition object`);
}
return parseTableDefinition(def, 'returned', moduleName);
};
}
function parseTableDefinition(def, verb, moduleName) {
// Validate required properties
if (!hasOwnProperty.call(def, 'rows')) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition without a "rows" property`);
}
if (!hasOwnProperty.call(def, 'columns')) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition without a "columns" property`);
}
// Validate "rows" property
const rows = def.rows;
if (typeof rows !== 'function' || Object.getPrototypeOf(rows) !== GeneratorFunctionPrototype) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "rows" property (should be a generator function)`);
}
// Validate "columns" property
let columns = def.columns;
if (!Array.isArray(columns) || !(columns = [...columns]).every(x => typeof x === 'string')) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "columns" property (should be an array of strings)`);
}
if (columns.length !== new Set(columns).size) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with duplicate column names`);
}
if (!columns.length) {
throw new RangeError(`Virtual table module "${moduleName}" ${verb} a table definition with zero columns`);
}
// Validate "parameters" property
let parameters;
if (hasOwnProperty.call(def, 'parameters')) {
parameters = def.parameters;
if (!Array.isArray(parameters) || !(parameters = [...parameters]).every(x => typeof x === 'string')) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "parameters" property (should be an array of strings)`);
}
} else {
parameters = inferParameters(rows);
}
if (parameters.length !== new Set(parameters).size) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with duplicate parameter names`);
}
if (parameters.length > 32) {
throw new RangeError(`Virtual table module "${moduleName}" ${verb} a table definition with more than the maximum number of 32 parameters`);
}
for (const parameter of parameters) {
if (columns.includes(parameter)) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with column "${parameter}" which was ambiguously defined as both a column and parameter`);
}
}
// Validate "safeIntegers" option
let safeIntegers = 2;
if (hasOwnProperty.call(def, 'safeIntegers')) {
const bool = def.safeIntegers;
if (typeof bool !== 'boolean') {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "safeIntegers" property (should be a boolean)`);
}
safeIntegers = +bool;
}
// Validate "directOnly" option
let directOnly = false;
if (hasOwnProperty.call(def, 'directOnly')) {
directOnly = def.directOnly;
if (typeof directOnly !== 'boolean') {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "directOnly" property (should be a boolean)`);
}
}
// Generate SQL for the virtual table definition
const columnDefinitions = [
...parameters.map(identifier).map(str => `${str} HIDDEN`),
...columns.map(identifier),
];
return [
`CREATE TABLE x(${columnDefinitions.join(', ')});`,
wrapGenerator(rows, new Map(columns.map((x, i) => [x, parameters.length + i])), moduleName),
parameters,
safeIntegers,
directOnly,
];
}
function wrapGenerator(generator, columnMap, moduleName) {
return function* virtualTable(...args) {
/*
We must defensively clone any buffers in the arguments, because
otherwise the generator could mutate one of them, which would cause
us to return incorrect values for hidden columns, potentially
corrupting the database.
*/
const output = args.map(x => Buffer.isBuffer(x) ? Buffer.from(x) : x);
for (let i = 0; i < columnMap.size; ++i) {
output.push(null); // Fill with nulls to prevent gaps in array (v8 optimization)
}
for (const row of generator(...args)) {
if (Array.isArray(row)) {
extractRowArray(row, output, columnMap.size, moduleName);
yield output;
} else if (typeof row === 'object' && row !== null) {
extractRowObject(row, output, columnMap, moduleName);
yield output;
} else {
throw new TypeError(`Virtual table module "${moduleName}" yielded something that isn't a valid row object`);
}
}
};
}
function extractRowArray(row, output, columnCount, moduleName) {
if (row.length !== columnCount) {
throw new TypeError(`Virtual table module "${moduleName}" yielded a row with an incorrect number of columns`);
}
const offset = output.length - columnCount;
for (let i = 0; i < columnCount; ++i) {
output[i + offset] = row[i];
}
}
function extractRowObject(row, output, columnMap, moduleName) {
let count = 0;
for (const key of Object.keys(row)) {
const index = columnMap.get(key);
if (index === undefined) {
throw new TypeError(`Virtual table module "${moduleName}" yielded a row with an undeclared column "${key}"`);
}
output[index] = row[key];
count += 1;
}
if (count !== columnMap.size) {
throw new TypeError(`Virtual table module "${moduleName}" yielded a row with missing columns`);
}
}
function inferParameters({ length }) {
if (!Number.isInteger(length) || length < 0) {
throw new TypeError('Expected function.length to be a positive integer');
}
const params = [];
for (let i = 0; i < length; ++i) {
params.push(`$${i + 1}`);
}
return params;
}
const { hasOwnProperty } = Object.prototype;
const { apply } = Function.prototype;
const GeneratorFunctionPrototype = Object.getPrototypeOf(function*(){});
const identifier = str => `"${str.replace(/"/g, '""')}"`;
const defer = x => () => x;

View File

@ -15,11 +15,6 @@ exports.close = function close() {
return this;
};
exports.loadExtension = function loadExtension(...args) {
this[cppdb].loadExtension(...args);
return this;
};
exports.defaultSafeIntegers = function defaultSafeIntegers(...args) {
this[cppdb].defaultSafeIntegers(...args);
return this;
@ -30,6 +25,10 @@ exports.unsafeMode = function unsafeMode(...args) {
return this;
};
exports.signalTokenize = function signalTokenize(...args) {
return this[cppdb].signalTokenize(...args);
};
exports.getters = {
name: {
get: function name() { return this[cppdb].name; },

View File

@ -12,8 +12,7 @@ function SqliteError(message, code) {
descriptor.value = '' + message;
Object.defineProperty(this, 'message', descriptor);
Error.captureStackTrace(this, SqliteError);
descriptor.value = code;
Object.defineProperty(this, 'code', descriptor);
this.code = code;
}
Object.setPrototypeOf(SqliteError, Error);
Object.setPrototypeOf(SqliteError.prototype, Error.prototype);

View File

@ -1,37 +1,41 @@
{
"name": "better-sqlite3",
"version": "7.1.4",
"name": "@signalapp/better-sqlite3",
"version": "9.0.13",
"description": "The fastest and simplest library for SQLite3 in Node.js.",
"homepage": "http://github.com/JoshuaWise/better-sqlite3",
"homepage": "http://github.com/WiseLibs/better-sqlite3",
"author": "Joshua Wise <joshuathomaswise@gmail.com>",
"main": "lib/index.js",
"repository": {
"type": "git",
"url": "git://github.com/JoshuaWise/better-sqlite3.git"
"url": "git://github.com/WiseLibs/better-sqlite3.git"
},
"main": "lib/index.js",
"types": "index.d.ts",
"files": [
"index.d.ts",
"binding.gyp",
"src/*.[ch]pp",
"lib/**",
"deps/**",
"!deps/sqlcipher.tar.gz",
"!deps/unverified.tmp"
],
"dependencies": {
"bindings": "^1.5.0",
"tar": "^6.1.0"
},
"devDependencies": {
"chai": "^4.3.4",
"cli-color": "^2.0.0",
"fs-extra": "^9.1.0",
"chai": "^4.3.6",
"cli-color": "^2.0.2",
"fs-extra": "^10.1.0",
"mocha": "^8.3.2",
"nodemark": "^0.3.0",
"sqlite": "^4.0.19",
"sqlite": "^4.0.23",
"sqlite3": "^5.0.2"
},
"scripts": {
"install": "npm run build-release",
"build-release": "node-gyp rebuild --release",
"build-debug": "node-gyp rebuild --debug",
"rebuild-release": "npm run lzz && npm run build-release",
"rebuild-debug": "npm run lzz && npm run build-debug",
"format": "xcrun clang-format --style=chromium -Werror --verbose -i src/*.cpp src/*.hpp",
"test": "mocha --exit --slow=75 --timeout=5000",
"benchmark": "node benchmark",
"download": "bash ./deps/download.sh",
"lzz": "lzz -hx hpp -sx cpp -k BETTER_SQLITE3 -d -hl -sl -e ./src/better_sqlite3.lzz"
"benchmark": "node benchmark"
},
"license": "MIT",
"keywords": [

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,82 +0,0 @@
#hdr
#include <climits>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include <set>
#include <unordered_map>
#include <sqlite3.h>
#include <node.h>
#include <node_object_wrap.h>
#include <node_buffer.h>
#end
#insert "util/macros.lzz"
#insert "util/query-macros.lzz"
#insert "util/constants.lzz"
#insert "util/bind-map.lzz"
struct Addon;
class Statement;
class Backup;
#insert "objects/database.lzz"
#insert "objects/statement.lzz"
#insert "objects/statement-iterator.lzz"
#insert "objects/backup.lzz"
#insert "util/custom-function.lzz"
#insert "util/custom-aggregate.lzz"
#insert "util/data.lzz"
#insert "util/binder.lzz"
struct Addon {
Addon(v8::Isolate* isolate) : privileged_info(NULL), next_id(0), cs(isolate) {}
CopyablePersistent<v8::Function> Statement;
CopyablePersistent<v8::Function> StatementIterator;
CopyablePersistent<v8::Function> Backup;
CopyablePersistent<v8::Function> SqliteError;
NODE_ARGUMENTS_POINTER privileged_info;
sqlite3_uint64 next_id;
CS cs;
std::set<Database*, Database::CompareDatabase> dbs;
NODE_METHOD(JS_setErrorConstructor) {
REQUIRE_ARGUMENT_FUNCTION(first, v8::Local<v8::Function> SqliteError);
OnlyAddon->SqliteError.Reset(OnlyIsolate, SqliteError);
}
static void Cleanup(void* ptr) {
Addon* addon = static_cast<Addon*>(ptr);
for (Database* db : addon->dbs) db->CloseHandles();
addon->dbs.clear();
delete addon;
}
inline sqlite3_uint64 NextId() {
return next_id++;
}
};
#src
NODE_MODULE_INIT(/* exports, context */) {
v8::Isolate* isolate = context->GetIsolate();
v8::HandleScope scope(isolate);
// Initialize addon instance.
Addon* addon = new Addon(isolate);
v8::Local<v8::External> data = v8::External::New(isolate, addon);
node::AddEnvironmentCleanupHook(isolate, Addon::Cleanup, addon);
// Create and export native-backed classes and functions.
exports->Set(context, InternalizedFromLatin1(isolate, "Database"), Database::Init(isolate, data)).FromJust();
exports->Set(context, InternalizedFromLatin1(isolate, "Statement"), Statement::Init(isolate, data)).FromJust();
exports->Set(context, InternalizedFromLatin1(isolate, "StatementIterator"), StatementIterator::Init(isolate, data)).FromJust();
exports->Set(context, InternalizedFromLatin1(isolate, "Backup"), Backup::Init(isolate, data)).FromJust();
exports->Set(context, InternalizedFromLatin1(isolate, "setErrorConstructor"), v8::FunctionTemplate::New(isolate, Addon::JS_setErrorConstructor, data)->GetFunction(context).ToLocalChecked()).FromJust();
// Store addon instance data.
addon->Statement.Reset(isolate, v8::Local<v8::Function>::Cast(exports->Get(context, InternalizedFromLatin1(isolate, "Statement")).ToLocalChecked()));
addon->StatementIterator.Reset(isolate, v8::Local<v8::Function>::Cast(exports->Get(context, InternalizedFromLatin1(isolate, "StatementIterator")).ToLocalChecked()));
addon->Backup.Reset(isolate, v8::Local<v8::Function>::Cast(exports->Get(context, InternalizedFromLatin1(isolate, "Backup")).ToLocalChecked()));
}
#end

39
src/local_vector.hpp Normal file
View File

@ -0,0 +1,39 @@
#ifndef SRC_LOCAL_VECTOR_H_
#define SRC_LOCAL_VECTOR_H_
#include <node.h>
// See: https://github.com/v8/v8/commit/e1649301dfbfd34a448c3a0232c8a6206b716c73
// Required V8 verison: 12.0.54 or higher
#if V8_MAJOR_VERSION > 12 || \
V8_MINOR_VERSION == 12 && \
(V8_MINOR_VERSION > 0 || \
V8_MINOR_VERSION == 0 && V8_PATCH_VERSION >= 54)
template <class T>
class LocalVector : public v8::LocalVector<T> {
public:
LocalVector(v8::Isolate* isolate) : v8::LocalVector<T>(isolate) {}
inline bool is_supported() { return true; }
};
#else
template <class T>
class LocalVector {
public:
LocalVector(v8::Isolate* isolate) {}
inline void reserve(size_t size) {}
inline size_t size() { return 0; }
inline void emplace_back(v8::Local<T> value) { abort(); }
inline v8::Local<T>* data() { abort(); }
inline bool is_supported() { return false; }
};
#endif
#endif // SRC_LOCAL_VECTOR_H_

View File

@ -1,132 +0,0 @@
class Backup : public node::ObjectWrap {
public:
INIT(Init) {
v8::Local<v8::FunctionTemplate> t = NewConstructorTemplate(isolate, data, JS_new, "Backup");
SetPrototypeMethod(isolate, data, t, "transfer", JS_transfer);
SetPrototypeMethod(isolate, data, t, "close", JS_close);
return t->GetFunction(OnlyContext).ToLocalChecked();
}
// Used to support ordered containers.
static inline bool Compare(Backup const * const a, Backup const * const b) {
return a->id < b->id;
}
// Whenever this is used, db->RemoveBackup must be invoked beforehand.
void CloseHandles() {
if (alive) {
alive = false;
std::string filename(sqlite3_db_filename(dest_handle, "main"));
sqlite3_backup_finish(backup_handle);
int status = sqlite3_close(dest_handle);
assert(status == SQLITE_OK); ((void)status);
if (unlink) remove(filename.c_str());
}
}
~Backup() {
if (alive) db->RemoveBackup(this);
CloseHandles();
}
private:
explicit Backup(Database* _db, sqlite3* _dest_handle, sqlite3_backup* _backup_handle, sqlite3_uint64 _id, bool _unlink) : node::ObjectWrap(),
db(_db),
dest_handle(_dest_handle),
backup_handle(_backup_handle),
id(_id),
alive(true),
unlink(_unlink) {
assert(db != NULL);
assert(dest_handle != NULL);
assert(backup_handle != NULL);
db->AddBackup(this);
}
NODE_METHOD(JS_new) {
UseAddon;
if (!addon->privileged_info) return ThrowTypeError("Disabled constructor");
assert(info.IsConstructCall());
Database* db = Unwrap<Database>(addon->privileged_info->This());
REQUIRE_DATABASE_OPEN(db->GetState());
REQUIRE_DATABASE_NOT_BUSY(db->GetState());
v8::Local<v8::Object> database = v8::Local<v8::Object>::Cast((*addon->privileged_info)[0]);
v8::Local<v8::String> attachedName = v8::Local<v8::String>::Cast((*addon->privileged_info)[1]);
v8::Local<v8::String> destFile = v8::Local<v8::String>::Cast((*addon->privileged_info)[2]);
bool unlink = v8::Local<v8::Boolean>::Cast((*addon->privileged_info)[3])->Value();
UseIsolate;
sqlite3* dest_handle;
v8::String::Utf8Value dest_file(isolate, destFile);
v8::String::Utf8Value attached_name(isolate, attachedName);
int mask = (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
if (sqlite3_open_v2(*dest_file, &dest_handle, mask, NULL) != SQLITE_OK) {
Database::ThrowSqliteError(addon, dest_handle);
int status = sqlite3_close(dest_handle);
assert(status == SQLITE_OK); ((void)status);
return;
}
sqlite3_extended_result_codes(dest_handle, 1);
sqlite3_limit(dest_handle, SQLITE_LIMIT_LENGTH, INT_MAX);
sqlite3_backup* backup_handle = sqlite3_backup_init(dest_handle, "main", db->GetHandle(), *attached_name);
if (backup_handle == NULL) {
Database::ThrowSqliteError(addon, dest_handle);
int status = sqlite3_close(dest_handle);
assert(status == SQLITE_OK); ((void)status);
return;
}
Backup* backup = new Backup(db, dest_handle, backup_handle, addon->NextId(), unlink);
backup->Wrap(info.This());
SetFrozen(isolate, OnlyContext, info.This(), addon->cs.database, database);
info.GetReturnValue().Set(info.This());
}
NODE_METHOD(JS_transfer) {
Backup* backup = Unwrap<Backup>(info.This());
REQUIRE_ARGUMENT_INT32(first, int pages);
REQUIRE_DATABASE_OPEN(backup->db->GetState());
assert(backup->db->GetState()->busy == false);
assert(backup->alive == true);
sqlite3_backup* backup_handle = backup->backup_handle;
int status = sqlite3_backup_step(backup_handle, pages) & 0xff;
Addon* addon = backup->db->GetAddon();
if (status == SQLITE_OK || status == SQLITE_DONE || status == SQLITE_BUSY) {
int total_pages = sqlite3_backup_pagecount(backup_handle);
int remaining_pages = sqlite3_backup_remaining(backup_handle);
UseIsolate;
UseContext;
v8::Local<v8::Object> result = v8::Object::New(isolate);
result->Set(ctx, CS::Get(isolate, addon->cs.totalPages), v8::Int32::New(isolate, total_pages)).FromJust();
result->Set(ctx, CS::Get(isolate, addon->cs.remainingPages), v8::Int32::New(isolate, remaining_pages)).FromJust();
info.GetReturnValue().Set(result);
if (status == SQLITE_DONE) backup->unlink = false;
} else {
Database::ThrowSqliteError(addon, sqlite3_errstr(status), status);
}
}
NODE_METHOD(JS_close) {
Backup* backup = Unwrap<Backup>(info.This());
assert(backup->db->GetState()->busy == false);
if (backup->alive) backup->db->RemoveBackup(backup);
backup->CloseHandles();
info.GetReturnValue().Set(info.This());
}
Database* const db;
sqlite3* const dest_handle;
sqlite3_backup* const backup_handle;
const sqlite3_uint64 id;
bool alive;
bool unlink;
};

View File

@ -1,376 +0,0 @@
class Database : public node::ObjectWrap {
public:
INIT(Init) {
v8::Local<v8::FunctionTemplate> t = NewConstructorTemplate(isolate, data, JS_new, "Database");
SetPrototypeMethod(isolate, data, t, "prepare", JS_prepare);
SetPrototypeMethod(isolate, data, t, "exec", JS_exec);
SetPrototypeMethod(isolate, data, t, "backup", JS_backup);
SetPrototypeMethod(isolate, data, t, "function", JS_function);
SetPrototypeMethod(isolate, data, t, "aggregate", JS_aggregate);
SetPrototypeMethod(isolate, data, t, "loadExtension", JS_loadExtension);
SetPrototypeMethod(isolate, data, t, "close", JS_close);
SetPrototypeMethod(isolate, data, t, "defaultSafeIntegers", JS_defaultSafeIntegers);
SetPrototypeMethod(isolate, data, t, "unsafeMode", JS_unsafeMode);
SetPrototypeGetter(isolate, data, t, "open", JS_open);
SetPrototypeGetter(isolate, data, t, "inTransaction", JS_inTransaction);
return t->GetFunction(OnlyContext).ToLocalChecked();
}
// Used to support ordered containers.
class CompareDatabase { public:
bool operator() (Database const * const a, Database const * const b) const {
return a < b;
}
};
class CompareStatement { public:
bool operator() (Statement const * const a, Statement const * const b) const {
return Statement::Compare(a, b);
}
};
class CompareBackup { public:
bool operator() (Backup const * const a, Backup const * const b) const {
return Backup::Compare(a, b);
}
};
// Proper error handling logic for when an sqlite3 operation fails.
void ThrowDatabaseError() {
if (was_js_error) was_js_error = false;
else ThrowSqliteError(addon, db_handle);
}
static void ThrowSqliteError(Addon* addon, sqlite3* db_handle) {
assert(db_handle != NULL);
ThrowSqliteError(addon, sqlite3_errmsg(db_handle), sqlite3_extended_errcode(db_handle));
}
static void ThrowSqliteError(Addon* addon, const char* message, int code) {
assert(message != NULL);
assert((code & 0xff) != SQLITE_OK);
assert((code & 0xff) != SQLITE_ROW);
assert((code & 0xff) != SQLITE_DONE);
EasyIsolate;
v8::Local<v8::Value> args[2] = {
StringFromUtf8(isolate, message, -1),
addon->cs.Code(isolate, code)
};
isolate->ThrowException(v8::Local<v8::Function>::New(isolate, addon->SqliteError)
->NewInstance(OnlyContext, 2, args)
.ToLocalChecked());
}
// Allows Statements to log their executed SQL.
bool Log(v8::Isolate* isolate, sqlite3_stmt* handle) {
assert(was_js_error == false);
if (!has_logger) return false;
char* expanded = sqlite3_expanded_sql(handle);
v8::Local<v8::Value> arg = StringFromUtf8(isolate, expanded ? expanded : sqlite3_sql(handle), -1);
was_js_error = v8::Local<v8::Function>::Cast(v8::Local<v8::Value>::New(isolate, logger))
->Call(OnlyContext, v8::Undefined(isolate), 1, &arg)
.IsEmpty();
if (expanded) sqlite3_free(expanded);
return was_js_error;
}
// Allow Statements to manage themselves when created and garbage collected.
inline void AddStatement(Statement* stmt) { stmts.insert(stmts.end(), stmt); }
inline void RemoveStatement(Statement* stmt) { stmts.erase(stmt); }
// Allow Backups to manage themselves when created and garbage collected.
inline void AddBackup(Backup* backup) { backups.insert(backups.end(), backup); }
inline void RemoveBackup(Backup* backup) { backups.erase(backup); }
// A view for Statements to see and modify Database state.
// The order of these fields must exactly match their actual order.
struct State {
const bool open;
bool busy;
const bool safe_ints;
const bool unsafe_mode;
bool was_js_error;
const bool has_logger;
unsigned short iterators;
Addon* const addon;
};
inline State* GetState() {
return reinterpret_cast<State*>(&open);
}
inline sqlite3* GetHandle() {
return db_handle;
}
inline Addon* GetAddon() {
return addon;
}
// Whenever this is used, addon->dbs.erase() must be invoked beforehand.
void CloseHandles() {
if (open) {
open = false;
for (Statement* stmt : stmts) stmt->CloseHandles();
for (Backup* backup : backups) backup->CloseHandles();
stmts.clear();
backups.clear();
int status = sqlite3_close(db_handle);
assert(status == SQLITE_OK); ((void)status);
}
}
~Database() {
if (open) addon->dbs.erase(this);
CloseHandles();
}
private:
explicit Database(sqlite3* _db_handle, v8::Isolate* isolate, Addon* _addon, v8::Local<v8::Value> _logger) : node::ObjectWrap(),
db_handle(_db_handle),
open(true),
busy(false),
safe_ints(false),
unsafe_mode(false),
was_js_error(false),
has_logger(_logger->IsFunction()),
iterators(0),
addon(_addon),
logger(isolate, _logger),
stmts(),
backups() {
assert(_db_handle != NULL);
addon->dbs.insert(this);
}
NODE_METHOD(JS_new) {
assert(info.IsConstructCall());
REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> filename);
REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> filenameGiven);
REQUIRE_ARGUMENT_BOOLEAN(third, bool in_memory);
REQUIRE_ARGUMENT_BOOLEAN(fourth, bool readonly);
REQUIRE_ARGUMENT_BOOLEAN(fifth, bool must_exist);
REQUIRE_ARGUMENT_INT32(sixth, int timeout);
REQUIRE_ARGUMENT_ANY(seventh, v8::Local<v8::Value> logger);
UseAddon;
UseIsolate;
sqlite3* db_handle;
v8::String::Utf8Value utf8(isolate, filename);
int mask = readonly ? SQLITE_OPEN_READONLY
: must_exist ? SQLITE_OPEN_READWRITE
: (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
if (sqlite3_open_v2(*utf8, &db_handle, mask, NULL) != SQLITE_OK) {
ThrowSqliteError(addon, db_handle);
int status = sqlite3_close(db_handle);
assert(status == SQLITE_OK); ((void)status);
return;
}
assert(sqlite3_db_mutex(db_handle) == NULL);
sqlite3_extended_result_codes(db_handle, 1);
sqlite3_busy_timeout(db_handle, timeout);
sqlite3_limit(db_handle, SQLITE_LIMIT_LENGTH, MAX_BUFFER_SIZE < MAX_STRING_SIZE ? MAX_BUFFER_SIZE : MAX_STRING_SIZE);
sqlite3_limit(db_handle, SQLITE_LIMIT_SQL_LENGTH, MAX_STRING_SIZE);
int status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL);
assert(status == SQLITE_OK);
status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL);
assert(status == SQLITE_OK);
UseContext;
Database* db = new Database(db_handle, isolate, addon, logger);
db->Wrap(info.This());
SetFrozen(isolate, ctx, info.This(), addon->cs.memory, v8::Boolean::New(isolate, in_memory));
SetFrozen(isolate, ctx, info.This(), addon->cs.readonly, v8::Boolean::New(isolate, readonly));
SetFrozen(isolate, ctx, info.This(), addon->cs.name, filenameGiven);
info.GetReturnValue().Set(info.This());
}
NODE_METHOD(JS_prepare) {
REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> source);
REQUIRE_ARGUMENT_OBJECT(second, v8::Local<v8::Object> database);
REQUIRE_ARGUMENT_BOOLEAN(third, bool pragmaMode);
(void)source;
(void)database;
(void)pragmaMode;
UseAddon;
UseIsolate;
v8::Local<v8::Function> c = v8::Local<v8::Function>::New(isolate, addon->Statement);
addon->privileged_info = &info;
v8::MaybeLocal<v8::Object> maybe_statement = c->NewInstance(OnlyContext, 0, NULL);
addon->privileged_info = NULL;
if (!maybe_statement.IsEmpty()) info.GetReturnValue().Set(maybe_statement.ToLocalChecked());
}
NODE_METHOD(JS_exec) {
Database* db = Unwrap<Database>(info.This());
REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> source);
REQUIRE_DATABASE_OPEN(db);
REQUIRE_DATABASE_NOT_BUSY(db);
REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db);
db->busy = true;
UseIsolate;
v8::String::Utf8Value utf8(isolate, source);
const char* sql = *utf8;
const char* tail;
int status;
const bool has_logger = db->has_logger;
sqlite3* const db_handle = db->db_handle;
sqlite3_stmt* handle;
for (;;) {
while (IS_SKIPPED(*sql)) ++sql;
status = sqlite3_prepare_v2(db_handle, sql, -1, &handle, &tail);
sql = tail;
if (!handle) break;
if (has_logger && db->Log(isolate, handle)) {
sqlite3_finalize(handle);
status = -1;
break;
}
do status = sqlite3_step(handle);
while (status == SQLITE_ROW);
status = sqlite3_finalize(handle);
if (status != SQLITE_OK) break;
}
db->busy = false;
if (status != SQLITE_OK) {
db->ThrowDatabaseError();
}
}
NODE_METHOD(JS_backup) {
REQUIRE_ARGUMENT_OBJECT(first, v8::Local<v8::Object> database);
REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> attachedName);
REQUIRE_ARGUMENT_STRING(third, v8::Local<v8::String> destFile);
REQUIRE_ARGUMENT_BOOLEAN(fourth, bool unlink);
(void)database;
(void)attachedName;
(void)destFile;
(void)unlink;
UseAddon;
UseIsolate;
v8::Local<v8::Function> c = v8::Local<v8::Function>::New(isolate, addon->Backup);
addon->privileged_info = &info;
v8::MaybeLocal<v8::Object> maybe_backup = c->NewInstance(OnlyContext, 0, NULL);
addon->privileged_info = NULL;
if (!maybe_backup.IsEmpty()) info.GetReturnValue().Set(maybe_backup.ToLocalChecked());
}
NODE_METHOD(JS_function) {
Database* db = Unwrap<Database>(info.This());
REQUIRE_ARGUMENT_FUNCTION(first, v8::Local<v8::Function> fn);
REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> nameString);
REQUIRE_ARGUMENT_INT32(third, int argc);
REQUIRE_ARGUMENT_INT32(fourth, int safe_ints);
REQUIRE_ARGUMENT_BOOLEAN(fifth, bool deterministic);
REQUIRE_DATABASE_OPEN(db);
REQUIRE_DATABASE_NOT_BUSY(db);
REQUIRE_DATABASE_NO_ITERATORS(db);
UseIsolate;
v8::String::Utf8Value name(isolate, nameString);
int mask = deterministic ? SQLITE_UTF8 | SQLITE_DETERMINISTIC : SQLITE_UTF8;
safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(db->safe_ints);
if (sqlite3_create_function_v2(db->db_handle, *name, argc, mask, new CustomFunction(isolate, db, fn, *name, safe_ints), CustomFunction::xFunc, NULL, NULL, CustomFunction::xDestroy) != SQLITE_OK) {
db->ThrowDatabaseError();
}
}
NODE_METHOD(JS_aggregate) {
Database* db = Unwrap<Database>(info.This());
REQUIRE_ARGUMENT_ANY(first, v8::Local<v8::Value> start);
REQUIRE_ARGUMENT_FUNCTION(second, v8::Local<v8::Function> step);
REQUIRE_ARGUMENT_ANY(third, v8::Local<v8::Value> inverse);
REQUIRE_ARGUMENT_ANY(fourth, v8::Local<v8::Value> result);
REQUIRE_ARGUMENT_STRING(fifth, v8::Local<v8::String> nameString);
REQUIRE_ARGUMENT_INT32(sixth, int argc);
REQUIRE_ARGUMENT_INT32(seventh, int safe_ints);
REQUIRE_ARGUMENT_BOOLEAN(eighth, bool deterministic);
REQUIRE_DATABASE_OPEN(db);
REQUIRE_DATABASE_NOT_BUSY(db);
REQUIRE_DATABASE_NO_ITERATORS(db);
UseIsolate;
v8::String::Utf8Value name(isolate, nameString);
auto xInverse = inverse->IsFunction() ? CustomAggregate::xInverse : NULL;
auto xValue = xInverse ? CustomAggregate::xValue : NULL;
int mask = deterministic ? SQLITE_UTF8 | SQLITE_DETERMINISTIC : SQLITE_UTF8;
safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(db->safe_ints);
if (sqlite3_create_window_function(db->db_handle, *name, argc, mask, new CustomAggregate(isolate, db, start, step, inverse, result, *name, safe_ints), CustomAggregate::xStep, CustomAggregate::xFinal, xValue, xInverse, CustomAggregate::xDestroy) != SQLITE_OK) {
db->ThrowDatabaseError();
}
}
NODE_METHOD(JS_loadExtension) {
Database* db = Unwrap<Database>(info.This());
v8::Local<v8::String> entryPoint;
REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> filename);
if (info.Length() > 1) { REQUIRE_ARGUMENT_STRING(second, entryPoint); }
REQUIRE_DATABASE_OPEN(db);
REQUIRE_DATABASE_NOT_BUSY(db);
REQUIRE_DATABASE_NO_ITERATORS(db);
UseIsolate;
char* error;
int status = sqlite3_load_extension(
db->db_handle,
*v8::String::Utf8Value(isolate, filename),
entryPoint.IsEmpty() ? NULL : *v8::String::Utf8Value(isolate, entryPoint),
&error
);
if (status != SQLITE_OK) {
ThrowSqliteError(db->addon, error, status);
}
sqlite3_free(error);
}
NODE_METHOD(JS_close) {
Database* db = Unwrap<Database>(info.This());
if (db->open) {
REQUIRE_DATABASE_NOT_BUSY(db);
REQUIRE_DATABASE_NO_ITERATORS(db);
db->addon->dbs.erase(db);
db->CloseHandles();
}
}
NODE_METHOD(JS_defaultSafeIntegers) {
Database* db = Unwrap<Database>(info.This());
if (info.Length() == 0) db->safe_ints = true;
else { REQUIRE_ARGUMENT_BOOLEAN(first, db->safe_ints); }
}
NODE_METHOD(JS_unsafeMode) {
Database* db = Unwrap<Database>(info.This());
if (info.Length() == 0) db->unsafe_mode = true;
else { REQUIRE_ARGUMENT_BOOLEAN(first, db->unsafe_mode); }
sqlite3_db_config(db->db_handle, SQLITE_DBCONFIG_DEFENSIVE, static_cast<int>(!db->unsafe_mode), NULL);
}
NODE_GETTER(JS_open) {
info.GetReturnValue().Set(Unwrap<Database>(info.This())->open);
}
NODE_GETTER(JS_inTransaction) {
Database* db = Unwrap<Database>(info.This());
info.GetReturnValue().Set(db->open && !static_cast<bool>(sqlite3_get_autocommit(db->db_handle)));
}
static const int MAX_BUFFER_SIZE = node::Buffer::kMaxLength > INT_MAX ? INT_MAX : static_cast<int>(node::Buffer::kMaxLength);
static const int MAX_STRING_SIZE = v8::String::kMaxLength > INT_MAX ? INT_MAX : static_cast<int>(v8::String::kMaxLength);
sqlite3* const db_handle;
bool open;
bool busy;
bool safe_ints;
bool unsafe_mode;
bool was_js_error;
const bool has_logger;
unsigned short iterators;
Addon* const addon;
const CopyablePersistent<v8::Value> logger;
std::set<Statement*, CompareStatement> stmts;
std::set<Backup*, CompareBackup> backups;
};

View File

@ -1,138 +0,0 @@
class StatementIterator : public node::ObjectWrap {
public:
INIT(Init) {
v8::Local<v8::FunctionTemplate> t = NewConstructorTemplate(isolate, data, JS_new, "StatementIterator");
SetPrototypeMethod(isolate, data, t, "next", JS_next);
SetPrototypeMethod(isolate, data, t, "return", JS_return);
SetPrototypeSymbolMethod(isolate, data, t, v8::Symbol::GetIterator(isolate), JS_symbolIterator);
return t->GetFunction(OnlyContext).ToLocalChecked();
}
// The ~Statement destructor currently covers any state this object creates.
// Additionally, we actually DON'T want to set stmt->locked or db_state
// ->iterators in this destructor, to ensure deterministic database access.
~StatementIterator() {}
private:
explicit StatementIterator(Statement* _stmt, bool _bound) : node::ObjectWrap(),
stmt(_stmt),
handle(_stmt->handle),
db_state(_stmt->db->GetState()),
bound(_bound),
safe_ints(_stmt->safe_ints),
mode(_stmt->mode),
alive(true),
logged(!db_state->has_logger) {
assert(stmt != NULL);
assert(handle != NULL);
assert(stmt->bound == bound);
assert(stmt->alive == true);
assert(stmt->locked == false);
assert(db_state->iterators < USHRT_MAX);
stmt->locked = true;
db_state->iterators += 1;
}
NODE_METHOD(JS_new) {
UseAddon;
if (!addon->privileged_info) return ThrowTypeError("Disabled constructor");
assert(info.IsConstructCall());
StatementIterator* iter;
{
NODE_ARGUMENTS info = *addon->privileged_info;
STATEMENT_START_LOGIC(REQUIRE_STATEMENT_RETURNS_DATA, DOES_ADD_ITERATOR);
iter = new StatementIterator(stmt, bound);
}
UseIsolate;
UseContext;
iter->Wrap(info.This());
SetFrozen(isolate, ctx, info.This(), addon->cs.statement, addon->privileged_info->This());
info.GetReturnValue().Set(info.This());
}
NODE_METHOD(JS_next) {
StatementIterator* iter = Unwrap<StatementIterator>(info.This());
REQUIRE_DATABASE_NOT_BUSY(iter->db_state);
if (iter->alive) iter->Next(info);
else info.GetReturnValue().Set(DoneRecord(OnlyIsolate, iter->db_state->addon));
}
NODE_METHOD(JS_return) {
StatementIterator* iter = Unwrap<StatementIterator>(info.This());
REQUIRE_DATABASE_NOT_BUSY(iter->db_state);
if (iter->alive) iter->Return(info);
else info.GetReturnValue().Set(DoneRecord(OnlyIsolate, iter->db_state->addon));
}
NODE_METHOD(JS_symbolIterator) {
info.GetReturnValue().Set(info.This());
}
void Next(NODE_ARGUMENTS info) {
assert(alive == true);
db_state->busy = true;
if (!logged) {
logged = true;
if (stmt->db->Log(OnlyIsolate, handle)) {
db_state->busy = false;
Throw();
return;
}
}
int status = sqlite3_step(handle);
db_state->busy = false;
if (status == SQLITE_ROW) {
UseIsolate;
UseContext;
info.GetReturnValue().Set(
NewRecord(isolate, ctx, Data::GetRowJS(isolate, ctx, handle, safe_ints, mode), db_state->addon, false)
);
} else {
if (status == SQLITE_DONE) Return(info);
else Throw();
}
}
void Return(NODE_ARGUMENTS info) {
Cleanup();
STATEMENT_RETURN_LOGIC(DoneRecord(OnlyIsolate, db_state->addon));
}
void Throw() {
Cleanup();
Database* db = stmt->db;
STATEMENT_THROW_LOGIC();
}
void Cleanup() {
assert(alive == true);
alive = false;
stmt->locked = false;
db_state->iterators -= 1;
sqlite3_reset(handle);
}
static inline v8::Local<v8::Object> NewRecord(v8::Isolate* isolate, v8::Local<v8::Context> ctx, v8::Local<v8::Value> value, Addon* addon, bool done) {
v8::Local<v8::Object> record = v8::Object::New(isolate);
record->Set(ctx, CS::Get(isolate, addon->cs.value), value).FromJust();
record->Set(ctx, CS::Get(isolate, addon->cs.done), v8::Boolean::New(isolate, done)).FromJust();
return record;
}
static inline v8::Local<v8::Object> DoneRecord(v8::Isolate* isolate, Addon* addon) {
return NewRecord(isolate, OnlyContext, v8::Undefined(isolate), addon, true);
}
Statement* const stmt;
sqlite3_stmt* const handle;
Database::State* const db_state;
const bool bound;
const bool safe_ints;
const char mode;
bool alive;
bool logged;
};

View File

@ -1,317 +0,0 @@
class Statement : public node::ObjectWrap {
friend class StatementIterator;
public:
INIT(Init) {
v8::Local<v8::FunctionTemplate> t = NewConstructorTemplate(isolate, data, JS_new, "Statement");
SetPrototypeMethod(isolate, data, t, "run", JS_run);
SetPrototypeMethod(isolate, data, t, "get", JS_get);
SetPrototypeMethod(isolate, data, t, "all", JS_all);
SetPrototypeMethod(isolate, data, t, "iterate", JS_iterate);
SetPrototypeMethod(isolate, data, t, "bind", JS_bind);
SetPrototypeMethod(isolate, data, t, "pluck", JS_pluck);
SetPrototypeMethod(isolate, data, t, "expand", JS_expand);
SetPrototypeMethod(isolate, data, t, "raw", JS_raw);
SetPrototypeMethod(isolate, data, t, "safeIntegers", JS_safeIntegers);
SetPrototypeMethod(isolate, data, t, "columns", JS_columns);
return t->GetFunction(OnlyContext).ToLocalChecked();
}
// Used to support ordered containers.
static inline bool Compare(Statement const * const a, Statement const * const b) {
return a->extras->id < b->extras->id;
}
// Returns the Statement's bind map (creates it upon first execution).
BindMap* GetBindMap(v8::Isolate* isolate) {
if (has_bind_map) return &extras->bind_map;
BindMap* bind_map = &extras->bind_map;
int param_count = sqlite3_bind_parameter_count(handle);
for (int i=1; i<=param_count; ++i) {
const char* name = sqlite3_bind_parameter_name(handle, i);
if (name != NULL) bind_map->Add(isolate, name + 1, i);
}
has_bind_map = true;
return bind_map;
}
// Whenever this is used, db->RemoveStatement must be invoked beforehand.
void CloseHandles() {
if (alive) {
alive = false;
sqlite3_finalize(handle);
}
}
~Statement() {
if (alive) db->RemoveStatement(this);
CloseHandles();
delete extras;
}
private:
// A class for holding values that are less often used.
class Extras { friend class Statement;
explicit Extras(sqlite3_uint64 _id) : bind_map(0), id(_id) {}
BindMap bind_map;
const sqlite3_uint64 id;
};
explicit Statement(Database* _db, sqlite3_stmt* _handle, sqlite3_uint64 _id, bool _returns_data) : node::ObjectWrap(),
db(_db),
handle(_handle),
extras(new Extras(_id)),
alive(true),
locked(false),
bound(false),
has_bind_map(false),
safe_ints(_db->GetState()->safe_ints),
mode(Data::FLAT),
returns_data(_returns_data) {
assert(db != NULL);
assert(handle != NULL);
assert(db->GetState()->open);
assert(!db->GetState()->busy);
db->AddStatement(this);
}
NODE_METHOD(JS_new) {
UseAddon;
if (!addon->privileged_info) {
return ThrowTypeError("Statements can only be constructed by the db.prepare() method");
}
assert(info.IsConstructCall());
Database* db = Unwrap<Database>(addon->privileged_info->This());
REQUIRE_DATABASE_OPEN(db->GetState());
REQUIRE_DATABASE_NOT_BUSY(db->GetState());
v8::Local<v8::String> source = v8::Local<v8::String>::Cast((*addon->privileged_info)[0]);
v8::Local<v8::Object> database = v8::Local<v8::Object>::Cast((*addon->privileged_info)[1]);
bool pragmaMode = v8::Local<v8::Boolean>::Cast((*addon->privileged_info)[2])->Value();
int flags = SQLITE_PREPARE_PERSISTENT;
if (pragmaMode) {
REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db->GetState());
flags = 0;
}
UseIsolate;
v8::String::Utf8Value utf8(isolate, source);
sqlite3_stmt* handle;
const char* tail;
if (sqlite3_prepare_v3(db->GetHandle(), *utf8, utf8.length() + 1, flags, &handle, &tail) != SQLITE_OK) {
return db->ThrowDatabaseError();
}
if (handle == NULL) {
return ThrowRangeError("The supplied SQL string contains no statements");
}
for (char c; (c = *tail); ++tail) {
if (IS_SKIPPED(c)) continue;
if (c == '/' && tail[1] == '*') {
tail += 2;
for (char c; (c = *tail); ++tail) {
if (c == '*' && tail[1] == '/') {
tail += 1;
break;
}
}
} else if (c == '-' && tail[1] == '-') {
tail += 2;
for (char c; (c = *tail); ++tail) {
if (c == '\n') break;
}
} else {
sqlite3_finalize(handle);
return ThrowRangeError("The supplied SQL string contains more than one statement");
}
}
UseContext;
bool returns_data = (sqlite3_stmt_readonly(handle) && sqlite3_column_count(handle) >= 1) || pragmaMode;
Statement* stmt = new Statement(db, handle, addon->NextId(), returns_data);
stmt->Wrap(info.This());
SetFrozen(isolate, ctx, info.This(), addon->cs.reader, v8::Boolean::New(isolate, returns_data));
SetFrozen(isolate, ctx, info.This(), addon->cs.source, source);
SetFrozen(isolate, ctx, info.This(), addon->cs.database, database);
info.GetReturnValue().Set(info.This());
}
NODE_METHOD(JS_run) {
STATEMENT_START(REQUIRE_STATEMENT_DOESNT_RETURN_DATA, DOES_MUTATE);
sqlite3* db_handle = db->GetHandle();
int total_changes_before = sqlite3_total_changes(db_handle);
sqlite3_step(handle);
if (sqlite3_reset(handle) == SQLITE_OK) {
int changes = sqlite3_total_changes(db_handle) == total_changes_before ? 0 : sqlite3_changes(db_handle);
sqlite3_int64 id = sqlite3_last_insert_rowid(db_handle);
Addon* addon = db->GetAddon();
UseContext;
v8::Local<v8::Object> result = v8::Object::New(isolate);
result->Set(ctx, CS::Get(isolate, addon->cs.changes), v8::Int32::New(isolate, changes)).FromJust();
result->Set(ctx, CS::Get(isolate, addon->cs.lastInsertRowid),
stmt->safe_ints
? v8::Local<v8::Value>::Cast(v8::BigInt::New(isolate, id))
: v8::Local<v8::Value>::Cast(v8::Number::New(isolate, (double)id))
).FromJust();
STATEMENT_RETURN(result);
}
STATEMENT_THROW();
}
NODE_METHOD(JS_get) {
STATEMENT_START(REQUIRE_STATEMENT_RETURNS_DATA, DOES_NOT_MUTATE);
int status = sqlite3_step(handle);
if (status == SQLITE_ROW) {
v8::Local<v8::Value> result = Data::GetRowJS(isolate, OnlyContext, handle, stmt->safe_ints, stmt->mode);
sqlite3_reset(handle);
STATEMENT_RETURN(result);
} else if (status == SQLITE_DONE) {
sqlite3_reset(handle);
STATEMENT_RETURN(v8::Undefined(isolate));
}
sqlite3_reset(handle);
STATEMENT_THROW();
}
NODE_METHOD(JS_all) {
STATEMENT_START(REQUIRE_STATEMENT_RETURNS_DATA, DOES_NOT_MUTATE);
UseContext;
v8::Local<v8::Array> result = v8::Array::New(isolate, 0);
uint32_t row_count = 0;
const bool safe_ints = stmt->safe_ints;
const char mode = stmt->mode;
bool js_error = false;
while (sqlite3_step(handle) == SQLITE_ROW) {
if (row_count == 0xffffffff) { ThrowRangeError("Array overflow (too many rows returned)"); js_error = true; break; }
result->Set(ctx, row_count++, Data::GetRowJS(isolate, ctx, handle, safe_ints, mode)).FromJust();
}
if (sqlite3_reset(handle) == SQLITE_OK && !js_error) {
STATEMENT_RETURN(result);
}
if (js_error) db->GetState()->was_js_error = true;
STATEMENT_THROW();
}
NODE_METHOD(JS_iterate) {
UseAddon;
UseIsolate;
v8::Local<v8::Function> c = v8::Local<v8::Function>::New(isolate, addon->StatementIterator);
addon->privileged_info = &info;
v8::MaybeLocal<v8::Object> maybe_iter = c->NewInstance(OnlyContext, 0, NULL);
addon->privileged_info = NULL;
if (!maybe_iter.IsEmpty()) info.GetReturnValue().Set(maybe_iter.ToLocalChecked());
}
NODE_METHOD(JS_bind) {
Statement* stmt = Unwrap<Statement>(info.This());
if (stmt->bound) return ThrowTypeError("The bind() method can only be invoked once per statement object");
REQUIRE_DATABASE_OPEN(stmt->db->GetState());
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
REQUIRE_STATEMENT_NOT_LOCKED(stmt);
STATEMENT_BIND(stmt->handle);
stmt->bound = true;
info.GetReturnValue().Set(info.This());
}
NODE_METHOD(JS_pluck) {
Statement* stmt = Unwrap<Statement>(info.This());
if (!stmt->returns_data) return ThrowTypeError("The pluck() method is only for statements that return data");
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
REQUIRE_STATEMENT_NOT_LOCKED(stmt);
bool use = true;
if (info.Length() != 0) { REQUIRE_ARGUMENT_BOOLEAN(first, use); }
stmt->mode = use ? Data::PLUCK : stmt->mode == Data::PLUCK ? Data::FLAT : stmt->mode;
info.GetReturnValue().Set(info.This());
}
NODE_METHOD(JS_expand) {
Statement* stmt = Unwrap<Statement>(info.This());
if (!stmt->returns_data) return ThrowTypeError("The expand() method is only for statements that return data");
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
REQUIRE_STATEMENT_NOT_LOCKED(stmt);
bool use = true;
if (info.Length() != 0) { REQUIRE_ARGUMENT_BOOLEAN(first, use); }
stmt->mode = use ? Data::EXPAND : stmt->mode == Data::EXPAND ? Data::FLAT : stmt->mode;
info.GetReturnValue().Set(info.This());
}
NODE_METHOD(JS_raw) {
Statement* stmt = Unwrap<Statement>(info.This());
if (!stmt->returns_data) return ThrowTypeError("The raw() method is only for statements that return data");
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
REQUIRE_STATEMENT_NOT_LOCKED(stmt);
bool use = true;
if (info.Length() != 0) { REQUIRE_ARGUMENT_BOOLEAN(first, use); }
stmt->mode = use ? Data::RAW : stmt->mode == Data::RAW ? Data::FLAT : stmt->mode;
info.GetReturnValue().Set(info.This());
}
NODE_METHOD(JS_safeIntegers) {
Statement* stmt = Unwrap<Statement>(info.This());
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
REQUIRE_STATEMENT_NOT_LOCKED(stmt);
if (info.Length() == 0) stmt->safe_ints = true;
else { REQUIRE_ARGUMENT_BOOLEAN(first, stmt->safe_ints); }
info.GetReturnValue().Set(info.This());
}
NODE_METHOD(JS_columns) {
Statement* stmt = Unwrap<Statement>(info.This());
if (!stmt->returns_data) return ThrowTypeError("The columns() method is only for statements that return data");
REQUIRE_DATABASE_OPEN(stmt->db->GetState());
REQUIRE_DATABASE_NOT_BUSY(stmt->db->GetState());
Addon* addon = stmt->db->GetAddon();
UseIsolate;
UseContext;
int column_count = sqlite3_column_count(stmt->handle);
v8::Local<v8::Array> columns = v8::Array::New(isolate);
v8::Local<v8::String> name = CS::Get(isolate, addon->cs.name);
v8::Local<v8::String> columnName = CS::Get(isolate, addon->cs.column);
v8::Local<v8::String> tableName = CS::Get(isolate, addon->cs.table);
v8::Local<v8::String> databaseName = CS::Get(isolate, addon->cs.database);
v8::Local<v8::String> typeName = CS::Get(isolate, addon->cs.type);
for (int i=0; i<column_count; ++i) {
v8::Local<v8::Object> column = v8::Object::New(isolate);
column->Set(ctx, name,
InternalizedFromUtf8OrNull(isolate, sqlite3_column_name(stmt->handle, i), -1)
).FromJust();
column->Set(ctx, columnName,
InternalizedFromUtf8OrNull(isolate, sqlite3_column_origin_name(stmt->handle, i), -1)
).FromJust();
column->Set(ctx, tableName,
InternalizedFromUtf8OrNull(isolate, sqlite3_column_table_name(stmt->handle, i), -1)
).FromJust();
column->Set(ctx, databaseName,
InternalizedFromUtf8OrNull(isolate, sqlite3_column_database_name(stmt->handle, i), -1)
).FromJust();
column->Set(ctx, typeName,
InternalizedFromUtf8OrNull(isolate, sqlite3_column_decltype(stmt->handle, i), -1)
).FromJust();
columns->Set(ctx, i, column).FromJust();
}
info.GetReturnValue().Set(columns);
}
Database* const db;
sqlite3_stmt* const handle;
Extras* const extras;
bool alive;
bool locked;
bool bound;
bool has_bind_map;
bool safe_ints;
char mode;
const bool returns_data;
};

View File

@ -1,67 +0,0 @@
class BindMap {
public:
// This class represents a mapping between a parameter name and its
// associated parameter index in a prepared statement.
class Pair {
friend class BindMap;
public:
inline int GetIndex() {
return index;
}
inline v8::Local<v8::String> GetName(v8::Isolate* isolate) {
return v8::Local<v8::String>::New(isolate, name);
}
private:
explicit Pair(v8::Isolate* isolate, const char* _name, int _index)
: name(isolate, InternalizedFromUtf8(isolate, _name, -1)), index(_index) {}
explicit Pair(v8::Isolate* isolate, Pair* pair)
: name(isolate, pair->name), index(pair->index) {}
const CopyablePersistent<v8::String> name;
const int index;
};
explicit BindMap(char _) {
assert(_ == 0);
pairs = NULL;
capacity = 0;
length = 0;
}
~BindMap() {
while (length) pairs[--length].~Pair();
FREE_ARRAY<Pair>(pairs);
}
inline Pair* GetPairs() { return pairs; }
inline int GetSize() { return length; }
// Adds a pair to the bind map, expanding the capacity if necessary.
void Add(v8::Isolate* isolate, const char* name, int index) {
assert(name != NULL);
if (capacity == length) Grow(isolate);
new (pairs + length++) Pair(isolate, name, index);
}
private:
void Grow(v8::Isolate* isolate) {
assert(capacity == length);
capacity = (capacity << 1) | 2;
Pair* new_pairs = ALLOC_ARRAY<Pair>(capacity);
for (int i=0; i<length; ++i) {
new (new_pairs + i) Pair(isolate, pairs + i);
pairs[i].~Pair();
}
FREE_ARRAY<Pair>(pairs);
pairs = new_pairs;
}
Pair* pairs;
int capacity;
int length;
};

View File

@ -1,189 +0,0 @@
class Binder {
public:
explicit Binder(sqlite3_stmt* _handle) {
handle = _handle;
param_count = sqlite3_bind_parameter_count(_handle);
anon_index = 0;
success = true;
}
bool Bind(NODE_ARGUMENTS info, int argc, Statement* stmt) {
assert(anon_index == 0);
Result result = BindArgs(info, argc, stmt);
if (success && result.count != param_count) {
if (result.count < param_count) {
if (!result.bound_object && stmt->GetBindMap(OnlyIsolate)->GetSize()) {
Fail(ThrowTypeError, "Missing named parameters");
} else {
Fail(ThrowRangeError, "Too few parameter values were provided");
}
} else {
Fail(ThrowRangeError, "Too many parameter values were provided");
}
}
return success;
}
private:
struct Result {
int count;
bool bound_object;
};
static bool IsPlainObject(v8::Isolate* isolate, v8::Local<v8::Object> obj) {
v8::Local<v8::Value> proto = obj->GetPrototype();
v8::Local<v8::Context> ctx = obj->CreationContext();
ctx->Enter();
v8::Local<v8::Value> baseProto = v8::Object::New(isolate)->GetPrototype();
ctx->Exit();
return proto->StrictEquals(baseProto) || proto->StrictEquals(v8::Null(isolate));
}
void Fail(void (*Throw)(const char* _), const char* message) {
assert(success == true);
assert((Throw == NULL) == (message == NULL));
assert(Throw == ThrowError || Throw == ThrowTypeError || Throw == ThrowRangeError || Throw == NULL);
if (Throw) Throw(message);
success = false;
}
int NextAnonIndex() {
while (sqlite3_bind_parameter_name(handle, ++anon_index) != NULL) {}
return anon_index;
}
// Binds the value at the given index or throws an appropriate error.
void BindValue(v8::Isolate* isolate, v8::Local<v8::Value> value, int index) {
int status = Data::BindValueFromJS(isolate, handle, index, value);
if (status != SQLITE_OK) {
switch (status) {
case -1:
return Fail(ThrowTypeError, "SQLite3 can only bind numbers, strings, bigints, buffers, and null");
case SQLITE_TOOBIG:
return Fail(ThrowRangeError, "The bound string, buffer, or bigint is too big");
case SQLITE_RANGE:
return Fail(ThrowRangeError, "Too many parameter values were provided");
case SQLITE_NOMEM:
return Fail(ThrowError, "Out of memory");
default:
return Fail(ThrowError, "An unexpected error occured while trying to bind parameters");
}
assert(false);
}
}
// Binds each value in the array or throws an appropriate error.
// The number of successfully bound parameters is returned.
int BindArray(v8::Isolate* isolate, v8::Local<v8::Array> arr) {
UseContext;
uint32_t length = arr->Length();
if (length > INT_MAX) {
Fail(ThrowRangeError, "Too many parameter values were provided");
return 0;
}
int len = static_cast<int>(length);
for (int i=0; i<len; ++i) {
v8::MaybeLocal<v8::Value> maybeValue = arr->Get(ctx, i);
if (maybeValue.IsEmpty()) {
Fail(NULL, NULL);
return i;
}
BindValue(isolate, maybeValue.ToLocalChecked(), NextAnonIndex());
if (!success) {
return i;
}
}
return len;
}
// Binds all named parameters using the values found in the given object.
// The number of successfully bound parameters is returned.
// If a named parameter is missing from the object, an error is thrown.
// This should only be invoked once per instance.
int BindObject(v8::Isolate* isolate, v8::Local<v8::Object> obj, Statement* stmt) {
UseContext;
BindMap* bind_map = stmt->GetBindMap(isolate);
BindMap::Pair* pairs = bind_map->GetPairs();
int len = bind_map->GetSize();
for (int i=0; i<len; ++i) {
v8::Local<v8::String> key = pairs[i].GetName(isolate);
// Check if the named parameter was provided.
v8::Maybe<bool> has_property = obj->HasOwnProperty(ctx, key);
if (has_property.IsNothing()) {
Fail(NULL, NULL);
return i;
}
if (!has_property.FromJust()) {
v8::String::Utf8Value param_name(isolate, key);
Fail(ThrowRangeError, CONCAT("Missing named parameter \"", *param_name, "\"").c_str());
return i;
}
// Get the current property value.
v8::MaybeLocal<v8::Value> maybeValue = obj->Get(ctx, key);
if (maybeValue.IsEmpty()) {
Fail(NULL, NULL);
return i;
}
BindValue(isolate, maybeValue.ToLocalChecked(), pairs[i].GetIndex());
if (!success) {
return i;
}
}
return len;
}
// Binds all parameters using the values found in the arguments object.
// Anonymous parameter values can be directly in the arguments object or in an Array.
// Named parameter values can be provided in a plain Object argument.
// Only one plain Object argument may be provided.
// If an error occurs, an appropriate error is thrown.
// The return value is a struct indicating how many parameters were successfully bound
// and whether or not it tried to bind an object.
Result BindArgs(NODE_ARGUMENTS info, int argc, Statement* stmt) {
UseIsolate;
int count = 0;
bool bound_object = false;
for (int i=0; i<argc; ++i) {
v8::Local<v8::Value> arg = info[i];
if (arg->IsArray()) {
count += BindArray(isolate, v8::Local<v8::Array>::Cast(arg));
if (!success) break;
continue;
}
if (arg->IsObject() && !node::Buffer::HasInstance(arg)) {
v8::Local<v8::Object> obj = v8::Local<v8::Object>::Cast(arg);
if (IsPlainObject(isolate, obj)) {
if (bound_object) {
Fail(ThrowTypeError, "You cannot specify named parameters in two different objects");
break;
}
bound_object = true;
count += BindObject(isolate, obj, stmt);
if (!success) break;
continue;
}
}
BindValue(isolate, arg, NextAnonIndex());
if (!success) break;
count += 1;
}
return { count, bound_object };
}
sqlite3_stmt* handle;
int param_count;
int anon_index; // This value should only be used by NextAnonIndex()
bool success; // This value should only be set by Fail()
};

View File

@ -1,150 +0,0 @@
class CS {
public:
static inline v8::Local<v8::String> Get(v8::Isolate* isolate, CopyablePersistent<v8::String>& constant) {
return v8::Local<v8::String>::New(isolate, constant);
}
v8::Local<v8::String> Code(v8::Isolate* isolate, int code) {
auto element = codes.find(code);
if (element != codes.end()) return v8::Local<v8::String>::New(isolate, element->second);
return StringFromUtf8(isolate, CONCAT("UNKNOWN_SQLITE_ERROR_", std::to_string(code).c_str(), "").c_str(), -1);
}
explicit CS(v8::Isolate* isolate) {
SetString(isolate, database, "database");
SetString(isolate, reader, "reader");
SetString(isolate, source, "source");
SetString(isolate, memory, "memory");
SetString(isolate, readonly, "readonly");
SetString(isolate, name, "name");
SetString(isolate, next, "next");
SetString(isolate, length, "length");
SetString(isolate, done, "done");
SetString(isolate, value, "value");
SetString(isolate, changes, "changes");
SetString(isolate, lastInsertRowid, "lastInsertRowid");
SetString(isolate, statement, "statement");
SetString(isolate, column, "column");
SetString(isolate, table, "table");
SetString(isolate, type, "type");
SetString(isolate, totalPages, "totalPages");
SetString(isolate, remainingPages, "remainingPages");
SetCode(isolate, SQLITE_OK, "SQLITE_OK");
SetCode(isolate, SQLITE_ERROR, "SQLITE_ERROR");
SetCode(isolate, SQLITE_INTERNAL, "SQLITE_INTERNAL");
SetCode(isolate, SQLITE_PERM, "SQLITE_PERM");
SetCode(isolate, SQLITE_ABORT, "SQLITE_ABORT");
SetCode(isolate, SQLITE_BUSY, "SQLITE_BUSY");
SetCode(isolate, SQLITE_LOCKED, "SQLITE_LOCKED");
SetCode(isolate, SQLITE_NOMEM, "SQLITE_NOMEM");
SetCode(isolate, SQLITE_READONLY, "SQLITE_READONLY");
SetCode(isolate, SQLITE_INTERRUPT, "SQLITE_INTERRUPT");
SetCode(isolate, SQLITE_IOERR, "SQLITE_IOERR");
SetCode(isolate, SQLITE_CORRUPT, "SQLITE_CORRUPT");
SetCode(isolate, SQLITE_NOTFOUND, "SQLITE_NOTFOUND");
SetCode(isolate, SQLITE_FULL, "SQLITE_FULL");
SetCode(isolate, SQLITE_CANTOPEN, "SQLITE_CANTOPEN");
SetCode(isolate, SQLITE_PROTOCOL, "SQLITE_PROTOCOL");
SetCode(isolate, SQLITE_EMPTY, "SQLITE_EMPTY");
SetCode(isolate, SQLITE_SCHEMA, "SQLITE_SCHEMA");
SetCode(isolate, SQLITE_TOOBIG, "SQLITE_TOOBIG");
SetCode(isolate, SQLITE_CONSTRAINT, "SQLITE_CONSTRAINT");
SetCode(isolate, SQLITE_MISMATCH, "SQLITE_MISMATCH");
SetCode(isolate, SQLITE_MISUSE, "SQLITE_MISUSE");
SetCode(isolate, SQLITE_NOLFS, "SQLITE_NOLFS");
SetCode(isolate, SQLITE_AUTH, "SQLITE_AUTH");
SetCode(isolate, SQLITE_FORMAT, "SQLITE_FORMAT");
SetCode(isolate, SQLITE_RANGE, "SQLITE_RANGE");
SetCode(isolate, SQLITE_NOTADB, "SQLITE_NOTADB");
SetCode(isolate, SQLITE_NOTICE, "SQLITE_NOTICE");
SetCode(isolate, SQLITE_WARNING, "SQLITE_WARNING");
SetCode(isolate, SQLITE_ROW, "SQLITE_ROW");
SetCode(isolate, SQLITE_DONE, "SQLITE_DONE");
SetCode(isolate, SQLITE_IOERR_READ, "SQLITE_IOERR_READ");
SetCode(isolate, SQLITE_IOERR_SHORT_READ, "SQLITE_IOERR_SHORT_READ");
SetCode(isolate, SQLITE_IOERR_WRITE, "SQLITE_IOERR_WRITE");
SetCode(isolate, SQLITE_IOERR_FSYNC, "SQLITE_IOERR_FSYNC");
SetCode(isolate, SQLITE_IOERR_DIR_FSYNC, "SQLITE_IOERR_DIR_FSYNC");
SetCode(isolate, SQLITE_IOERR_TRUNCATE, "SQLITE_IOERR_TRUNCATE");
SetCode(isolate, SQLITE_IOERR_FSTAT, "SQLITE_IOERR_FSTAT");
SetCode(isolate, SQLITE_IOERR_UNLOCK, "SQLITE_IOERR_UNLOCK");
SetCode(isolate, SQLITE_IOERR_RDLOCK, "SQLITE_IOERR_RDLOCK");
SetCode(isolate, SQLITE_IOERR_DELETE, "SQLITE_IOERR_DELETE");
SetCode(isolate, SQLITE_IOERR_BLOCKED, "SQLITE_IOERR_BLOCKED");
SetCode(isolate, SQLITE_IOERR_NOMEM, "SQLITE_IOERR_NOMEM");
SetCode(isolate, SQLITE_IOERR_ACCESS, "SQLITE_IOERR_ACCESS");
SetCode(isolate, SQLITE_IOERR_CHECKRESERVEDLOCK, "SQLITE_IOERR_CHECKRESERVEDLOCK");
SetCode(isolate, SQLITE_IOERR_LOCK, "SQLITE_IOERR_LOCK");
SetCode(isolate, SQLITE_IOERR_CLOSE, "SQLITE_IOERR_CLOSE");
SetCode(isolate, SQLITE_IOERR_DIR_CLOSE, "SQLITE_IOERR_DIR_CLOSE");
SetCode(isolate, SQLITE_IOERR_SHMOPEN, "SQLITE_IOERR_SHMOPEN");
SetCode(isolate, SQLITE_IOERR_SHMSIZE, "SQLITE_IOERR_SHMSIZE");
SetCode(isolate, SQLITE_IOERR_SHMLOCK, "SQLITE_IOERR_SHMLOCK");
SetCode(isolate, SQLITE_IOERR_SHMMAP, "SQLITE_IOERR_SHMMAP");
SetCode(isolate, SQLITE_IOERR_SEEK, "SQLITE_IOERR_SEEK");
SetCode(isolate, SQLITE_IOERR_DELETE_NOENT, "SQLITE_IOERR_DELETE_NOENT");
SetCode(isolate, SQLITE_IOERR_MMAP, "SQLITE_IOERR_MMAP");
SetCode(isolate, SQLITE_IOERR_GETTEMPPATH, "SQLITE_IOERR_GETTEMPPATH");
SetCode(isolate, SQLITE_IOERR_CONVPATH, "SQLITE_IOERR_CONVPATH");
SetCode(isolate, SQLITE_IOERR_VNODE, "SQLITE_IOERR_VNODE");
SetCode(isolate, SQLITE_IOERR_AUTH, "SQLITE_IOERR_AUTH");
SetCode(isolate, SQLITE_LOCKED_SHAREDCACHE, "SQLITE_LOCKED_SHAREDCACHE");
SetCode(isolate, SQLITE_BUSY_RECOVERY, "SQLITE_BUSY_RECOVERY");
SetCode(isolate, SQLITE_BUSY_SNAPSHOT, "SQLITE_BUSY_SNAPSHOT");
SetCode(isolate, SQLITE_CANTOPEN_NOTEMPDIR, "SQLITE_CANTOPEN_NOTEMPDIR");
SetCode(isolate, SQLITE_CANTOPEN_ISDIR, "SQLITE_CANTOPEN_ISDIR");
SetCode(isolate, SQLITE_CANTOPEN_FULLPATH, "SQLITE_CANTOPEN_FULLPATH");
SetCode(isolate, SQLITE_CANTOPEN_CONVPATH, "SQLITE_CANTOPEN_CONVPATH");
SetCode(isolate, SQLITE_CORRUPT_VTAB, "SQLITE_CORRUPT_VTAB");
SetCode(isolate, SQLITE_READONLY_RECOVERY, "SQLITE_READONLY_RECOVERY");
SetCode(isolate, SQLITE_READONLY_CANTLOCK, "SQLITE_READONLY_CANTLOCK");
SetCode(isolate, SQLITE_READONLY_ROLLBACK, "SQLITE_READONLY_ROLLBACK");
SetCode(isolate, SQLITE_READONLY_DBMOVED, "SQLITE_READONLY_DBMOVED");
SetCode(isolate, SQLITE_ABORT_ROLLBACK, "SQLITE_ABORT_ROLLBACK");
SetCode(isolate, SQLITE_CONSTRAINT_CHECK, "SQLITE_CONSTRAINT_CHECK");
SetCode(isolate, SQLITE_CONSTRAINT_COMMITHOOK, "SQLITE_CONSTRAINT_COMMITHOOK");
SetCode(isolate, SQLITE_CONSTRAINT_FOREIGNKEY, "SQLITE_CONSTRAINT_FOREIGNKEY");
SetCode(isolate, SQLITE_CONSTRAINT_FUNCTION, "SQLITE_CONSTRAINT_FUNCTION");
SetCode(isolate, SQLITE_CONSTRAINT_NOTNULL, "SQLITE_CONSTRAINT_NOTNULL");
SetCode(isolate, SQLITE_CONSTRAINT_PRIMARYKEY, "SQLITE_CONSTRAINT_PRIMARYKEY");
SetCode(isolate, SQLITE_CONSTRAINT_TRIGGER, "SQLITE_CONSTRAINT_TRIGGER");
SetCode(isolate, SQLITE_CONSTRAINT_UNIQUE, "SQLITE_CONSTRAINT_UNIQUE");
SetCode(isolate, SQLITE_CONSTRAINT_VTAB, "SQLITE_CONSTRAINT_VTAB");
SetCode(isolate, SQLITE_CONSTRAINT_ROWID, "SQLITE_CONSTRAINT_ROWID");
SetCode(isolate, SQLITE_NOTICE_RECOVER_WAL, "SQLITE_NOTICE_RECOVER_WAL");
SetCode(isolate, SQLITE_NOTICE_RECOVER_ROLLBACK, "SQLITE_NOTICE_RECOVER_ROLLBACK");
SetCode(isolate, SQLITE_WARNING_AUTOINDEX, "SQLITE_WARNING_AUTOINDEX");
SetCode(isolate, SQLITE_AUTH_USER, "SQLITE_AUTH_USER");
SetCode(isolate, SQLITE_OK_LOAD_PERMANENTLY, "SQLITE_OK_LOAD_PERMANENTLY");
}
CopyablePersistent<v8::String> database;
CopyablePersistent<v8::String> reader;
CopyablePersistent<v8::String> source;
CopyablePersistent<v8::String> memory;
CopyablePersistent<v8::String> readonly;
CopyablePersistent<v8::String> name;
CopyablePersistent<v8::String> next;
CopyablePersistent<v8::String> length;
CopyablePersistent<v8::String> done;
CopyablePersistent<v8::String> value;
CopyablePersistent<v8::String> changes;
CopyablePersistent<v8::String> lastInsertRowid;
CopyablePersistent<v8::String> statement;
CopyablePersistent<v8::String> column;
CopyablePersistent<v8::String> table;
CopyablePersistent<v8::String> type;
CopyablePersistent<v8::String> totalPages;
CopyablePersistent<v8::String> remainingPages;
private:
static void SetString(v8::Isolate* isolate, CopyablePersistent<v8::String>& constant, const char* str) {
constant.Reset(isolate, InternalizedFromLatin1(isolate, str));
}
void SetCode(v8::Isolate* isolate, int code, const char* str) {
codes.emplace(std::piecewise_construct, std::forward_as_tuple(code), std::forward_as_tuple(isolate, InternalizedFromLatin1(isolate, str)));
}
std::unordered_map<int, CopyablePersistent<v8::String> > codes;
};

View File

@ -1,106 +0,0 @@
class CustomAggregate : public CustomFunction {
public:
explicit CustomAggregate(v8::Isolate* _isolate, Database* _db, v8::Local<v8::Value> _start, v8::Local<v8::Function> _step, v8::Local<v8::Value> _inverse, v8::Local<v8::Value> _result, const char* _name, bool _safe_ints)
: CustomFunction(_isolate, _db, _step, _name, _safe_ints), invoke_result(_result->IsFunction()), invoke_start(_start->IsFunction()), inverse(_isolate, _inverse->IsFunction() ? v8::Local<v8::Function>::Cast(_inverse) : v8::Local<v8::Function>()), result(_isolate, _result->IsFunction() ? v8::Local<v8::Function>::Cast(_result) : v8::Local<v8::Function>()), start(_isolate, _start) {}
static void xStep(sqlite3_context* invocation, int argc, sqlite3_value** argv) {
xStepBase(invocation, argc, argv, &CustomAggregate::fn);
}
static void xInverse(sqlite3_context* invocation, int argc, sqlite3_value** argv) {
xStepBase(invocation, argc, argv, &CustomAggregate::inverse);
}
static void xValue(sqlite3_context* invocation) {
xValueBase(invocation, false);
}
static void xFinal(sqlite3_context* invocation) {
xValueBase(invocation, true);
}
private:
static inline void xStepBase(sqlite3_context* invocation, int argc, sqlite3_value** argv, const CopyablePersistent<v8::Function> CustomAggregate::*ptrtm) {
AGGREGATE_START();
v8::Local<v8::Value> args_fast[5];
v8::Local<v8::Value>* args = argc <= 4 ? args_fast : ALLOC_ARRAY<v8::Local<v8::Value>>(argc + 1);
args[0] = v8::Local<v8::Value>::New(isolate, acc->value);
if (argc != 0) Data::GetArgumentsJS(isolate, args + 1, argv, argc, self->safe_ints);
v8::MaybeLocal<v8::Value> maybe_return_value = v8::Local<v8::Function>::New(isolate, self->*ptrtm)->Call(OnlyContext, v8::Undefined(isolate), argc + 1, args);
if (args != args_fast) delete[] args;
if (maybe_return_value.IsEmpty()) {
self->PropagateJSError(invocation);
} else {
v8::Local<v8::Value> return_value = maybe_return_value.ToLocalChecked();
if (!return_value->IsUndefined()) acc->value.Reset(isolate, return_value);
}
}
static inline void xValueBase(sqlite3_context* invocation, bool is_final) {
AGGREGATE_START();
if (!is_final) {
acc->is_window = true;
} else if (acc->is_window) {
DestroyAccumulator(invocation);
return;
}
v8::Local<v8::Value> result = v8::Local<v8::Value>::New(isolate, acc->value);
if (self->invoke_result) {
v8::MaybeLocal<v8::Value> maybe_result = v8::Local<v8::Function>::New(isolate, self->result)->Call(OnlyContext, v8::Undefined(isolate), 1, &result);
if (maybe_result.IsEmpty()) {
self->PropagateJSError(invocation);
return;
}
result = maybe_result.ToLocalChecked();
}
Data::ResultValueFromJS(isolate, invocation, result, self);
if (is_final) DestroyAccumulator(invocation);
}
struct Accumulator { public:
CopyablePersistent<v8::Value> value;
bool initialized;
bool is_window;
}
Accumulator* GetAccumulator(sqlite3_context* invocation) {
Accumulator* acc = static_cast<Accumulator*>(sqlite3_aggregate_context(invocation, sizeof(Accumulator)));
if (!acc->initialized) {
assert(acc->value.IsEmpty());
acc->initialized = true;
if (invoke_start) {
v8::MaybeLocal<v8::Value> maybe_seed = v8::Local<v8::Function>::Cast(v8::Local<v8::Value>::New(isolate, start))->Call(OnlyContext, v8::Undefined(isolate), 0, NULL);
if (maybe_seed.IsEmpty()) PropagateJSError(invocation);
else acc->value.Reset(isolate, maybe_seed.ToLocalChecked());
} else {
assert(!start.IsEmpty());
acc->value.Reset(isolate, start);
}
}
return acc;
}
static void DestroyAccumulator(sqlite3_context* invocation) {
Accumulator* acc = static_cast<Accumulator*>(sqlite3_aggregate_context(invocation, sizeof(Accumulator)));
assert(acc->initialized);
acc->value.Reset();
}
void PropagateJSError(sqlite3_context* invocation) {
DestroyAccumulator(invocation);
CustomFunction::PropagateJSError(invocation);
}
const bool invoke_result;
const bool invoke_start;
const CopyablePersistent<v8::Function> inverse;
const CopyablePersistent<v8::Function> result;
const CopyablePersistent<v8::Value> start;
};

View File

@ -1,52 +0,0 @@
class CustomFunction {
public:
explicit CustomFunction(v8::Isolate* _isolate, Database* _db, v8::Local<v8::Function> _fn, const char* _name, bool _safe_ints)
: name(COPY(_name)), db(_db), isolate(_isolate), fn(_isolate, _fn), safe_ints(_safe_ints) {}
virtual ~CustomFunction() { delete[] name; }
static void xDestroy(void* self) {
delete static_cast<CustomFunction*>(self);
}
static void xFunc(sqlite3_context* invocation, int argc, sqlite3_value** argv) {
FUNCTION_START();
v8::Local<v8::Value> args_fast[4];
v8::Local<v8::Value>* args = NULL;
if (argc != 0) {
args = argc <= 4 ? args_fast : ALLOC_ARRAY<v8::Local<v8::Value>>(argc);
Data::GetArgumentsJS(isolate, args, argv, argc, self->safe_ints);
}
v8::MaybeLocal<v8::Value> maybe_return_value = v8::Local<v8::Function>::New(isolate, self->fn)->Call(OnlyContext, v8::Undefined(isolate), argc, args);
if (args != args_fast) delete[] args;
if (maybe_return_value.IsEmpty()) self->PropagateJSError(invocation);
else Data::ResultValueFromJS(isolate, invocation, maybe_return_value.ToLocalChecked(), self);
}
void ThrowResultValueError(sqlite3_context* invocation, bool isBigInt) {
if (isBigInt) {
ThrowRangeError(CONCAT("User-defined function ", name, "() returned a bigint that was too big").c_str());
} else {
ThrowTypeError(CONCAT("User-defined function ", name, "() returned an invalid value").c_str());
}
PropagateJSError(invocation);
}
protected:
virtual void PropagateJSError(sqlite3_context* invocation) {
assert(db->GetState()->was_js_error == false);
db->GetState()->was_js_error = true;
sqlite3_result_error(invocation, "", 0);
}
private:
const char* const name;
Database* const db;
protected:
v8::Isolate* const isolate;
const CopyablePersistent<v8::Function> fn;
const bool safe_ints;
};

View File

@ -1,147 +0,0 @@
#define JS_VALUE_TO_SQLITE(to, value, isolate, ...) \
if (value->IsNumber()) { \
return sqlite3_##to##_double( \
__VA_ARGS__, \
v8::Local<v8::Number>::Cast(value)->Value() \
); \
} else if (value->IsBigInt()) { \
bool lossless; \
int64_t v = v8::Local<v8::BigInt>::Cast(value)->Int64Value(&lossless); \
if (lossless) { \
return sqlite3_##to##_int64(__VA_ARGS__, v); \
} \
} else if (value->IsString()) { \
v8::String::Utf8Value utf8( \
isolate, \
v8::Local<v8::String>::Cast(value) \
); \
return sqlite3_##to##_text( \
__VA_ARGS__, \
*utf8, \
utf8.length(), \
SQLITE_TRANSIENT \
); \
} else if (node::Buffer::HasInstance(value)) { \
return sqlite3_##to##_blob( \
__VA_ARGS__, \
node::Buffer::Data(value), \
node::Buffer::Length(value), \
SQLITE_TRANSIENT \
); \
} else if (value->IsNull() || value->IsUndefined()) { \
return sqlite3_##to##_null(__VA_ARGS__); \
}
#define SQLITE_VALUE_TO_JS(from, isolate, safe_ints, ...) \
switch (sqlite3_##from##_type(__VA_ARGS__)) { \
case SQLITE_INTEGER: \
if (safe_ints) { \
return v8::BigInt::New( \
isolate, \
sqlite3_##from##_int64(__VA_ARGS__) \
); \
} \
case SQLITE_FLOAT: \
return v8::Number::New( \
isolate, \
sqlite3_##from##_double(__VA_ARGS__) \
); \
case SQLITE_TEXT: \
return StringFromUtf8( \
isolate, \
reinterpret_cast<const char*>(sqlite3_##from##_text(__VA_ARGS__)), \
sqlite3_##from##_bytes(__VA_ARGS__) \
); \
case SQLITE_BLOB: \
return node::Buffer::Copy( \
isolate, \
static_cast<const char*>(sqlite3_##from##_blob(__VA_ARGS__)), \
sqlite3_##from##_bytes(__VA_ARGS__) \
).ToLocalChecked(); \
default: \
assert(sqlite3_##from##_type(__VA_ARGS__) == SQLITE_NULL); \
return v8::Null(isolate); \
} \
assert(false);
namespace Data {
static const char FLAT = 0;
static const char PLUCK = 1;
static const char EXPAND = 2;
static const char RAW = 3;
v8::Local<v8::Value> GetValueJS(v8::Isolate* isolate, sqlite3_stmt* handle, int column, bool safe_ints) {
SQLITE_VALUE_TO_JS(column, isolate, safe_ints, handle, column);
}
v8::Local<v8::Value> GetValueJS(v8::Isolate* isolate, sqlite3_value* value, bool safe_ints) {
SQLITE_VALUE_TO_JS(value, isolate, safe_ints, value);
}
v8::Local<v8::Value> GetFlatRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints) {
v8::Local<v8::Object> row = v8::Object::New(isolate);
int column_count = sqlite3_column_count(handle);
for (int i=0; i<column_count; ++i) {
row->Set(ctx,
InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1),
Data::GetValueJS(isolate, handle, i, safe_ints)).FromJust();
}
return row;
}
v8::Local<v8::Value> GetExpandedRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints) {
v8::Local<v8::Object> row = v8::Object::New(isolate);
int column_count = sqlite3_column_count(handle);
for (int i=0; i<column_count; ++i) {
const char* table_raw = sqlite3_column_table_name(handle, i);
v8::Local<v8::String> table = InternalizedFromUtf8(isolate, table_raw == NULL ? "$" : table_raw, -1);
v8::Local<v8::String> column = InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1);
v8::Local<v8::Value> value = Data::GetValueJS(isolate, handle, i, safe_ints);
if (row->HasOwnProperty(ctx, table).FromJust()) {
v8::Local<v8::Object>::Cast(row->Get(ctx, table).ToLocalChecked())->Set(ctx, column, value).FromJust();
} else {
v8::Local<v8::Object> nested = v8::Object::New(isolate);
row->Set(ctx, table, nested).FromJust();
nested->Set(ctx, column, value).FromJust();
}
}
return row;
}
v8::Local<v8::Value> GetRawRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints) {
v8::Local<v8::Array> row = v8::Array::New(isolate);
int column_count = sqlite3_column_count(handle);
for (int i=0; i<column_count; ++i) {
row->Set(ctx, i, Data::GetValueJS(isolate, handle, i, safe_ints)).FromJust();
}
return row;
}
v8::Local<v8::Value> GetRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints, char mode) {
if (mode == FLAT) return GetFlatRowJS(isolate, ctx, handle, safe_ints);
if (mode == PLUCK) return GetValueJS(isolate, handle, 0, safe_ints);
if (mode == EXPAND) return GetExpandedRowJS(isolate, ctx, handle, safe_ints);
if (mode == RAW) return GetRawRowJS(isolate, ctx, handle, safe_ints);
assert(false);
return v8::Local<v8::Value>();
}
void GetArgumentsJS(v8::Isolate* isolate, v8::Local<v8::Value>* out, sqlite3_value** values, int argument_count, bool safe_ints) {
assert(argument_count > 0);
for (int i=0; i<argument_count; ++i) {
out[i] = Data::GetValueJS(isolate, values[i], safe_ints);
}
}
int BindValueFromJS(v8::Isolate* isolate, sqlite3_stmt* handle, int index, v8::Local<v8::Value> value) {
JS_VALUE_TO_SQLITE(bind, value, isolate, handle, index);
return value->IsBigInt() ? SQLITE_TOOBIG : -1;
}
void ResultValueFromJS(v8::Isolate* isolate, sqlite3_context* invocation, v8::Local<v8::Value> value, CustomFunction* function) {
JS_VALUE_TO_SQLITE(result, value, isolate, invocation);
function->ThrowResultValueError(invocation, value->IsBigInt());
}
}

View File

@ -1,175 +0,0 @@
#define NODE_ARGUMENTS const v8::FunctionCallbackInfo<v8::Value>&
#define NODE_ARGUMENTS_POINTER const v8::FunctionCallbackInfo<v8::Value>*
#define NODE_METHOD(name) static void name(NODE_ARGUMENTS info)
#define NODE_GETTER(name) static void name(v8::Local<v8::String> _, const v8::PropertyCallbackInfo<v8::Value>& info)
#define INIT(name) static v8::Local<v8::Function> name(v8::Isolate* isolate, v8::Local<v8::External> data)
#define EasyIsolate v8::Isolate* isolate = v8::Isolate::GetCurrent()
#define OnlyIsolate info.GetIsolate()
#define OnlyContext isolate->GetCurrentContext()
#define OnlyAddon static_cast<Addon*>(v8::Local<v8::External>::Cast(info.Data())->Value())
#define UseIsolate v8::Isolate* isolate = OnlyIsolate
#define UseContext v8::Local<v8::Context> ctx = OnlyContext
#define UseAddon Addon* addon = OnlyAddon
#define Unwrap node::ObjectWrap::Unwrap
inline v8::Local<v8::String> StringFromUtf8(v8::Isolate* isolate, const char* data, int length) {
return v8::String::NewFromUtf8(isolate, data, v8::NewStringType::kNormal, length).ToLocalChecked();
}
inline v8::Local<v8::String> InternalizedFromUtf8(v8::Isolate* isolate, const char* data, int length) {
return v8::String::NewFromUtf8(isolate, data, v8::NewStringType::kInternalized, length).ToLocalChecked();
}
inline v8::Local<v8::Value> InternalizedFromUtf8OrNull(v8::Isolate* isolate, const char* data, int length) {
if (data == NULL) return v8::Null(isolate);
return InternalizedFromUtf8(isolate, data, length);
}
inline v8::Local<v8::String> InternalizedFromLatin1(v8::Isolate* isolate, const char* str) {
return v8::String::NewFromOneByte(isolate, reinterpret_cast<const uint8_t*>(str), v8::NewStringType::kInternalized).ToLocalChecked();
}
#hdr
template <class T> using CopyablePersistent = v8::Persistent<T, v8::CopyablePersistentTraits<T>>;
#end
inline void SetFrozen(v8::Isolate* isolate, v8::Local<v8::Context> ctx, v8::Local<v8::Object> obj, CopyablePersistent<v8::String>& key, v8::Local<v8::Value> value) {
obj->DefineOwnProperty(ctx, CS::Get(isolate, key), value, static_cast<v8::PropertyAttribute>(v8::DontDelete | v8::ReadOnly)).FromJust();
}
void ThrowError(const char* message) { EasyIsolate; isolate->ThrowException(v8::Exception::Error(StringFromUtf8(isolate, message, -1))); }
void ThrowTypeError(const char* message) { EasyIsolate; isolate->ThrowException(v8::Exception::TypeError(StringFromUtf8(isolate, message, -1))); }
void ThrowRangeError(const char* message) { EasyIsolate; isolate->ThrowException(v8::Exception::RangeError(StringFromUtf8(isolate, message, -1))); }
#define REQUIRE_ARGUMENT_ANY(at, var) \
if (info.Length() <= (at())) \
return ThrowTypeError("Expected a "#at" argument"); \
var = info[at()]
#define _REQUIRE_ARGUMENT(at, var, Type, message, ...) \
if (info.Length() <= (at()) || !info[at()]->Is##Type()) \
return ThrowTypeError("Expected "#at" argument to be "#message); \
var = v8::Local<v8::Type>::Cast(info[at()])__VA_ARGS__
#define REQUIRE_ARGUMENT_INT32(at, var) \
_REQUIRE_ARGUMENT(at, var, Int32, a 32-bit signed integer, ->Value())
#define REQUIRE_ARGUMENT_BOOLEAN(at, var) \
_REQUIRE_ARGUMENT(at, var, Boolean, a boolean, ->Value())
#define REQUIRE_ARGUMENT_STRING(at, var) \
_REQUIRE_ARGUMENT(at, var, String, a string)
#define REQUIRE_ARGUMENT_OBJECT(at, var) \
_REQUIRE_ARGUMENT(at, var, Object, an object)
#define REQUIRE_ARGUMENT_FUNCTION(at, var) \
_REQUIRE_ARGUMENT(at, var, Function, a function)
#define REQUIRE_DATABASE_OPEN(db) \
if (!db->open) \
return ThrowTypeError("The database connection is not open")
#define REQUIRE_DATABASE_NOT_BUSY(db) \
if (db->busy) \
return ThrowTypeError("This database connection is busy executing a query")
#define REQUIRE_DATABASE_NO_ITERATORS(db) \
if (db->iterators) \
return ThrowTypeError("This database connection is busy executing a query")
#define REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db) \
if (!db->unsafe_mode) { \
REQUIRE_DATABASE_NO_ITERATORS(db); \
} ((void)0)
#define REQUIRE_STATEMENT_NOT_LOCKED(stmt) \
if (stmt->locked) \
return ThrowTypeError("This statement is busy executing a query")
#define first() 0
#define second() 1
#define third() 2
#define fourth() 3
#define fifth() 4
#define sixth() 5
#define seventh() 6
#define eighth() 7
#define ninth() 8
#define tenth() 9
// Returns a std:string of the concatenation of 3 well-formed C-strings.
std::string CONCAT(const char* a, const char* b, const char* c) {
std::string result(a);
result += b;
result += c;
return result;
}
// Returns a copy of a well-formed C-string.
const char* COPY(const char* source) {
size_t bytes = strlen(source) + 1;
char* dest = new char[bytes];
memcpy(dest, source, bytes);
return dest;
}
// Determines whether to skip the given character at the start of an SQL string.
inline bool IS_SKIPPED(char c) {
return c == ' ' || c == ';' || (c >= '\t' && c <= '\r');
}
// Allocates an empty array, without calling constructors/initializers.
template<class T> inline T* ALLOC_ARRAY(size_t count) {
return static_cast<T*>(::operator new[](count * sizeof(T)));
}
// Deallocates an array, without calling destructors.
template<class T> inline void FREE_ARRAY(T* array_pointer) {
::operator delete[](array_pointer);
}
v8::Local<v8::FunctionTemplate> NewConstructorTemplate(
v8::Isolate* isolate,
v8::Local<v8::External> data,
v8::FunctionCallback func,
const char* name
) {
v8::Local<v8::FunctionTemplate> t = v8::FunctionTemplate::New(isolate, func, data);
t->InstanceTemplate()->SetInternalFieldCount(1);
t->SetClassName(InternalizedFromLatin1(isolate, name));
return t;
}
void SetPrototypeMethod(
v8::Isolate* isolate,
v8::Local<v8::External> data,
v8::Local<v8::FunctionTemplate> recv,
const char* name,
v8::FunctionCallback func
) {
v8::HandleScope scope(isolate);
recv->PrototypeTemplate()->Set(
InternalizedFromLatin1(isolate, name),
v8::FunctionTemplate::New(isolate, func, data, v8::Signature::New(isolate, recv))
);
}
void SetPrototypeSymbolMethod(
v8::Isolate* isolate,
v8::Local<v8::External> data,
v8::Local<v8::FunctionTemplate> recv,
v8::Local<v8::Symbol> symbol,
v8::FunctionCallback func
) {
v8::HandleScope scope(isolate);
recv->PrototypeTemplate()->Set(
symbol,
v8::FunctionTemplate::New(isolate, func, data, v8::Signature::New(isolate, recv))
);
}
void SetPrototypeGetter(
v8::Isolate* isolate,
v8::Local<v8::External> data,
v8::Local<v8::FunctionTemplate> recv,
const char* name,
v8::AccessorGetterCallback func
) {
v8::HandleScope scope(isolate);
recv->InstanceTemplate()->SetAccessor(
InternalizedFromLatin1(isolate, name),
func,
0,
data,
v8::AccessControl::DEFAULT,
v8::PropertyAttribute::None,
v8::AccessorSignature::New(isolate, recv)
);
}

View File

@ -1,72 +0,0 @@
#define STATEMENT_BIND(handle) \
Binder binder(handle); \
if (!binder.Bind(info, info.Length(), stmt)) { \
sqlite3_clear_bindings(handle); \
return; \
} ((void)0)
#define STATEMENT_THROW_LOGIC() \
db->ThrowDatabaseError(); \
if (!bound) { sqlite3_clear_bindings(handle); } \
return
#define STATEMENT_RETURN_LOGIC(return_value) \
info.GetReturnValue().Set(return_value); \
if (!bound) { sqlite3_clear_bindings(handle); } \
return
#define STATEMENT_START_LOGIC(RETURNS_DATA_CHECK, MUTATE_CHECK) \
Statement* stmt = Unwrap<Statement>(info.This()); \
RETURNS_DATA_CHECK(); \
sqlite3_stmt* handle = stmt->handle; \
Database* db = stmt->db; \
REQUIRE_DATABASE_OPEN(db->GetState()); \
REQUIRE_DATABASE_NOT_BUSY(db->GetState()); \
MUTATE_CHECK(); \
const bool bound = stmt->bound; \
if (!bound) { \
STATEMENT_BIND(handle); \
} else if (info.Length() > 0) { \
return ThrowTypeError("This statement already has bound parameters"); \
} ((void)0)
#define STATEMENT_THROW() db->GetState()->busy = false; STATEMENT_THROW_LOGIC()
#define STATEMENT_RETURN(x) db->GetState()->busy = false; STATEMENT_RETURN_LOGIC(x)
#define STATEMENT_START(x, y) \
STATEMENT_START_LOGIC(x, y); \
db->GetState()->busy = true; \
UseIsolate; \
if (db->Log(isolate, handle)) { \
STATEMENT_THROW(); \
} ((void)0)
#define DOES_NOT_MUTATE() REQUIRE_STATEMENT_NOT_LOCKED(stmt)
#define DOES_MUTATE() \
assert(!stmt->locked); \
REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db->GetState())
#define DOES_ADD_ITERATOR() \
DOES_NOT_MUTATE(); \
if (db->GetState()->iterators == USHRT_MAX) \
return ThrowRangeError("Too many active database iterators")
#define REQUIRE_STATEMENT_RETURNS_DATA() \
if (!stmt->returns_data) \
return ThrowTypeError("This statement does not return data. Use run() instead")
#define REQUIRE_STATEMENT_DOESNT_RETURN_DATA() \
if (stmt->returns_data) \
return ThrowTypeError("This statement returns data. Use get(), all(), or iterate() instead")
#define _FUNCTION_START(type) \
type* self = static_cast<type*>(sqlite3_user_data(invocation)); \
v8::Isolate* isolate = self->isolate; \
v8::HandleScope scope(isolate)
#define FUNCTION_START() \
_FUNCTION_START(CustomFunction)
#define AGGREGATE_START() \
_FUNCTION_START(CustomAggregate); \
Accumulator* acc = self->GetAccumulator(invocation); \
if (acc->value.IsEmpty()) return

View File

@ -1,5 +1,6 @@
'use strict';
const { existsSync } = require('fs');
const fs = require('fs');
const path = require('path');
const Database = require('../.');
describe('new Database()', function () {
@ -31,10 +32,10 @@ describe('new Database()', function () {
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync('')).to.be.false;
expect(existsSync('null')).to.be.false;
expect(existsSync('undefined')).to.be.false;
expect(existsSync('[object Object]')).to.be.false;
expect(fs.existsSync('')).to.be.false;
expect(fs.existsSync('null')).to.be.false;
expect(fs.existsSync('undefined')).to.be.false;
expect(fs.existsSync('[object Object]')).to.be.false;
db.close();
}
});
@ -45,49 +46,49 @@ describe('new Database()', function () {
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(':memory:')).to.be.false;
expect(fs.existsSync(':memory:')).to.be.false;
});
it('should allow disk-bound databases to be created', function () {
expect(existsSync(util.next())).to.be.false;
const db = this.db = Database(util.current());
expect(fs.existsSync(util.next())).to.be.false;
const db = this.db = new Database(util.current());
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
});
it('should allow readonly database connections to be created', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
expect(() => (this.db = new Database(util.current(), { readonly: true }))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
(new Database(util.current())).close();
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
const db = this.db = new Database(util.current(), { readonly: true });
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.true;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
});
it('should not allow the "readonly" option for in-memory databases', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
expect(() => (this.db = new Database(':memory:', { readonly: true }))).to.throw(TypeError);
expect(() => (this.db = new Database('', { readonly: true }))).to.throw(TypeError);
expect(existsSync(util.current())).to.be.false;
expect(fs.existsSync(util.current())).to.be.false;
});
it('should accept the "fileMustExist" option', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
expect(() => (this.db = new Database(util.current(), { fileMustExist: true }))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
(new Database(util.current())).close();
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
const db = this.db = new Database(util.current(), { fileMustExist: true });
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
});
util.itUnix('should accept the "timeout" option', function () {
this.slow(4000);
@ -115,12 +116,38 @@ describe('new Database()', function () {
expect(() => (this.db = new Database(util.current(), { timeout: 75.01 }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.current(), { timeout: 0x80000000 }))).to.throw(RangeError);
});
it('should accept the "nativeBinding" option', function () {
this.slow(500);
const oldBinding = require('bindings')({ bindings: 'better_sqlite3.node', path: true });
const newBinding = path.join(path.dirname(oldBinding), 'test.node');
expect(oldBinding).to.be.a('string');
fs.copyFileSync(oldBinding, newBinding);
const getBinding = db => db[Object.getOwnPropertySymbols(db)[0]].constructor;
let db1;
let db2;
let db3;
try {
db1 = new Database('');
db2 = new Database('', { nativeBinding: oldBinding });
db3 = new Database('', { nativeBinding: newBinding });
expect(db1.open).to.be.true;
expect(db2.open).to.be.true;
expect(db3.open).to.be.true;
expect(getBinding(db1)).to.equal(getBinding(db2));
expect(getBinding(db1)).to.not.equal(getBinding(db3));
expect(getBinding(db2)).to.not.equal(getBinding(db3));
} finally {
if (db1) db1.close();
if (db2) db2.close();
if (db3) db3.close();
}
});
it('should throw an Error if the directory does not exist', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
const filepath = `temp/nonexistent/abcfoobar123/${util.current()}`;
expect(() => (this.db = new Database(filepath))).to.throw(TypeError);
expect(existsSync(filepath)).to.be.false;
expect(existsSync(util.current())).to.be.false;
expect(fs.existsSync(filepath)).to.be.false;
expect(fs.existsSync(util.current())).to.be.false;
});
it('should have a proper prototype chain', function () {
const db = this.db = new Database(util.next());
@ -131,4 +158,29 @@ describe('new Database()', function () {
expect(Database.prototype.close).to.equal(db.close);
expect(Database.prototype).to.equal(Object.getPrototypeOf(db));
});
it('should work properly when called as a function', function () {
const db = this.db = Database(util.next());
expect(db).to.be.an.instanceof(Database);
expect(db.constructor).to.equal(Database);
expect(Database.prototype.close).to.equal(db.close);
expect(Database.prototype).to.equal(Object.getPrototypeOf(db));
});
it('should work properly when subclassed', function () {
class MyDatabase extends Database {
foo() {
return 999;
}
}
const db = this.db = new MyDatabase(util.next());
expect(db).to.be.an.instanceof(Database);
expect(db).to.be.an.instanceof(MyDatabase);
expect(db.constructor).to.equal(MyDatabase);
expect(Database.prototype.close).to.equal(db.close);
expect(MyDatabase.prototype.close).to.equal(db.close);
expect(Database.prototype.foo).to.be.undefined;
expect(MyDatabase.prototype.foo).to.equal(db.foo);
expect(Database.prototype).to.equal(Object.getPrototypeOf(MyDatabase.prototype));
expect(MyDatabase.prototype).to.equal(Object.getPrototypeOf(db));
expect(db.foo()).to.equal(999);
});
});

View File

@ -30,6 +30,7 @@ describe('Database#close()', function () {
expect(() => this.db.pragma('cache_size')).to.throw(TypeError);
expect(() => this.db.function('foo', () => {})).to.throw(TypeError);
expect(() => this.db.aggregate('foo', { step: () => {} })).to.throw(TypeError);
expect(() => this.db.table('foo', () => {})).to.throw(TypeError);
});
it('should prevent any existing statements from running', function () {
this.db.prepare('CREATE TABLE people (name TEXT)').run();

View File

@ -9,11 +9,12 @@ describe('Database#prepare()', function () {
this.db.close();
});
function assertStmt(stmt, source, db, reader) {
function assertStmt(stmt, source, db, reader, readonly) {
expect(stmt.source).to.equal(source);
expect(stmt.constructor.name).to.equal('Statement');
expect(stmt.database).to.equal(db);
expect(stmt.reader).to.equal(reader);
expect(stmt.readonly).to.equal(readonly);
expect(() => new stmt.constructor(source)).to.throw(TypeError);
}
@ -40,13 +41,20 @@ describe('Database#prepare()', function () {
it('should create a prepared Statement object', function () {
const stmt1 = this.db.prepare('CREATE TABLE people (name TEXT) ');
const stmt2 = this.db.prepare('CREATE TABLE people (name TEXT); ');
assertStmt(stmt1, 'CREATE TABLE people (name TEXT) ', this.db, false);
assertStmt(stmt2, 'CREATE TABLE people (name TEXT); ', this.db, false);
assertStmt(stmt1, 'CREATE TABLE people (name TEXT) ', this.db, false, false);
assertStmt(stmt2, 'CREATE TABLE people (name TEXT); ', this.db, false, false);
expect(stmt1).to.not.equal(stmt2);
expect(stmt1).to.not.equal(this.db.prepare('CREATE TABLE people (name TEXT) '));
});
it('should create a prepared Statement object with just an expression', function () {
const stmt = this.db.prepare('SELECT 555');
assertStmt(stmt, 'SELECT 555', this.db, true);
assertStmt(stmt, 'SELECT 555', this.db, true, true);
});
it('should set the correct values for "reader" and "readonly"', function () {
this.db.exec('CREATE TABLE data (value)');
assertStmt(this.db.prepare('SELECT 555'), 'SELECT 555', this.db, true, true);
assertStmt(this.db.prepare('BEGIN'), 'BEGIN', this.db, false, true);
assertStmt(this.db.prepare('BEGIN EXCLUSIVE'), 'BEGIN EXCLUSIVE', this.db, false, false);
assertStmt(this.db.prepare('DELETE FROM data RETURNING *'), 'DELETE FROM data RETURNING *', this.db, true, false);
});
});

View File

@ -19,10 +19,6 @@ describe('Statement#run()', function () {
this.db.close();
});
it('should throw an exception when used on a statement that returns data', function () {
const stmt = this.db.prepare('SELECT 555');
expect(() => stmt.run()).to.throw(TypeError);
});
it('should work with CREATE TABLE', function () {
const { info } = this.db.init();
expect(info.changes).to.equal(0);
@ -34,6 +30,12 @@ describe('Statement#run()', function () {
expect(info.changes).to.equal(0);
expect(info.lastInsertRowid).to.equal(0);
});
it('should work with SELECT', function () {
const stmt = this.db.prepare('SELECT 555');
const info = stmt.run();
expect(info.changes).to.equal(0);
expect(info.lastInsertRowid).to.equal(0);
});
it('should work with INSERT INTO', function () {
let stmt = this.db.init().prepare("INSERT INTO entries VALUES ('foo', 25, 3.14, x'1133ddff')");
let info = stmt.run();

View File

@ -32,6 +32,14 @@ describe('Statement#get()', function () {
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 5 ORDER BY rowid");
expect(stmt.get()).to.deep.equal({ a: 'foo', b: 6, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null });
});
it('should work with RETURNING clause', function () {
let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *");
expect(stmt.reader).to.be.true;
expect(stmt.get()).to.deep.equal({ a: 'bar', b: 888, c: null, d: null, e: null });
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 900 ORDER BY rowid");
expect(stmt.get()).to.deep.equal({ a: 'baz', b: 999, c: null, d: null, e: null });
});
it('should obey the current pluck and expand settings', function () {
const stmt = this.db.prepare("SELECT *, 2 + 3.5 AS c FROM entries ORDER BY rowid");
const expanded = { entries: { a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }, $: { c: 5.5 } };

View File

@ -43,6 +43,20 @@ describe('Statement#all()', function () {
expect(index).to.equal(rows.length);
}
});
it('should work with RETURNING clause', function () {
let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *");
expect(stmt.reader).to.be.true;
expect(stmt.all()).to.deep.equal([
{ a: 'bar', b: 888, c: null, d: null, e: null },
{ a: 'baz', b: 999, c: null, d: null, e: null },
]);
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 800 ORDER BY rowid");
expect(stmt.all()).to.deep.equal([
{ a: 'bar', b: 888, c: null, d: null, e: null },
{ a: 'baz', b: 999, c: null, d: null, e: null },
]);
});
it('should obey the current pluck and expand settings', function () {
const stmt = this.db.prepare("SELECT *, 2 + 3.5 AS c FROM entries ORDER BY rowid");
const expanded = new Array(10).fill().map((_, i) => ({

View File

@ -32,6 +32,7 @@ describe('Statement#iterate()', function () {
let count = 0;
let stmt = this.db.prepare("SELECT * FROM entries ORDER BY rowid");
expect(stmt.reader).to.be.true;
expect(stmt.busy).to.be.false;
const iterator = stmt.iterate();
expect(iterator).to.not.be.null;
@ -41,22 +42,43 @@ describe('Statement#iterate()', function () {
expect(iterator.throw).to.not.be.a('function');
expect(iterator[Symbol.iterator]).to.be.a('function');
expect(iterator[Symbol.iterator]()).to.equal(iterator);
expect(stmt.busy).to.be.true;
for (const data of iterator) {
row.b = ++count;
expect(data).to.deep.equal(row);
expect(stmt.busy).to.be.true;
}
expect(count).to.equal(10);
expect(stmt.busy).to.be.false;
count = 0;
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 5 ORDER BY rowid");
expect(stmt.busy).to.be.false;
const iterator2 = stmt.iterate();
expect(iterator).to.not.equal(iterator2);
expect(stmt.busy).to.be.true;
for (const data of iterator2) {
row.b = ++count + 5;
expect(data).to.deep.equal(row);
expect(stmt.busy).to.be.true;
}
expect(count).to.equal(5);
expect(stmt.busy).to.be.false;
});
it('should work with RETURNING clause', function () {
let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *");
expect(stmt.reader).to.be.true;
expect([...stmt.iterate()]).to.deep.equal([
{ a: 'bar', b: 888, c: null, d: null, e: null },
{ a: 'baz', b: 999, c: null, d: null, e: null },
]);
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 800 ORDER BY rowid");
expect([...stmt.iterate()]).to.deep.equal([
{ a: 'bar', b: 888, c: null, d: null, e: null },
{ a: 'baz', b: 999, c: null, d: null, e: null },
]);
});
it('should obey the current pluck and expand settings', function () {
const shouldHave = (desiredData) => {

View File

@ -92,4 +92,16 @@ describe('Statement#bind()', function () {
expect(() => stmt1.bind(arr)).to.throw(err);
expect(() => stmt2.bind(obj)).to.throw(err);
});
it('should properly bind empty buffers', function () {
this.db.prepare('INSERT INTO entries (c) VALUES (?)').bind(Buffer.alloc(0)).run();
const result = this.db.prepare('SELECT c FROM entries').pluck().get();
expect(result).to.be.an.instanceof(Buffer);
expect(result.length).to.equal(0);
});
it('should properly bind empty strings', function () {
this.db.prepare('INSERT INTO entries (a) VALUES (?)').bind('').run();
const result = this.db.prepare('SELECT a FROM entries').pluck().get();
expect(result).to.be.a('string');
expect(result.length).to.equal(0);
});
});

View File

@ -27,6 +27,7 @@ describe('Database#function()', function () {
it('should throw an exception if boolean options are provided as non-booleans', function () {
expect(() => this.db.function('a', { varargs: undefined }, () => {})).to.throw(TypeError);
expect(() => this.db.function('b', { deterministic: undefined }, () => {})).to.throw(TypeError);
expect(() => this.db.function('b', { directOnly: undefined }, () => {})).to.throw(TypeError);
expect(() => this.db.function('c', { safeIntegers: undefined }, () => {})).to.throw(TypeError);
});
it('should throw an exception if the provided name is empty', function () {

View File

@ -40,6 +40,7 @@ describe('Database#aggregate()', function () {
it('should throw an exception if boolean options are provided as non-booleans', function () {
expect(() => this.db.aggregate('a', { step: () => {}, varargs: undefined })).to.throw(TypeError);
expect(() => this.db.aggregate('b', { step: () => {}, deterministic: undefined })).to.throw(TypeError);
expect(() => this.db.aggregate('b', { step: () => {}, directOnly: undefined })).to.throw(TypeError);
expect(() => this.db.aggregate('c', { step: () => {}, safeIntegers: undefined })).to.throw(TypeError);
});
it('should throw an exception if function options are provided as non-fns', function () {

View File

@ -1,75 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const Database = require('../.');
describe('Database#loadExtension()', function () {
let filepath;
before(function () {
const releaseFilepath = path.join(__dirname, '..', 'build', 'Release', 'test_extension.node');
const debugFilepath = path.join(__dirname, '..', 'build', 'Debug', 'test_extension.node');
try {
fs.accessSync(releaseFilepath);
filepath = releaseFilepath;
} catch (_) {
fs.accessSync(debugFilepath);
filepath = debugFilepath;
}
});
beforeEach(function () {
this.db = new Database(util.next());
});
afterEach(function () {
this.db.close();
});
it('should throw an exception if a string argument is not given', function () {
expect(() => this.db.loadExtension()).to.throw(TypeError);
expect(() => this.db.loadExtension(undefined)).to.throw(TypeError);
expect(() => this.db.loadExtension(null)).to.throw(TypeError);
expect(() => this.db.loadExtension(123)).to.throw(TypeError);
expect(() => this.db.loadExtension(new String(filepath))).to.throw(TypeError);
expect(() => this.db.loadExtension([filepath])).to.throw(TypeError);
});
it('should throw an exception if the database is busy', function () {
let invoked = false;
for (const value of this.db.prepare('select 555').pluck().iterate()) {
expect(value).to.equal(555);
expect(() => this.db.loadExtension(filepath)).to.throw(TypeError);
invoked = true;
}
expect(invoked).to.be.true;
});
it('should throw an exception if the extension is not found', function () {
try {
this.db.loadExtension(filepath + 'x');
} catch (err) {
expect(err).to.be.an.instanceof(Database.SqliteError);
expect(err.message).to.be.a('string');
expect(err.message.length).to.be.above(0);
expect(err.message).to.not.equal('not an error');
expect(err.code).to.equal('SQLITE_ERROR');
return;
}
throw new Error('This code should not have been reached');
});
it('should register the specified extension', function () {
expect(this.db.loadExtension(filepath)).to.equal(this.db);
expect(this.db.prepare('SELECT testExtensionFunction(NULL, 123, 99, 2)').pluck().get()).to.equal(4);
expect(this.db.prepare('SELECT testExtensionFunction(NULL, 2)').pluck().get()).to.equal(2);
});
it('should not allow registering extensions with SQL', function () {
expect(() => this.db.prepare('SELECT load_extension(?)').get(filepath)).to.throw(Database.SqliteError);
expect(this.db.loadExtension(filepath)).to.equal(this.db);
expect(() => this.db.prepare('SELECT load_extension(?)').get(filepath)).to.throw(Database.SqliteError);
this.db.close();
this.db = new Database(util.next());
try {
this.db.loadExtension(filepath + 'x');
} catch (err) {
expect(() => this.db.prepare('SELECT load_extension(?)').get(filepath)).to.throw(Database.SqliteError);
return;
}
throw new Error('This code should not have been reached');
});
});

695
test/34.database.table.js Normal file
View File

@ -0,0 +1,695 @@
'use strict';
const Database = require('../.');
describe('Database#table()', function () {
beforeEach(function () {
this.db = new Database(util.next());
});
afterEach(function () {
this.db.close();
});
it('should throw an exception if the correct arguments are not provided', function () {
expect(() => this.db.table()).to.throw(TypeError);
expect(() => this.db.table(null)).to.throw(TypeError);
expect(() => this.db.table('a')).to.throw(TypeError);
expect(() => this.db.table({})).to.throw(TypeError);
expect(() => this.db.table({ rows: function*(){}, columns: ['x'] })).to.throw(TypeError);
expect(() => this.db.table({ name: 'b', rows: function*(){}, columns: ['x'] })).to.throw(TypeError);
expect(() => this.db.table(() => {})).to.throw(TypeError);
expect(() => this.db.table(function* c() {})).to.throw(TypeError);
expect(() => this.db.table({}, function d() {})).to.throw(TypeError);
expect(() => this.db.table({ name: 'e', rows: function* e() {}, columns: ['x'] }, function e() {})).to.throw(TypeError);
expect(() => this.db.table('f')).to.throw(TypeError);
expect(() => this.db.table('g', null)).to.throw(TypeError);
expect(() => this.db.table('h', {})).to.throw(TypeError);
expect(() => this.db.table('i', Object.create(Function.prototype))).to.throw(TypeError);
expect(() => this.db.table('j', { columns: ['x'] }, function j() {})).to.throw(TypeError);
expect(() => this.db.table('k', { name: 'k', columns: ['x'] }, function* k() {})).to.throw(TypeError);
expect(() => this.db.table('l', { name: 'l', rows: function* l() {} })).to.throw(TypeError);
expect(() => this.db.table(new String('m'), { columns: ['x'], rows: function* m() {} })).to.throw(TypeError);
expect(() => this.db.table(new String('n'), () => {})).to.throw(TypeError);
});
it('should throw an exception if boolean options are provided as non-booleans', function () {
expect(() => this.db.table('a', { columns: ['x'], rows: function*(){}, directOnly: undefined })).to.throw(TypeError);
expect(() => this.db.table('b', { columns: ['x'], rows: function*(){}, safeIntegers: undefined })).to.throw(TypeError);
});
it('should throw an exception if the "columns" option is invalid', function () {
expect(() => this.db.table('a', { rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('b', { columns: undefined, rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('c', { columns: 'x', rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('d', { columns: { length: 1, 0: 'x', [Symbol.iterator]: () => ['x'].values() }, rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('e', { columns: ['x',, 'y'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('f', { columns: ['x', new String('y')], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('g', { columns: ['x', 'x'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('h', { columns: [], rows: function*(){} })).to.throw(RangeError);
});
it('should throw an exception if the "parameters" option is invalid', function () {
expect(() => this.db.table('a', { parameters: undefined, columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('b', { parameters: 'x', columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('c', { parameters: { length: 1, 0: 'x', [Symbol.iterator]: () => ['x'].values() }, columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('d', { parameters: ['x',, 'y'], columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('e', { parameters: ['x', new String('y')], columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('f', { parameters: ['x', 'x'], columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('g', { parameters: ['x'], columns: ['x'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('h', { parameters: [...Array(33)].map((_, i) => `p${i}`), columns: ['foo'], rows: function*(){} })).to.throw(RangeError);
});
it('should throw an exception if the "rows" option is invalid', function () {
expect(() => this.db.table('a', { columns: ['x'] })).to.throw(TypeError);
expect(() => this.db.table('b', { columns: ['x'], rows: undefined })).to.throw(TypeError);
expect(() => this.db.table('c', { columns: ['x'], rows: {} })).to.throw(TypeError);
expect(() => this.db.table('d', { columns: ['x'], rows: () => {} })).to.throw(TypeError);
expect(() => this.db.table('e', { columns: ['x'], rows: function () {} })).to.throw(TypeError);
expect(() => this.db.table('f', { columns: ['x'], rows: Object.create(Function.prototype) })).to.throw(TypeError);
expect(() => this.db.table('g', { columns: ['x'], rows: Object.create(Object.getPrototypeOf(function*(){})) })).to.throw(TypeError);
expect(() => this.db.table('h', { columns: ['x'], rows: Object.setPrototypeOf(() => {}, Object.create(Object.getPrototypeOf(function*(){}))) })).to.throw(TypeError);
});
it('should throw an exception if the provided name is empty', function () {
expect(() => this.db.table('', { columns: ['x'], rows: function* () {} })).to.throw(TypeError);
expect(() => this.db.table('', { name: 'a', columns: ['x'], rows: function* () {} })).to.throw(TypeError);
expect(() => this.db.table('', { name: 'b', columns: ['x'], rows: function* b() {} })).to.throw(TypeError);
expect(() => this.db.table('', function c() {})).to.throw(TypeError);
});
it('should throw an exception if generator.length is invalid', function () {
const length = x => Object.defineProperty(function*(){}, 'length', { value: x });
expect(() => this.db.table('a', { columns: ['x'], rows: length(undefined) })).to.throw(TypeError);
expect(() => this.db.table('b', { columns: ['x'], rows: length(null) })).to.throw(TypeError);
expect(() => this.db.table('c', { columns: ['x'], rows: length('1') })).to.throw(TypeError);
expect(() => this.db.table('d', { columns: ['x'], rows: length(NaN) })).to.throw(TypeError);
expect(() => this.db.table('e', { columns: ['x'], rows: length(Infinity) })).to.throw(TypeError);
expect(() => this.db.table('f', { columns: ['x'], rows: length(1.000000001) })).to.throw(TypeError);
expect(() => this.db.table('g', { columns: ['x'], rows: length(-0.000000001) })).to.throw(TypeError);
expect(() => this.db.table('h', { columns: ['x'], rows: length(-1) })).to.throw(TypeError);
expect(() => this.db.table('i', { columns: ['x'], rows: length(32.000000001) })).to.throw(TypeError);
expect(() => this.db.table('j', { columns: ['x'], rows: length(33) })).to.throw(RangeError);
});
it('should register a virtual table and return the database object', function () {
const length = x => Object.defineProperty(function*(){}, 'length', { value: x });
expect(this.db.table('a', { columns: ['x'], rows: function* () {} })).to.equal(this.db);
expect(this.db.table('b', { columns: ['x'], rows: length(1) })).to.equal(this.db);
expect(this.db.table('c', { columns: ['x'], rows: length(32) })).to.equal(this.db);
});
it('should enable the registered virtual table to be queried from SQL', function () {
const rows = [
{ a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') },
{ a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') },
];
this.db.table('vtab', {
columns: ['a', 'b', 'c', 'd', 'e'],
*rows() {
for (const obj of rows) {
yield Object.values(obj);
}
},
});
expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows);
expect(this.db.prepare('SELECT * FROM vtab WHERE b < 500').all()).to.deep.equal(rows.slice(0, 1));
expect(this.db.prepare('SELECT * FROM vtab ORDER BY d DESC').all()).to.deep.equal(rows.slice().reverse());
});
it('should infer parameters for the virtual table', function () {
this.db.table('vtab', {
columns: ['a', 'b'],
*rows(x, y) {
yield [x, y];
yield [x * 2, y * 3];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?, ?)').all(2, 3))
.to.deep.equal([{ a: 2, b: 3 }, { a: 4, b: 9 }]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ?').all(2, 3))
.to.deep.equal([{ a: 2, b: 3 }, { a: 4, b: 9 }]);
expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?)'))
.to.throw(Database.SqliteError);
expect(() => this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ? AND "$3" = ?'))
.to.throw(Database.SqliteError);
});
it('should accept explicit parameters for the virtual table', function () {
this.db.table('vtab', {
columns: ['a', 'b'],
parameters: ['x', 'y', 'z'],
*rows(p1, p2, p3, p4) {
yield [arguments[0], arguments[1] + arguments[2]];
yield [arguments[0] * 2, (arguments[1] + arguments[2]) * 3];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').all(2, 3, 4))
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').all(2, 3, 4))
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?)'))
.to.throw(Database.SqliteError);
expect(() => this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ? AND "$3" = ?'))
.to.throw(Database.SqliteError);
});
it('should accept a large number of parameters for the virtual table', function () {
const args = ['foo', 'bar', 1, -2, Buffer.from('hello'), 5, -10, 'baz', 99.9, -0.5];
this.db.table('vtab', {
columns: ['x'],
*rows(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) {
yield [p10];
yield [p9];
yield [p8];
yield [p7];
yield [p6];
yield [p5];
yield [p4];
yield [p3];
yield [p2];
yield [p1];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').pluck().all(args))
.to.deep.equal(args.slice().reverse());
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?)').pluck().all(args.slice(0, -1)))
.to.deep.equal([null].concat(args.slice(0, -1).reverse()));
expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'))
.to.throw(Database.SqliteError);
});
it('should correctly handle arguments even when used out of order', function () {
const calls = [];
this.db.table('vtab', {
columns: ['x', 'y'],
*rows(x, y) {
calls.push([...arguments]);
yield { x, y };
},
});
expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ?').get(10, 5))
.to.deep.equal({ x: 10, y: 5 });
expect(calls.splice(0)).to.deep.equal([[10, 5]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$1" = ?').get(5, 10))
.to.deep.equal({ x: 10, y: 5 });
expect(calls.splice(0)).to.deep.equal([[10, 5]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(5, 5, 10))
.to.deep.equal({ x: 10, y: 5 });
expect(calls.splice(0)).to.deep.equal([[10, 5]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(5, 9, 10))
.to.be.undefined;
expect(calls.splice(0)).to.deep.equal([]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(9, 5, 10))
.to.be.undefined;
expect(calls.splice(0)).to.deep.equal([]);
});
it('should correctly handle arguments that are constrained to other arguments', function () {
const calls = [];
this.db.table('vtab', {
columns: ['x', 'y'],
*rows(x, y) {
calls.push([...arguments]);
yield { x, y };
},
});
expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = "$1"').get(10))
.to.deep.equal({ x: 10, y: 10 });
expect(calls.splice(0)).to.deep.equal([[10, 10]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = "$1" AND "$1" = ?').get(10))
.to.deep.equal({ x: 10, y: 10 });
expect(calls.splice(0)).to.deep.equal([[10, 10]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = "$1" AND "$1" = ?').get(10, 10))
.to.deep.equal({ x: 10, y: 10 });
expect(calls.splice(0)).to.deep.equal([[10, 10]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = "$1" AND "$1" = ?').get(5, 10))
.to.be.undefined;
expect(calls.splice(0)).to.deep.equal([]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = "$1" AND "$2" = ? AND "$1" = ?').get(5, 10))
.to.be.undefined;
expect(calls.splice(0)).to.deep.equal([]);
});
it('should throw an exception if the database is busy', function () {
let ranOnce = false;
for (const x of this.db.prepare('SELECT 2').pluck().iterate()) {
expect(x).to.equal(2);
ranOnce = true;
expect(() => this.db.table('a', { columns: ['x'], rows: function* () {} })).to.throw(TypeError);
}
expect(ranOnce).to.be.true;
this.db.table('b', { columns: ['x'], rows: function* () {} });
});
it('should cause the database to become busy when querying the virtual table', function () {
let checkCount = 0;
const expectBusy = function* () {
for (let i = 0; i < 3; ++i) {
expect(() => this.db.exec('SELECT * FROM a')).to.throw(TypeError);
expect(() => this.db.prepare('SELECT 555')).to.throw(TypeError);
expect(() => this.db.pragma('cache_size')).to.throw(TypeError);
expect(() => this.db.function('x', () => {})).to.throw(TypeError);
expect(() => this.db.table('y', { columns: ['x'], rows: function* () {} })).to.throw(TypeError);
checkCount += 1;
yield [i];
}
};
this.db.table('a', { columns: ['x'], rows: function* () {} });
this.db.table('b', { columns: ['x'], rows: expectBusy });
expect(this.db.prepare('SELECT * FROM b').pluck().all()).to.deep.equal([0, 1, 2]);
expect(checkCount).to.equal(3);
this.db.exec('SELECT * FROM a');
this.db.prepare('SELECT 555');
this.db.pragma('cache_size');
this.db.function('xx', () => {});
this.db.table('yy', { columns: ['x'], rows: function* () {} })
});
it('should cause the virtual table to throw when yielding an invalid value', function () {
this.db.table('a', {
columns: ['x'],
*rows() { yield [42]; }
});
this.db.table('b', {
columns: ['x'],
*rows() { yield 42; }
});
this.db.table('c', {
columns: ['x'],
*rows() { yield; }
});
this.db.table('d', {
columns: ['x'],
*rows() { yield null; }
});
expect(this.db.prepare('SELECT * FROM a').get()).to.deep.equal({ x: 42 });
expect(() => this.db.prepare('SELECT * FROM b').get()).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM c').get()).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM d').get()).to.throw(TypeError);
});
it('should allow arrays to be yielded as rows', function () {
const rows = [
{ a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') },
{ a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') },
];
this.db.table('vtab', {
columns: ['a', 'b', 'c', 'd', 'e'],
*rows() {
for (const obj of rows) {
yield Object.values(obj);
}
},
});
expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows);
});
it('should allow objects to be yielded as rows', function () {
const rows = [
{ a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') },
{ a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') },
{ e: Buffer.from('hello'), d: 'world', c: 0.1, b: 10, a: null },
{ d: 'old friend', c: -0.1, e: Buffer.from('goodbye'), a: null, b: -10 },
];
this.db.table('vtab', {
columns: ['a', 'b', 'c', 'd', 'e'],
*rows() {
for (const obj of rows) {
yield obj;
}
},
});
expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows);
});
it('should throw an exception if an invalid array is yielded', function () {
const tests = [
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5, 6],
[1, 2, 3, 4],
[],
[1, 2, 3, 4, new Number(5)],
[1, 2, 3, 4, [5]],
[1, 2, 3, 4, new Date()],
];
this.db.table('vtab', {
columns: ['a', 'b', 'c', 'd', 'e'],
*rows(n) {
yield tests[n];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?)').raw().all(0)).to.deep.equal([tests[0]]);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(1)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(2)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(3)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(4)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(5)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(6)).to.throw(TypeError);
});
it('should throw an exception if an invalid object is yielded', function () {
const tests = [
{ a: 1, b: 2, c: 3, d: 4, e: 5 },
{ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 },
{ a: 1, b: 2, c: 3, d: 4 },
{},
{ a: 1, b: 2, c: 3, d: 4, e: new Number(5) },
{ a: 1, b: 2, c: 3, d: 4, e: [5] },
{ a: 1, b: 2, c: 3, d: 4, e: new Date() },
{ a: 1, b: 2, c: 3, d: 4, f: 5 },
];
this.db.table('vtab', {
columns: ['a', 'b', 'c', 'd', 'e'],
*rows(n) {
yield tests[n];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?)').all(0)).to.deep.equal([tests[0]]);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(1)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(2)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(3)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(4)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(5)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(6)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(7)).to.throw(TypeError);
});
it('should automatically assign rowids without affecting yielded objects', function () {
let rows = [{ x: 5 }, { x: 10 }];
this.db.table('a', {
columns: ['x'],
*rows() { yield* rows; },
});
expect(this.db.prepare('SELECT rowid, * FROM a').all())
.to.deep.equal([{ rowid: 1, x: 5 }, { rowid: 2, x: 10 }]);
expect(rows).to.deep.equal([{ x: 5 }, { x: 10 }]);
rows = [{ rowid: 5 }, { rowid: 10 }];
this.db.table('b', {
columns: ['rowid'],
*rows() { yield* rows; },
});
expect(this.db.prepare('SELECT oid AS oid, * FROM b').all())
.to.deep.equal([{ oid: 1, rowid: 5 }, { oid: 2, rowid: 10 }]);
expect(rows).to.deep.equal([{ rowid: 5 }, { rowid: 10 }]);
});
it('should be driven by stmt.iterate() one row at a time', function () {
let state = 0;
this.db.table('vtab', {
columns: ['x'],
*rows() {
state += 1;
yield ['foo'];
state += 1;
yield ['bar'];
state += 1;
yield ['baz'];
state += 1;
yield ['qux'];
state += 1;
},
});
const values = [];
for (const value of this.db.prepare('SELECT * FROM vtab').pluck().iterate()) {
values.push(value);
if (value === 'baz') break;
}
expect(values).to.deep.equal(['foo', 'bar', 'baz']);
expect(state).to.equal(3);
});
it('should throw an exception if preparing a statement that uses an unsupported operator on a parameter', function () {
this.db.table('vtab', {
columns: ['a', 'b'],
parameters: ['x', 'y', 'z'],
*rows(x, y, z) {
yield [x, y + z];
yield [x * 2, (y + z) * 3];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').all(2, 3, 4))
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').all(2, 3, 4))
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
expect(() => this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z > ?'))
.to.throw(Database.SqliteError);
expect(() => this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y < ? AND z = ?'))
.to.throw(Database.SqliteError);
expect(() => this.db.prepare('SELECT * FROM vtab WHERE x IS ? AND y = ? AND z = ?'))
.to.throw(Database.SqliteError);
});
it('should properly escape column and parameter names', function () {
this.db.table('vtab', {
columns: ['foo);'],
parameters: ['x"); SELECT "y', 'y'],
*rows(x, y) {
yield [x];
yield [y];
yield [x + y];
},
});
expect(this.db.prepare('SELECT "foo);" FROM vtab WHERE "x""); SELECT ""y" = ? AND y = ?').all(5, 10))
.to.deep.equal([{ 'foo);': 5 }, { 'foo);': 10 }, { 'foo);': 15 }]);
});
it('should not allow CREATE VIRTUAL TABLE statements by default', function () {
this.db.table('mod', {
columns: ['x'],
*rows() {},
});
expect(() => this.db.exec('CREATE VIRTUAL TABLE a USING mod')).to.throw(Database.SqliteError);
expect(() => this.db.exec('CREATE VIRTUAL TABLE b USING mod()')).to.throw(Database.SqliteError);
expect(() => this.db.exec('CREATE VIRTUAL TABLE c USING mod(foo)')).to.throw(Database.SqliteError);
});
it('should support CREATE VIRTUAL TABLE statements by accepting a factory function', function () {
let table = '';
this.db.table('mod', function (...args) {
expect(this).to.deep.equal({ module: 'mod', database: 'main', table });
return {
columns: ['x'],
*rows() { yield* args.map(x => [x]); },
};
});
expect(() => this.db.prepare('SELECT * FROM mod')).to.throw(Database.SqliteError);
table = 'foo';
this.db.exec(`CREATE VIRTUAL TABLE ${table} USING mod(hello world, how are you?)`);
table = 'bar';
this.db.exec(`CREATE VIRTUAL TABLE ${table} USING mod(1, 2, 3)`);
expect(this.db.prepare('SELECT x FROM foo').pluck().all()).to.deep.equal(['hello world', 'how are you?']);
expect(this.db.prepare('SELECT x FROM bar').pluck().all()).to.deep.equal(['1', '2', '3']);
expect(() => this.db.prepare('SELECT * FROM mod')).to.throw(Database.SqliteError);
});
it('should correctly handle omitted arguments in any order', function () {
this.db.table('vtab', {
columns: ['value'],
parameters: ['x', 'y', 'z'],
*rows(x = 100, y = 10, z = 1) {
expect(arguments.length).to.equal(3);
yield [x + y + z];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').pluck().get(2.2, 3.3, 4.4)).to.equal(9.9);
expect(this.db.prepare('SELECT * FROM vtab(?, ?)').pluck().get(2.2, 3.3)).to.equal(6.5);
expect(this.db.prepare('SELECT * FROM vtab(?)').pluck().get(2.2)).to.equal(13.2);
expect(this.db.prepare('SELECT * FROM vtab').pluck().get()).to.equal(111);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').pluck().get(2.2, 3.3, 4.4)).to.equal(9.9);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ?').pluck().get(2.2, 3.3)).to.equal(6.5);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND z = ?').pluck().get(2.2, 3.3)).to.equal(15.5);
expect(this.db.prepare('SELECT * FROM vtab WHERE y = ? AND z = ?').pluck().get(2.2, 3.3)).to.equal(105.5);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ?').pluck().get(2.2)).to.equal(13.2);
expect(this.db.prepare('SELECT * FROM vtab WHERE y = ?').pluck().get(2.2)).to.equal(103.2);
expect(this.db.prepare('SELECT * FROM vtab WHERE z = ?').pluck().get(2.2)).to.equal(112.2);
});
it('should not call the generator function if any arguments are NULL', function () {
let calls = 0;
this.db.table('vtab', {
columns: ['val'],
parameters: ['x', 'y', 'z'],
*rows(x = 0, y = 0, z = 0) {
calls += 1;
yield [x + y + z];
},
});
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, 10, 100)).to.deep.equal([111]);
expect(this.db.prepare('SELECT val FROM vtab(?, ?)').pluck().all(1, 10)).to.deep.equal([11]);
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, 10, null)).to.deep.equal([]);
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, null, 100)).to.deep.equal([]);
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(null, 10, 100)).to.deep.equal([]);
expect(this.db.prepare('SELECT val FROM vtab(?, ?)').pluck().all(1, null)).to.deep.equal([]);
expect(calls).to.equal(2);
});
it('should close a statement iterator that caused a virtual table to throw', function () {
this.db.prepare('CREATE TABLE iterable (x INTEGER)').run();
this.db.prepare('INSERT INTO iterable WITH RECURSIVE temp(x) AS (SELECT 1 UNION ALL SELECT x * 2 FROM temp LIMIT 10) SELECT * FROM temp').run();
let i = 0;
const err = new Error('foo');
this.db.table('vtab', {
columns: ['value'],
parameters: ['x'],
*rows(x) {
if (++i >= 5) throw err;
yield [x];
},
});
const iterator = this.db.prepare('SELECT value FROM vtab JOIN iterable USING (x)').pluck().iterate();
let total = 0;
expect(() => {
for (const value of iterator) {
total += value;
expect(() => this.db.exec('SELECT value FROM vtab JOIN iterable USING (x) LIMIT 4')).to.throw(TypeError);
}
}).to.throw(err);
expect(total).to.equal(1 + 2 + 4 + 8);
expect(iterator.next()).to.deep.equal({ value: undefined, done: true });
expect(total).to.equal(1 + 2 + 4 + 8);
i = 0;
this.db.exec('SELECT value FROM vtab JOIN iterable USING (x) LIMIT 4');
expect(i).to.equal(4);
});
it('should not be able to affect bound buffers mid-query', function () {
const input = Buffer.alloc(1024 * 8).fill(0xbb);
let called = false;
this.db.table('vtab', {
columns: ['x'],
*rows(arg) {
called = true;
input[0] = 2;
arg[0] = 2;
yield [123];
},
});
const [output, arg, num] = this.db.prepare('SELECT :input, "$1", x FROM vtab(:input)').raw().get({ input });
expect(called).to.be.true;
expect(output.equals(Buffer.alloc(1024 * 8).fill(0xbb))).to.be.true;
expect(arg.equals(Buffer.alloc(1024 * 8).fill(0xbb))).to.be.true;
expect(num).to.equal(123);
});
describe('should propagate exceptions', function () {
const exceptions = [new TypeError('foobar'), new Error('baz'), { yup: 'ok' }, 'foobarbazqux', '', null, 123.4];
const expectError = (exception, fn) => {
try { fn(); } catch (ex) {
expect(ex).to.equal(exception);
return;
}
throw new TypeError('Expected table to throw an exception');
};
specify('thrown in the factory function', function () {
exceptions.forEach((exception, index) => {
const calls = [];
this.db.table(`mod${index}`, () => {
calls.push('a');
throw exception;
calls.push('b');
return {
columns: ['x'],
*rows() {
calls.push('c');
yield [42];
calls.push('d');
},
};
});
expect(calls.splice(0)).to.deep.equal([]);
expectError(exception, () => this.db.exec(`CREATE VIRTUAL TABLE vtab${index} USING mod${index}()`));
expect(calls.splice(0)).to.deep.equal(['a']);
expect(() => this.db.prepare(`SELECT * FROM vtab${index}`)).to.throw(Database.SqliteError);
expect(calls.splice(0)).to.deep.equal([]);
});
});
specify('thrown in the rows() function', function () {
exceptions.forEach((exception, index) => {
const calls = [];
this.db.table(`mod${index}`, () => {
calls.push('a');
return {
columns: ['x'],
*rows() {
calls.push('b');
yield [42];
calls.push('c');
throw exception;
calls.push('d');
},
};
});
expect(calls.splice(0)).to.deep.equal([]);
this.db.exec(`CREATE VIRTUAL TABLE vtab${index} USING mod${index}()`);
expect(calls.splice(0)).to.deep.equal(['a']);
expect(this.db.prepare(`SELECT * FROM vtab${index}`).pluck().get()).to.equal(42);
expect(calls.splice(0)).to.deep.equal(['b']);
expectError(exception, () => this.db.prepare(`SELECT * FROM vtab${index}`).pluck().all());
expect(calls.splice(0)).to.deep.equal(['b', 'c']);
});
});
specify('thrown due to yielding an invalid value', function () {
const calls = [];
this.db.table('mod', () => {
calls.push('a');
return {
columns: ['x'],
*rows() {
calls.push('b');
yield [42];
calls.push('c');
yield [new Number(42)];
calls.push('d');
},
};
});
expect(calls.splice(0)).to.deep.equal([]);
this.db.exec('CREATE VIRTUAL TABLE vtab USING mod()');
expect(calls.splice(0)).to.deep.equal(['a']);
expect(this.db.prepare('SELECT * FROM vtab').pluck().get()).to.equal(42);
expect(calls.splice(0)).to.deep.equal(['b']);
expect(() => this.db.prepare('SELECT * FROM vtab').pluck().all()).to.throw(TypeError);
expect(calls.splice(0)).to.deep.equal(['b', 'c']);
});
});
describe('should not affect external environment', function () {
specify('busy state', function () {
this.db.table('vtab', {
columns: ['x'],
*rows(arg) {
expect(() => this.db.exec('SELECT 555')).to.throw(TypeError);
yield [arg * 2];
},
});
let ranOnce = false;
for (const x of this.db.prepare('SELECT * FROM vtab(555)').pluck().iterate()) {
ranOnce = true;
expect(x).to.equal(1110);
expect(() => this.db.exec('SELECT 555')).to.throw(TypeError);
}
expect(ranOnce).to.be.true;
this.db.exec('SELECT 555');
});
specify('was_js_error state', function () {
this.db.prepare('CREATE TABLE data (value INTEGER)').run();
const stmt = this.db.prepare('SELECT value FROM data');
this.db.prepare('DROP TABLE data').run();
const err = new Error('foo');
this.db.table('vtab', {
columns: ['x'],
*rows() { throw err; },
});
expect(() => this.db.prepare('SELECT * FROM vtab').get()).to.throw(err);
try { stmt.get(); } catch (ex) {
expect(ex).to.be.an.instanceof(Error);
expect(ex).to.not.equal(err);
expect(ex.message).to.not.equal(err.message);
expect(ex).to.be.an.instanceof(Database.SqliteError);
return;
}
throw new TypeError('Expected the statement to throw an exception');
});
});
it('should correctly handle limit and offset clause', function () {
let lastValue;
this.db.table('vtab', {
columns: ['x'],
*rows() {
lastValue = 1;
yield { x: lastValue };
lastValue = 2;
yield { x: lastValue };
lastValue = 3;
yield { x: lastValue };
lastValue = null;
},
});
expect(this.db.prepare('SELECT * FROM vtab LIMIT 1').all())
.to.deep.equal([{ x: 1 }]);
expect(lastValue).to.equal(1);
expect(this.db.prepare('SELECT * FROM vtab LIMIT 1 OFFSET 2').all())
.to.deep.equal([{ x: 3 }]);
expect(lastValue).to.equal(3);
expect(this.db.prepare('SELECT * FROM vtab LIMIT 100 OFFSET 1').all())
.to.deep.equal([{ x: 2 }, { x: 3 }]);
expect(lastValue).to.be.null;
});
});

View File

@ -0,0 +1,78 @@
'use strict';
const Database = require('../.');
const segmenter = new Intl.Segmenter([], {
granularity: 'word',
});
const DIACRITICS = /[\u0300-\u036f]/g;
function removeDiacritics(str) {
return str.normalize('NFD').replace(DIACRITICS, '');
}
describe('Database#serialize()', function () {
beforeEach(function () {
this.db = new Database(':memory:');
this.db.createFTS5Tokenizer('js', class Tokenizer {
constructor(params) {
expect(params).to.eql(['arg1', 'arg2']);
}
run(str) {
const result = [];
let off = 0;
for (const seg of segmenter.segment(str)) {
const len = Buffer.byteLength(seg.segment);
if (seg.isWordLike) {
const normalized = removeDiacritics(seg.segment);
result.push(off, off + len, normalized === seg.segment ? undefined : normalized);
}
off += len;
}
return result;
}
});
this.db.prepare("CREATE VIRTUAL TABLE fts USING fts5(content, tokenize='js arg1 arg2')").run();
this.insertStmt = this.db.prepare("INSERT INTO fts (content) VALUES (?)");
this.lookupStmt = this.db.prepare(
"SELECT snippet(fts, -1, '[', ']', '...', 20) " +
"FROM fts " +
"WHERE content MATCH $query").pluck();
});
afterEach(function () {
this.db.close();
});
it("should support CJK symbols at the start", function() {
this.insertStmt.run("知识需要时间");
const rows = this.lookupStmt.all({ query: "知*" });
expect(rows).to.eql(["[知识]需要时间"]);
});
it("should support CJK symbols in the middle", function() {
this.insertStmt.run("知识需要时间");
const rows = this.lookupStmt.all({ query: "需*" });
expect(rows).to.eql(["知识[需要]时间"]);
});
it("should support Korean symbols", function() {
this.insertStmt.run("안녕 세상");
const rows = this.lookupStmt.all({ query: "세*" });
expect(rows).to.eql(["안녕 [세상]"]);
});
it("should support normalization", function() {
this.insertStmt.run("dïācrîtįcs");
const rows = this.lookupStmt.all({ query: "diacritics*" });
expect(rows).to.eql(["[dïācrîtįcs]"]);
});
it("should support punctuation", function() {
this.insertStmt.run("hello!world! how are you?");
const rows = this.lookupStmt.all({ query: "h*" });
expect(rows).to.eql(["[hello]!world! [how] are you?"]);
});
});

View File

@ -0,0 +1,71 @@
'use strict';
const Database = require('../.');
const segmenter = new Intl.Segmenter([], {
granularity: 'word',
});
const DIACRITICS = /[\u0300-\u036f]/g;
function removeDiacritics(str) {
return str.normalize('NFD').replace(DIACRITICS, '');
}
describe('Database#serialize()', function () {
beforeEach(function () {
this.db = new Database(':memory:');
this.db.prepare("CREATE VIRTUAL TABLE fts USING fts5(content, tokenize='signal_tokenizer')").run();
this.insertStmt = this.db.prepare("INSERT INTO fts (content) VALUES (?)");
this.lookupStmt = this.db.prepare(
"SELECT snippet(fts, -1, '[', ']', '...', 20) " +
"FROM fts " +
"WHERE content MATCH $query").pluck();
});
afterEach(function () {
this.db.close();
});
it("should support CJK symbols at the start", function() {
this.insertStmt.run("知识需要时间");
const rows = this.lookupStmt.all({ query: "知*" });
expect(rows).to.eql(["[知]识需要时间"]);
});
it("should support CJK symbols in the middle", function() {
this.insertStmt.run("知识需要时间");
const rows = this.lookupStmt.all({ query: "需*" });
expect(rows).to.eql(["知识[需]要时间"]);
});
it("should support Korean symbols", function() {
this.insertStmt.run("안녕 세상");
const rows = this.lookupStmt.all({ query: "세*" });
expect(rows).to.eql(["안녕 [세상]"]);
});
it("should support normalization", function() {
this.insertStmt.run("dïācrîtįcs");
const rows = this.lookupStmt.all({ query: "diacritics*" });
expect(rows).to.eql(["[dïācrîtįcs]"]);
});
it("should support punctuation", function() {
this.insertStmt.run("Hello!world! how are you?");
const rows = this.lookupStmt.all({ query: "h*" });
expect(rows).to.eql(["[Hello]!world! [how] are you?"]);
});
it("should ignore invalid utf8", function() {
this.insertStmt.run(Buffer.from([ 0x74 /* 't' */, 0xc3, 0x28 ]));
const rows = this.lookupStmt.all({ query: "t*" });
expect(rows).to.eql([]);
});
it("should tokenize using signalTokenize", function() {
expect(this.db.signalTokenize("Hello signal.org!")).to.eql([
"hello",
"signal.org",
]);
});
});

View File

@ -62,6 +62,16 @@ describe('BigInts', function () {
expect(this.db.prepare('SELECT customfunc(?)').pluck().get(2)).to.equal('number2');
expect(this.db.prepare('SELECT customfunc(?)').pluck().get(BigInt(2))).to.equal('bigint2');
});
it('should get passed to aggregates defined with the "safeIntegers" option', function () {
this.db.aggregate('customagg', { safeIntegers: true, step: (_, a) => { return (typeof a) + a; } });
expect(this.db.prepare('SELECT customagg(?)').pluck().get(2)).to.equal('number2');
expect(this.db.prepare('SELECT customagg(?)').pluck().get(BigInt(2))).to.equal('bigint2');
});
it('should get passed to virtual tables defined with the "safeIntegers" option', function () {
this.db.table('customvtab', { safeIntegers: true, columns: ['x'], *rows(a) { yield [(typeof a) + a]; } });
expect(this.db.prepare('SELECT * FROM customvtab(?)').pluck().get(2)).to.equal('number2');
expect(this.db.prepare('SELECT * FROM customvtab(?)').pluck().get(BigInt(2))).to.equal('bigint2');
});
it('should respect the default setting on the database', function () {
let arg;
const int = BigInt('1006028374637854687');
@ -70,6 +80,16 @@ describe('BigInts', function () {
this.db.prepare(`SELECT ${name}(?)`).get(int);
return arg;
};
const customAggregateArg = (name, options, dontDefine) => {
dontDefine || this.db.aggregate(name, { ...options, step: (_, a) => { arg = a; } });
this.db.prepare(`SELECT ${name}(?)`).get(int);
return arg;
};
const customTableArg = (name, options, dontDefine) => {
dontDefine || this.db.table(name, { ...options, columns: ['x'], *rows(a) { arg = a; } });
this.db.prepare(`SELECT * FROM ${name}(?)`).get(int);
return arg;
};
this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)').run(int, int, int);
this.db.defaultSafeIntegers(true);
@ -78,6 +98,10 @@ describe('BigInts', function () {
expect(stmt.safeIntegers(false).get()).to.equal(1006028374637854700);
expect(customFunctionArg('a1')).to.deep.equal(int);
expect(customFunctionArg('a2', { safeIntegers: false })).to.equal(1006028374637854700);
expect(customAggregateArg('a1')).to.deep.equal(int);
expect(customAggregateArg('a2', { safeIntegers: false })).to.equal(1006028374637854700);
expect(customTableArg('a1')).to.deep.equal(int);
expect(customTableArg('a2', { safeIntegers: false })).to.equal(1006028374637854700);
this.db.defaultSafeIntegers(false);
@ -86,6 +110,10 @@ describe('BigInts', function () {
expect(stmt2.safeIntegers().get()).to.deep.equal(int);
expect(customFunctionArg('a3')).to.equal(1006028374637854700);
expect(customFunctionArg('a4', { safeIntegers: true })).to.deep.equal(int);
expect(customAggregateArg('a3')).to.equal(1006028374637854700);
expect(customAggregateArg('a4', { safeIntegers: true })).to.deep.equal(int);
expect(customTableArg('a3')).to.equal(1006028374637854700);
expect(customTableArg('a4', { safeIntegers: true })).to.deep.equal(int);
this.db.defaultSafeIntegers();
@ -95,11 +123,23 @@ describe('BigInts', function () {
expect(customFunctionArg('a2', {}, true)).to.equal(1006028374637854700);
expect(customFunctionArg('a3', {}, true)).to.equal(1006028374637854700);
expect(customFunctionArg('a4', {}, true)).to.deep.equal(int);
expect(customAggregateArg('a1', {}, true)).to.deep.equal(int);
expect(customAggregateArg('a2', {}, true)).to.equal(1006028374637854700);
expect(customAggregateArg('a3', {}, true)).to.equal(1006028374637854700);
expect(customAggregateArg('a4', {}, true)).to.deep.equal(int);
expect(customTableArg('a1', {}, true)).to.deep.equal(int);
expect(customTableArg('a2', {}, true)).to.equal(1006028374637854700);
expect(customTableArg('a3', {}, true)).to.equal(1006028374637854700);
expect(customTableArg('a4', {}, true)).to.deep.equal(int);
const stmt3 = this.db.prepare('SELECT a FROM entries').pluck();
expect(stmt3.get()).to.deep.equal(int);
expect(stmt3.safeIntegers(false).get()).to.equal(1006028374637854700);
expect(customFunctionArg('a5')).to.deep.equal(int);
expect(customFunctionArg('a6', { safeIntegers: false })).to.equal(1006028374637854700);
expect(customAggregateArg('a5')).to.deep.equal(int);
expect(customAggregateArg('a6', { safeIntegers: false })).to.equal(1006028374637854700);
expect(customTableArg('a5')).to.deep.equal(int);
expect(customTableArg('a6', { safeIntegers: false })).to.equal(1006028374637854700);
});
});

View File

@ -167,30 +167,23 @@ describe('integrity checks', function () {
});
});
describe('Database#loadExtension()', function () {
let filepath;
before(function () {
const releaseFilepath = path.join(__dirname, '..', 'build', 'Release', 'test_extension.node');
const debugFilepath = path.join(__dirname, '..', 'build', 'Debug', 'test_extension.node');
try {
fs.accessSync(releaseFilepath);
filepath = releaseFilepath;
} catch (_) {
fs.accessSync(debugFilepath);
filepath = debugFilepath;
}
});
describe('Database#table()', function () {
specify('while iterating (blocked)', function () {
whileIterating(this, blocked(() => this.db.loadExtension(filepath)));
normally(allowed(() => this.db.loadExtension(filepath)));
let i = 0;
whileIterating(this, blocked(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
expect(i).to.equal(5);
normally(allowed(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
});
specify('while busy (blocked)', function () {
whileBusy(this, blocked(() => this.db.loadExtension(filepath)));
normally(allowed(() => this.db.loadExtension(filepath)));
let i = 0;
whileBusy(this, blocked(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
expect(i).to.equal(5);
normally(allowed(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
});
specify('while closed (blocked)', function () {
whileClosed(this, blocked(() => this.db.loadExtension(filepath)));
let i = 0;
whileClosed(this, blocked(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
expect(i).to.equal(1);
});
});