├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── bench ├── bench.c ├── bench.js ├── bench_bun.js ├── bench_bun_ffi.js ├── bench_deno.js ├── bench_deno_ffi.js ├── bench_deno_wasm.js ├── bench_node.js ├── bench_python.py ├── download.sh ├── makefile ├── northwind │ ├── bun.js │ ├── deno.js │ ├── deno_old.js │ ├── deno_wasm.js │ └── node.mjs └── results.png ├── deno.json ├── deps.ts ├── doc.md ├── mod.ts ├── package.json ├── scripts └── build.ts ├── src ├── blob.ts ├── constants.ts ├── database.ts ├── ffi.ts ├── statement.ts └── util.ts └── test ├── deps.ts └── test.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [djdeveloperr] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Setup repo 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Deno 17 | uses: denoland/setup-deno@main 18 | with: 19 | deno-version: "v2.x" 20 | 21 | - name: Check Formatting 22 | run: deno fmt --check 23 | 24 | - name: Lint 25 | run: deno lint 26 | 27 | - name: Check 28 | run: deno check src/ test/ scripts/ mod.ts 29 | 30 | build-and-test: 31 | strategy: 32 | matrix: 33 | os: [ubuntu-24.04, ubuntu-24.04-arm, windows-2022, macos-14] 34 | 35 | runs-on: ${{ matrix.os }} 36 | 37 | steps: 38 | - name: Checkout repo 39 | uses: actions/checkout@v4 40 | with: 41 | submodules: true 42 | 43 | - name: Install Deno 44 | uses: denoland/setup-deno@main 45 | with: 46 | deno-version: "v2.x" 47 | 48 | - name: Setup MSVC Dev Env 49 | if: matrix.os == 'windows-2022' 50 | uses: ilammy/msvc-dev-cmd@v1 51 | 52 | - name: Build sqlite3 (windows) 53 | if: matrix.os == 'windows-2022' 54 | env: 55 | TARGET_ARCH: x86_64 56 | run: deno task build 57 | 58 | - name: Build sqlite3 (linux x86_64) 59 | if: matrix.os == 'ubuntu-24.04' 60 | env: 61 | TARGET_ARCH: x86_64 62 | run: deno task build 63 | 64 | - name: Build sqlite3 (linux arm) 65 | if: matrix.os == 'ubuntu-24.04-arm' 66 | env: 67 | TARGET_ARCH: aarch64 68 | run: deno task build 69 | 70 | - name: Build sqlite3 (macos-x86_64) 71 | if: matrix.os == 'macos-14' 72 | env: 73 | TARGET_ARCH: x86_64 74 | CC: clang -arch x86_64 75 | run: deno task build 76 | 77 | - name: Build sqlite3 (macos-aarch64) 78 | if: matrix.os == 'macos-14' 79 | env: 80 | TARGET_ARCH: aarch64 81 | CC: clang -arch arm64 82 | run: deno task build 83 | 84 | - name: Run Tests 85 | env: 86 | DENO_SQLITE_LOCAL: 1 87 | run: deno task test 88 | 89 | - name: Release 90 | if: ${{ github.ref == 'refs/heads/main' }} 91 | uses: softprops/action-gh-release@master 92 | env: 93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 94 | with: 95 | tag_name: "Release Draft" 96 | draft: true 97 | files: | 98 | build/libsqlite3.so 99 | build/libsqlite3_aarch64.so 100 | build/libsqlite3.dylib 101 | build/libsqlite3_aarch64.dylib 102 | build/sqlite3.dll 103 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: release 3 | 4 | jobs: 5 | publish: 6 | runs-on: ubuntu-latest 7 | 8 | permissions: 9 | contents: read 10 | id-token: write 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Publish package 16 | run: npx jsr publish 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | *.db 3 | *.bat 4 | *.exp 5 | *.pdb 6 | *.dll 7 | *.a 8 | *.so 9 | *.db-shm 10 | *.db-wal 11 | package-lock.json 12 | *.sqlite 13 | node_modules/ 14 | build/ 15 | # C benchmark binary 16 | bench/bench 17 | deno.lock 18 | bun.lockb 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sqlite"] 2 | path = sqlite 3 | url = https://github.com/sqlite/sqlite 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 DjDeveloperr 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deno SQLite3 2 | 3 | [![Tags](https://img.shields.io/github/release/denodrivers/sqlite3)](https://github.com/denodrivers/sqlite3/releases) 4 | [![Doc](https://doc.deno.land/badge.svg)](https://jsr.io/@db/sqlite/doc) 5 | [![Checks](https://github.com/denodrivers/sqlite3/actions/workflows/ci.yml/badge.svg)](https://github.com/denodrivers/sqlite3/actions/workflows/ci.yml) 6 | [![License](https://img.shields.io/github/license/denodrivers/sqlite3)](https://github.com/denodrivers/sqlite3/blob/master/LICENSE) 7 | [![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/DjDeveloperr) 8 | 9 | The fastest and correct module for SQLite3 in Deno. 10 | 11 | ## Example 12 | 13 | ```ts 14 | import { Database } from "jsr:@db/sqlite@0.11"; 15 | 16 | const db = new Database("test.db"); 17 | 18 | const [version] = db.prepare("select sqlite_version()").value<[string]>()!; 19 | console.log(version); 20 | 21 | db.close(); 22 | ``` 23 | 24 | ## Usage 25 | 26 | Since this library depends on the FFI API, you must pass `--allow-env`, 27 | `--allow-ffi`. Network and FS permissions are also needed to download and cache 28 | prebuilt library. 29 | 30 | You can also just use `--allow-all` / `-A` flag since FFI basically gives full 31 | access. 32 | 33 | ```sh 34 | deno run -A 35 | ``` 36 | 37 | ## Benchmark 38 | 39 | ![image](./bench/results.png) 40 | 41 | [Benchmark](./bench) based on 42 | [just-js/02-sqlite](https://just-js.github.io/benchmarks/02-sqlite.html) 43 | 44 | See [bench](./bench) for benchmarks source. 45 | 46 | ## Documentation 47 | 48 | See [doc.md](https://github.com/denodrivers/sqlite3/blob/main/doc.md) for 49 | documentation. 50 | 51 | Check out the complete API reference [here](https://jsr.io/@db/sqlite/doc). 52 | 53 | ## Native Library 54 | 55 | It will download and cache a prebuilt shared library from GitHub releases, for 56 | which it will need network and file system read/write permission. 57 | 58 | If you want to use custom library, then you can set the `DENO_SQLITE_PATH` 59 | environment variable, to a fully specified path to the SQLite3 shared library. 60 | 61 | ## Contributing 62 | 63 | Code is formatted using `deno fmt` and linted using `deno lint`. Please make 64 | sure to run these commands before committing. 65 | 66 | You can optionally build sqlite3 from source. Make sure that you have the 67 | submodule (`git submodule update --init --recursive`). 68 | 69 | ```sh 70 | deno task build 71 | ``` 72 | 73 | When running tests and benchmarks, you use the `DENO_SQLITE_LOCAL=1` env 74 | variable otherwise it won't use to locally compiled SQLite library. 75 | 76 | ```sh 77 | DENO_SQLITE_LOCAL=1 deno task bench 78 | ``` 79 | 80 | ## Related 81 | 82 | - [x/sqlite](https://deno.land/x/sqlite), WASM based. 83 | 84 | ## License 85 | 86 | Apache-2.0. Check [LICENSE](./LICENSE) for details. 87 | 88 | Copyright © 2023 DjDeveloperr 89 | -------------------------------------------------------------------------------- /bench/bench.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int total = 5; 7 | int count = 1000000; 8 | 9 | int get_version (sqlite3_stmt* stmt) { 10 | if (sqlite3_step(stmt) == SQLITE_ROW) { 11 | int val = sqlite3_column_int(stmt, 0); 12 | sqlite3_reset(stmt); 13 | return val; 14 | } 15 | sqlite3_finalize(stmt); 16 | return 0; 17 | } 18 | 19 | void bench (sqlite3_stmt* stmt) { 20 | float start, end; 21 | start = (float)clock() / (CLOCKS_PER_SEC / 1000); 22 | for (int i = 0; i < count; i++) get_version(stmt); 23 | end = (float)clock() / (CLOCKS_PER_SEC / 1000); 24 | printf("time %.0f ms rate %.0f\n", (end - start), count / ((end - start) / 1000)); 25 | } 26 | 27 | int main (int argc, char** argv) { 28 | if (argc > 1) total = atoi(argv[1]); 29 | if (argc > 2) count = atoi(argv[2]); 30 | sqlite3* db; 31 | int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_PRIVATECACHE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_CREATE; 32 | sqlite3_open_v2(":memory:", &db, flags, NULL); 33 | sqlite3_stmt* stmt; 34 | sqlite3_prepare_v2(db, "pragma user_version", -1, &stmt, 0); 35 | char *err_msg = 0; 36 | sqlite3_exec(db, "PRAGMA auto_vacuum = none", 0, 0, &err_msg); 37 | sqlite3_exec(db, "PRAGMA temp_store = memory", 0, 0, &err_msg); 38 | sqlite3_exec(db, "PRAGMA locking_mode = exclusive", 0, 0, &err_msg); 39 | sqlite3_exec(db, "PRAGMA user_version = 100", 0, 0, &err_msg); 40 | while (total--) bench(stmt); 41 | sqlite3_finalize(stmt); 42 | sqlite3_close(db); 43 | } -------------------------------------------------------------------------------- /bench/bench.js: -------------------------------------------------------------------------------- 1 | import { createCanvas } from "https://deno.land/x/skia_canvas@0.5.2/mod.ts"; 2 | import "https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"; 3 | import "https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0/dist/chartjs-plugin-datalabels.min.js"; 4 | 5 | Chart.register(ChartDataLabels); 6 | 7 | function $(name) { 8 | const lines = new TextDecoder().decode( 9 | Deno.spawnSync(Deno.execPath(), { 10 | args: ["task", name], 11 | env: { 12 | NO_COLOR: "1", 13 | }, 14 | stdout: "piped", 15 | }).stdout, 16 | ) 17 | .split("\n") 18 | .map((line) => line.trim()) 19 | .filter((line) => line.includes(" rate ")); 20 | const all = lines.map((e) => Number(e.split(" rate ")[1])); 21 | return all.reduce((p, a) => p + a, 0) / all.length; 22 | } 23 | 24 | console.log("Running C benchmark..."); 25 | const cOut = $("bench-c"); 26 | console.log("C Avg:", cOut); 27 | 28 | console.log("Running Deno benchmark..."); 29 | const denoOut = $("bench-deno"); 30 | console.log("Deno Avg:", denoOut); 31 | 32 | console.log("Running Deno Wasm benchmark..."); 33 | const denoWasmOut = $("bench-deno-wasm"); 34 | console.log("Deno Wasm Avg:", denoWasmOut); 35 | 36 | console.log("Running Deno FFI benchmark..."); 37 | const denoFfiOut = $("bench-deno-ffi"); 38 | console.log("Deno FFI Avg:", denoFfiOut); 39 | 40 | console.log("Running Node benchmark..."); 41 | const nodeOut = $("bench-node"); 42 | console.log("Node Avg:", nodeOut); 43 | 44 | console.log("Running Bun benchmark..."); 45 | const bunOut = $("bench-bun"); 46 | console.log("Bun Avg:", bunOut); 47 | 48 | console.log("Running Bun FFI benchmark..."); 49 | const bunFfiOut = $("bench-bun-ffi"); 50 | console.log("Bun FFI Avg:", bunFfiOut); 51 | 52 | console.log("Running Python benchmark..."); 53 | const pyOut = $("bench-python"); 54 | console.log("Python Avg:", pyOut); 55 | 56 | const data = { 57 | labels: [ 58 | "C", 59 | "Deno FFI", 60 | "x/sqlite3 (FFI)", 61 | "x/sqlite (WASM)", 62 | "better-sqlite3", 63 | "bun:ffi", 64 | "bun:sqlite", 65 | "python sqlite", 66 | ], 67 | datasets: [{ 68 | label: "Performance", 69 | data: [ 70 | cOut, 71 | denoFfiOut, 72 | denoOut, 73 | denoWasmOut, 74 | nodeOut, 75 | bunFfiOut, 76 | bunOut, 77 | pyOut, 78 | ], 79 | backgroundColor: [ 80 | "#8dc149", 81 | "#4285f4", 82 | "#ea4336", 83 | "#fbbb07", 84 | "#34a753", 85 | "#ff6d01", 86 | "#5d5986", 87 | "#417996", 88 | ], 89 | borderWidth: 0, 90 | borderRadius: 6, 91 | }], 92 | }; 93 | 94 | const canvas = createCanvas(800, 600); 95 | const ctx = canvas.getContext("2d"); 96 | 97 | console.log("Rendering chart..."); 98 | 99 | const _chart = new Chart(ctx, { 100 | type: "bar", 101 | data, 102 | options: { 103 | plugins: { 104 | title: { 105 | display: true, 106 | text: `SQLite Benchmark`, 107 | }, 108 | subtitle: { 109 | display: true, 110 | text: `Higher is better`, 111 | }, 112 | datalabels: { 113 | anchor: "end", 114 | align: "top", 115 | formatter: Math.round, 116 | font: { 117 | weight: "normal", 118 | size: 14, 119 | }, 120 | }, 121 | }, 122 | scales: { 123 | y: { 124 | beginAtZero: true, 125 | title: { 126 | display: true, 127 | text: "ops/sec", 128 | }, 129 | }, 130 | x: { 131 | title: { 132 | display: true, 133 | text: "module", 134 | }, 135 | }, 136 | }, 137 | responsive: false, 138 | animation: false, 139 | }, 140 | plugins: [ 141 | { 142 | id: "custom_canvas_background_color", 143 | beforeDraw: (chart) => { 144 | const { ctx } = chart; 145 | ctx.save(); 146 | ctx.fillStyle = "white"; 147 | ctx.fillRect(0, 0, chart.width, chart.height); 148 | ctx.restore(); 149 | }, 150 | }, 151 | ], 152 | }); 153 | 154 | canvas.save("bench/results.png"); 155 | console.log("Done!"); 156 | -------------------------------------------------------------------------------- /bench/bench_bun.js: -------------------------------------------------------------------------------- 1 | import process from "node:process"; 2 | import { Database } from "bun:sqlite"; 3 | 4 | // Unsafe concurrency is default. 5 | const db = Database.open(":memory:"); 6 | 7 | db.exec("PRAGMA auto_vacuum = none"); 8 | db.exec("PRAGMA temp_store = memory"); 9 | db.exec("PRAGMA locking_mode = exclusive"); 10 | db.exec("PRAGMA user_version = 100"); 11 | 12 | const sql = "pragma user_version"; 13 | 14 | function createQuery(sql) { 15 | return db.prepare(sql); 16 | } 17 | 18 | let total = parseInt(process.argv[2], 10); 19 | const runs = parseInt(process.argv[3], 10); 20 | 21 | function bench(query) { 22 | const start = performance.now(); 23 | for (let i = 0; i < runs; i++) query(); 24 | const elapsed = Math.floor(performance.now() - start); 25 | const rate = Math.floor(runs / (elapsed / 1000)); 26 | console.log(`time ${elapsed} ms rate ${rate}`); 27 | if (--total) process.nextTick(() => bench(query)); 28 | } 29 | 30 | const query = createQuery(sql); 31 | bench(() => query.get()); 32 | -------------------------------------------------------------------------------- /bench/bench_bun_ffi.js: -------------------------------------------------------------------------------- 1 | import process from "node:process"; 2 | import { dlopen, ptr } from "bun:ffi"; 3 | 4 | import { 5 | SQLITE3_OPEN_CREATE, 6 | SQLITE3_OPEN_MEMORY, 7 | SQLITE3_OPEN_PRIVATECACHE, 8 | SQLITE3_OPEN_READWRITE, 9 | } from "../src/constants.ts"; 10 | 11 | const { 12 | symbols: { 13 | sqlite3_open_v2, 14 | sqlite3_exec, 15 | sqlite3_prepare_v2, 16 | sqlite3_reset, 17 | sqlite3_step, 18 | sqlite3_column_int, 19 | sqlite3_errstr, 20 | }, 21 | } = dlopen("build/libsqlite3.so", { 22 | sqlite3_open_v2: { 23 | args: [ 24 | "ptr", // const char *filename 25 | "ptr", // sqlite3 **ppDb 26 | "i32", // int flags 27 | "u64", // const char *zVfs 28 | ], 29 | returns: "i32", 30 | }, 31 | 32 | sqlite3_errstr: { 33 | args: ["i32" /** int errcode */], 34 | returns: "cstring", 35 | }, 36 | 37 | sqlite3_prepare_v2: { 38 | args: [ 39 | "u64", // sqlite3 *db 40 | "ptr", // const char *zSql 41 | "i32", // int nByte 42 | "ptr", // sqlite3_stmt **ppStmt 43 | "u64", // const char **pzTail 44 | ], 45 | returns: "i32", 46 | }, 47 | 48 | sqlite3_exec: { 49 | args: [ 50 | "u64", // sqlite3 *db 51 | "ptr", // const char *sql 52 | "u64", // sqlite3_callback callback 53 | "u64", // void *arg 54 | "ptr", // char **errmsg 55 | ], 56 | returns: "i32", 57 | }, 58 | 59 | sqlite3_reset: { 60 | args: [ 61 | "u64", // sqlite3_stmt *pStmt 62 | ], 63 | returns: "i32", 64 | }, 65 | 66 | sqlite3_step: { 67 | args: [ 68 | "u64", // sqlite3_stmt *pStmt 69 | ], 70 | returns: "i32", 71 | }, 72 | 73 | sqlite3_column_int: { 74 | args: [ 75 | "u64", // sqlite3_stmt *pStmt 76 | "i32", // int iCol 77 | ], 78 | returns: "i32", 79 | }, 80 | }); 81 | 82 | function unwrap(code) { 83 | if (code !== 0) { 84 | throw new Error(`SQLite3 error: ${sqlite3_errstr(code)}: ${code}`); 85 | } 86 | } 87 | 88 | const pHandle = new Uint32Array(2); 89 | const encoder = new TextEncoder(); 90 | 91 | unwrap( 92 | sqlite3_open_v2( 93 | ptr(encoder.encode(":memory:\0")), 94 | ptr(pHandle), 95 | SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_PRIVATECACHE | 96 | SQLITE3_OPEN_CREATE | SQLITE3_OPEN_MEMORY, 97 | 0, 98 | ), 99 | ); 100 | 101 | const db = pHandle[0] + 2 ** 32 * pHandle[1]; 102 | 103 | function exec(sql) { 104 | const _pErr = new Uint32Array(2); 105 | unwrap(sqlite3_exec(db, ptr(encoder.encode(sql + "\0")), 0, 0, ptr(_pErr))); 106 | } 107 | 108 | exec("PRAGMA auto_vacuum = none"); 109 | exec("PRAGMA temp_store = memory"); 110 | exec("PRAGMA locking_mode = exclusive"); 111 | exec("PRAGMA user_version = 100"); 112 | 113 | let total = parseInt(process.argv[2], 10); 114 | const runs = parseInt(process.argv[3], 10); 115 | 116 | function bench(query) { 117 | const start = performance.now(); 118 | for (let i = 0; i < runs; i++) query(); 119 | const elapsed = Math.floor(performance.now() - start); 120 | const rate = Math.floor(runs / (elapsed / 1000)); 121 | console.log(`time ${elapsed} ms rate ${rate}`); 122 | if (--total) process.nextTick(() => bench(query)); 123 | } 124 | 125 | function prepareStatement() { 126 | const pHandle = new Uint32Array(2); 127 | const s = encoder.encode("PRAGMA user_version"); 128 | unwrap( 129 | sqlite3_prepare_v2( 130 | db, 131 | ptr(s), 132 | s.byteLength, 133 | ptr(pHandle), 134 | 0, 135 | ), 136 | ); 137 | return pHandle[0] + 2 ** 32 * pHandle[1]; 138 | } 139 | 140 | const prepared = prepareStatement(); 141 | 142 | function run() { 143 | sqlite3_step(prepared); 144 | const int = sqlite3_column_int(prepared, 0); 145 | sqlite3_reset(prepared); 146 | return int; 147 | } 148 | 149 | console.log(`user_version: ${run()}`); 150 | 151 | bench(run); 152 | -------------------------------------------------------------------------------- /bench/bench_deno.js: -------------------------------------------------------------------------------- 1 | import { Database } from "../mod.ts"; 2 | 3 | const db = new Database(":memory:", { unsafeConcurrency: true }); 4 | 5 | db.run("PRAGMA auto_vacuum = none"); 6 | db.run("PRAGMA temp_store = memory"); 7 | db.run("PRAGMA locking_mode = exclusive"); 8 | db.run("PRAGMA user_version = 100"); 9 | 10 | const sql = "pragma user_version"; 11 | 12 | let total = parseInt(Deno.args[0], 10); 13 | const runs = parseInt(Deno.args[1], 10); 14 | 15 | function bench(query) { 16 | const start = performance.now(); 17 | for (let i = 0; i < runs; i++) query(); 18 | const elapsed = Math.floor(performance.now() - start); 19 | const rate = Math.floor(runs / (elapsed / 1000)); 20 | console.log(`time ${elapsed} ms rate ${rate}`); 21 | if (--total) bench(query); 22 | } 23 | 24 | const query = db.prepare(sql); 25 | bench(() => query.get()); 26 | -------------------------------------------------------------------------------- /bench/bench_deno_ffi.js: -------------------------------------------------------------------------------- 1 | import ffi from "../src/ffi.ts"; 2 | import { toCString, unwrap } from "../src/util.ts"; 3 | import { 4 | SQLITE3_OPEN_CREATE, 5 | SQLITE3_OPEN_MEMORY, 6 | SQLITE3_OPEN_PRIVATECACHE, 7 | SQLITE3_OPEN_READWRITE, 8 | } from "../src/constants.ts"; 9 | 10 | const { 11 | sqlite3_open_v2, 12 | sqlite3_exec, 13 | sqlite3_prepare_v2, 14 | sqlite3_reset, 15 | sqlite3_step, 16 | sqlite3_column_int, 17 | } = ffi; 18 | 19 | const pHandle = new Uint32Array(2); 20 | unwrap( 21 | sqlite3_open_v2( 22 | toCString(":memory:"), 23 | pHandle, 24 | SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_PRIVATECACHE | 25 | SQLITE3_OPEN_CREATE | SQLITE3_OPEN_MEMORY, 26 | null, 27 | ), 28 | ); 29 | const db = Deno.UnsafePointer.create(pHandle[0] + 2 ** 32 * pHandle[1]); 30 | 31 | function exec(sql) { 32 | const _pErr = new Uint32Array(2); 33 | unwrap(sqlite3_exec(db, toCString(sql), null, null, _pErr)); 34 | } 35 | 36 | exec("PRAGMA auto_vacuum = none"); 37 | exec("PRAGMA temp_store = memory"); 38 | exec("PRAGMA locking_mode = exclusive"); 39 | exec("PRAGMA user_version = 100"); 40 | 41 | const sql = "pragma user_version"; 42 | 43 | let total = parseInt(Deno.args[0], 10); 44 | const runs = parseInt(Deno.args[1], 10); 45 | 46 | function bench(query) { 47 | const start = performance.now(); 48 | for (let i = 0; i < runs; i++) query(); 49 | const elapsed = Math.floor(performance.now() - start); 50 | const rate = Math.floor(runs / (elapsed / 1000)); 51 | console.log(`time ${elapsed} ms rate ${rate}`); 52 | if (--total) bench(query); 53 | } 54 | 55 | function prepareStatement() { 56 | const pHandle = new Uint32Array(2); 57 | unwrap( 58 | sqlite3_prepare_v2( 59 | db, 60 | toCString(sql), 61 | sql.length, 62 | pHandle, 63 | null, 64 | ), 65 | ); 66 | return Deno.UnsafePointer.create(pHandle[0] + 2 ** 32 * pHandle[1]); 67 | } 68 | 69 | const prepared = prepareStatement(); 70 | function run() { 71 | sqlite3_step(prepared); 72 | const int = sqlite3_column_int(prepared, 0); 73 | sqlite3_reset(prepared); 74 | return int; 75 | } 76 | 77 | console.log(`user_version: ${run()}`); 78 | 79 | bench(run); 80 | -------------------------------------------------------------------------------- /bench/bench_deno_wasm.js: -------------------------------------------------------------------------------- 1 | import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts"; 2 | import { nextTick } from "https://deno.land/std@0.126.0/node/_next_tick.ts"; 3 | 4 | const db = new DB(":memory:"); 5 | 6 | db.query("PRAGMA auto_vacuum = none"); 7 | db.query("PRAGMA temp_store = memory"); 8 | db.query("PRAGMA locking_mode = exclusive"); 9 | db.query("PRAGMA user_version = 100"); 10 | 11 | const sql = "pragma user_version"; 12 | 13 | let total = parseInt(Deno.args[0], 10); 14 | const runs = parseInt(Deno.args[1], 10); 15 | 16 | function bench(query) { 17 | const start = performance.now(); 18 | for (let i = 0; i < runs; i++) query(); 19 | const elapsed = Math.floor(performance.now() - start); 20 | const rate = Math.floor(runs / (elapsed / 1000)); 21 | console.log(`time ${elapsed} ms rate ${rate}`); 22 | if (--total) nextTick(() => bench(query)); 23 | } 24 | 25 | const query = db.prepareQuery(sql); 26 | bench(() => query.one()); 27 | -------------------------------------------------------------------------------- /bench/bench_node.js: -------------------------------------------------------------------------------- 1 | import process from "node:process"; 2 | const db = require("better-sqlite3")(":memory:"); 3 | 4 | db.exec("PRAGMA auto_vacuum = none"); 5 | db.exec("PRAGMA temp_store = memory"); 6 | db.exec("PRAGMA locking_mode = exclusive"); 7 | db.exec("PRAGMA user_version = 100"); 8 | 9 | const sql = "pragma user_version"; 10 | 11 | function createQuery(sql) { 12 | return db.prepare(sql); 13 | } 14 | 15 | let total = parseInt(process.argv[2], 10); 16 | const runs = parseInt(process.argv[3], 10); 17 | 18 | function bench(query) { 19 | const start = Date.now(); 20 | for (let i = 0; i < runs; i++) query(); 21 | const elapsed = Date.now() - start; 22 | const rate = Math.floor(runs / (elapsed / 1000)); 23 | console.log(`time ${elapsed} ms rate ${rate}`); 24 | if (--total) process.nextTick(() => bench(query)); 25 | } 26 | 27 | const query = createQuery(sql); 28 | bench(() => query.get()); 29 | -------------------------------------------------------------------------------- /bench/bench_python.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import datetime 3 | 4 | conn = sqlite3.connect(":memory:") 5 | 6 | cursor = conn.cursor() 7 | 8 | cursor.execute("PRAGMA auto_vacuum = none") 9 | cursor.execute("PRAGMA temp_store = memory") 10 | cursor.execute("PRAGMA locking_mode = exclusive") 11 | cursor.execute("PRAGMA user_version = 100") 12 | 13 | conn.commit() 14 | 15 | runs = 1000000 16 | def bench(): 17 | start = datetime.datetime.now() 18 | for i in range(runs): 19 | cursor.execute("pragma user_version") 20 | cursor.fetchone() 21 | 22 | elapsed = datetime.datetime.now() - start; 23 | rate = runs / elapsed.total_seconds() 24 | print("time {} ms rate {}".format(round(elapsed.total_seconds() * 1000), round(rate))) 25 | 26 | for _ in range(50): 27 | bench() -------------------------------------------------------------------------------- /bench/download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | if [ -f src/northwind.sqlite ]; then 5 | exit 0 6 | fi 7 | 8 | curl -LJO https://raw.githubusercontent.com/jpwhite3/northwind-SQLite3/46d5f8a64f396f87cd374d1600dbf521523980e8/Northwind_large.sqlite.zip 9 | 10 | unzip Northwind_large.sqlite.zip 11 | 12 | rm Northwind_large.sqlite.zip 13 | mv Northwind_large.sqlite ./northwind.sqlite 14 | 15 | rm -rf __MACOSX 16 | rm -rf Northwind* || echo "" -------------------------------------------------------------------------------- /bench/makefile: -------------------------------------------------------------------------------- 1 | build-c: 2 | gcc -Wall -Wextra -pthread -o bench bench.c -l:libsqlite3.a -ldl -lm 3 | -------------------------------------------------------------------------------- /bench/northwind/bun.js: -------------------------------------------------------------------------------- 1 | import { bench, run } from "mitata"; 2 | import { Database } from "bun:sqlite"; 3 | 4 | const db = Database.open("./bench/northwind.sqlite"); 5 | 6 | { 7 | const sql = db.prepare(`SELECT * FROM "Order"`); 8 | bench('SELECT * FROM "Order" all', () => { 9 | sql.all(); 10 | }); 11 | bench('SELECT * FROM "Order" values', () => { 12 | sql.values(); 13 | }); 14 | bench('SELECT * FROM "Order" run', () => { 15 | sql.run(); 16 | }); 17 | } 18 | 19 | { 20 | const sql = db.prepare(`SELECT * FROM "Product"`); 21 | bench('SELECT * FROM "Product" all', () => { 22 | sql.all(); 23 | }); 24 | bench('SELECT * FROM "Product" values', () => { 25 | sql.values(); 26 | }); 27 | bench('SELECT * FROM "Product" run', () => { 28 | sql.run(); 29 | }); 30 | } 31 | 32 | { 33 | const sql = db.prepare(`SELECT * FROM "OrderDetail"`); 34 | bench('SELECT * FROM "OrderDetail" all', () => { 35 | sql.all(); 36 | }); 37 | bench('SELECT * FROM "OrderDetail" values', () => { 38 | sql.values(); 39 | }); 40 | bench('SELECT * FROM "OrderDetail" run', () => { 41 | sql.run(); 42 | }); 43 | } 44 | 45 | await run(); 46 | -------------------------------------------------------------------------------- /bench/northwind/deno.js: -------------------------------------------------------------------------------- 1 | import { Database } from "../../mod.ts"; 2 | 3 | const db = new Database("./bench/northwind.sqlite", { 4 | unsafeConcurrency: true, 5 | }); 6 | 7 | { 8 | const sql = db.prepare(`SELECT * FROM "Order"`); 9 | 10 | Deno.bench('SELECT * FROM "Order" all', () => { 11 | sql.all(); 12 | }); 13 | 14 | Deno.bench('SELECT * FROM "Order" values', () => { 15 | sql.values(); 16 | }); 17 | 18 | Deno.bench('SELECT * FROM "Order" run', () => { 19 | sql.run(); 20 | }); 21 | } 22 | 23 | { 24 | const sql = db.prepare(`SELECT * FROM "Product"`); 25 | 26 | Deno.bench('SELECT * FROM "Product" all', () => { 27 | sql.all(); 28 | }); 29 | 30 | Deno.bench('SELECT * FROM "Product" values', () => { 31 | sql.values(); 32 | }); 33 | 34 | Deno.bench('SELECT * FROM "Product" run', () => { 35 | sql.run(); 36 | }); 37 | } 38 | 39 | { 40 | const sql = db.prepare(`SELECT * FROM "OrderDetail"`); 41 | 42 | Deno.bench('SELECT * FROM "OrderDetail" all', () => { 43 | sql.all(); 44 | }); 45 | 46 | Deno.bench('SELECT * FROM "OrderDetail" values', () => { 47 | sql.values(); 48 | }); 49 | 50 | Deno.bench('SELECT * FROM "OrderDetail" run', () => { 51 | sql.run(); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /bench/northwind/deno_old.js: -------------------------------------------------------------------------------- 1 | import { Database } from "https://deno.land/x/sqlite3@0.4.3/mod.ts"; 2 | import { bench, run } from "../../node_modules/mitata/src/cli.mjs"; 3 | 4 | const db = new Database("./bench/northwind.sqlite"); 5 | 6 | { 7 | const sql = db.prepare(`SELECT * FROM "Order"`); 8 | bench('SELECT * FROM "Order"', () => { 9 | sql.reset(); 10 | const rows = []; 11 | for (const row of sql) { 12 | rows.push(row.asObject()); 13 | } 14 | }); 15 | } 16 | 17 | { 18 | const sql = db.prepare(`SELECT * FROM "Product"`); 19 | bench('SELECT * FROM "Product"', () => { 20 | sql.reset(); 21 | const rows = []; 22 | for (const row of sql) { 23 | rows.push(row.asObject()); 24 | } 25 | }); 26 | } 27 | 28 | { 29 | const sql = db.prepare(`SELECT * FROM "OrderDetail"`); 30 | bench('SELECT * FROM "OrderDetail"', () => { 31 | sql.reset(); 32 | const rows = []; 33 | for (const row of sql) { 34 | rows.push(row.asObject()); 35 | } 36 | }); 37 | } 38 | 39 | await run(); 40 | -------------------------------------------------------------------------------- /bench/northwind/deno_wasm.js: -------------------------------------------------------------------------------- 1 | import { DB } from "https://deno.land/x/sqlite@v3.4.1/mod.ts"; 2 | import { bench, run } from "https://esm.sh/mitata"; 3 | 4 | const db = new DB("./bench/northwind.sqlite"); 5 | 6 | { 7 | const sql = db.prepareQuery(`SELECT * FROM "Order"`); 8 | bench('SELECT * FROM "Order" all', () => { 9 | sql.all(); 10 | }); 11 | bench('SELECT * FROM "Order" values', () => { 12 | sql.allEntries(); 13 | }); 14 | bench('SELECT * FROM "Order" run', () => { 15 | sql.execute(); 16 | }); 17 | } 18 | 19 | { 20 | const sql = db.prepareQuery(`SELECT * FROM "Product"`); 21 | bench('SELECT * FROM "Product" all', () => { 22 | sql.all(); 23 | }); 24 | bench('SELECT * FROM "Product" values', () => { 25 | sql.allEntries(); 26 | }); 27 | bench('SELECT * FROM "Product" run', () => { 28 | sql.execute(); 29 | }); 30 | } 31 | 32 | { 33 | const sql = db.prepareQuery(`SELECT * FROM "OrderDetail"`); 34 | bench('SELECT * FROM "OrderDetail" all', () => { 35 | sql.all(); 36 | }); 37 | bench('SELECT * FROM "OrderDetail" values', () => { 38 | sql.allEntries(); 39 | }); 40 | bench('SELECT * FROM "OrderDetail" run', () => { 41 | sql.execute(); 42 | }); 43 | } 44 | 45 | await run(); 46 | -------------------------------------------------------------------------------- /bench/northwind/node.mjs: -------------------------------------------------------------------------------- 1 | import { bench, run } from "mitata"; 2 | import { createRequire } from "node:module"; 3 | 4 | const require = createRequire(import.meta.url); 5 | const db = require("better-sqlite3")("./bench/northwind.sqlite"); 6 | 7 | { 8 | const sql = db.prepare(`SELECT * FROM "Order"`); 9 | 10 | bench('SELECT * FROM "Order" all', () => { 11 | sql.all(); 12 | }); 13 | 14 | bench('SELECT * FROM "Order" values', () => { 15 | sql.raw(true); 16 | sql.all(); 17 | }); 18 | 19 | bench('SELECT * FROM "Order" run', () => { 20 | sql.run(); 21 | }); 22 | } 23 | 24 | { 25 | const sql = db.prepare(`SELECT * FROM "Product"`); 26 | 27 | bench('SELECT * FROM "Product" all', () => { 28 | sql.all(); 29 | }); 30 | 31 | bench('SELECT * FROM "Product" values', () => { 32 | sql.raw(true); 33 | sql.all(); 34 | }); 35 | 36 | bench('SELECT * FROM "Product" run', () => { 37 | sql.run(); 38 | }); 39 | } 40 | 41 | { 42 | const sql = db.prepare(`SELECT * FROM "OrderDetail"`); 43 | 44 | bench('SELECT * FROM "OrderDetail" all', () => { 45 | sql.all(); 46 | }); 47 | 48 | bench('SELECT * FROM "OrderDetail" values', () => { 49 | sql.raw(true); 50 | sql.all(); 51 | }); 52 | 53 | bench('SELECT * FROM "OrderDetail" run', () => { 54 | sql.run(); 55 | }); 56 | } 57 | 58 | await run(); 59 | -------------------------------------------------------------------------------- /bench/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denodrivers/sqlite3/dc59c15c7d1c63b1155be475be07756e4e35feff/bench/results.png -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@db/sqlite", 3 | "version": "0.12.0", 4 | "github": "https://github.com/denodrivers/sqlite3", 5 | 6 | "exports": "./mod.ts", 7 | 8 | "exclude": [ 9 | "sqlite", 10 | "scripts" 11 | ], 12 | 13 | "tasks": { 14 | "test": "deno test -A test/test.ts", 15 | "build": "deno run -A scripts/build.ts", 16 | "bench-deno": "deno run -A bench/bench_deno.js 50 1000000", 17 | "bench-deno-ffi": "deno run -A bench/bench_deno_ffi.js 50 1000000", 18 | "bench-deno-wasm": "deno run -A bench/bench_deno_wasm.js 50 1000000", 19 | "bench-node": "node bench/bench_node.js 50 1000000", 20 | "bench-bun": "bun run bench/bench_bun.js 50 1000000", 21 | "bench-bun-ffi": "bun run bench/bench_bun_ffi.js 50 1000000", 22 | "bench-c": "./bench/bench 50 1000000", 23 | "bench-python": "python ./bench/bench_python.py", 24 | "bench:northwind": "deno bench -A bench/northwind/deno.js", 25 | "bench-wasm:northwind": "deno run -A bench/northwind/deno_wasm.js", 26 | "bench-node:northwind": "node bench/northwind/node.mjs", 27 | "bench-bun:northwind": "bun run bench/northwind/bun.js" 28 | }, 29 | 30 | "lint": { 31 | "exclude": ["bench"], 32 | "rules": { 33 | "exclude": [ 34 | "camelcase", 35 | "no-explicit-any" 36 | ], 37 | "include": [ 38 | "explicit-function-return-type", 39 | "eqeqeq", 40 | "explicit-module-boundary-types" 41 | ] 42 | } 43 | }, 44 | "compilerOptions": { 45 | "types": [ 46 | "./node_modules/bun-types/index.d.ts" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { fromFileUrl } from "jsr:@std/path@1.0"; 2 | export { dlopen } from "jsr:@denosaurs/plug@1"; 3 | -------------------------------------------------------------------------------- /doc.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Opening Database 4 | 5 | To open a new Database connection, construct the `Database` class with the path 6 | to the database file. If the file does not exist, it will be created unless you 7 | pass `create: false` in the options. 8 | 9 | ### Options 10 | 11 | - `create: boolean` - Whether to create the database file if it does not exist. 12 | Defaults to `true`. 13 | - `readonly: boolean` - Whether to open the database in read-only mode. Defaults 14 | to `false`. 15 | - `memory: boolean` - Whether to open the database in memory. Defaults to 16 | `false`. 17 | - `int64: boolean` - Whether to support BigInt columns. False by default, which 18 | means integers larger than 32 bit will be inaccurate. Causes mixed values to 19 | be returned like `number | bigint`. 20 | - `flags: number` - Raw flags to pass to the C API. Normally you don't need 21 | this. Passing this ignores all other options. 22 | - `unsafeConcurrency: boolean` - Enable optimizations that will affect 23 | syncronization with other clients. This can largerly improve performance for 24 | cases where you only have one client. 25 | - `enableLoadExtension: boolean` - Enables the loading of SQLite extensions from 26 | a dynamic library, this needs to be set to true for the method `loadExtension` 27 | to work. Defaults to `false`. 28 | - `parseJson: boolean` - Enables parsing JSON. True by default. 29 | 30 | ### Usage 31 | 32 | ```ts 33 | // Open using default options 34 | const db = new Database("test.db"); 35 | 36 | // Open using URL path (relative to current file/module, not CWD) 37 | const db = new Database(new URL("./test.db", import.meta.url)); 38 | 39 | // Open in memory 40 | const db = new Database(":memory:"); 41 | 42 | // Open in read-only mode 43 | const db = new Database("test.db", { readonly: true }); 44 | 45 | // Open existing database, error if it doesn't exist 46 | const db = new Database("test.db", { create: false }); 47 | ``` 48 | 49 | ## Properties of `Database` 50 | 51 | - `inTransaction: boolean` - Whether the database is currently in a transaction. 52 | - `open: boolean` - Whether the database connection is open. 53 | - `path: string` - The path to the database file (not full path, just the once 54 | passed to the constructor). 55 | - `totalChanges: number` - The total number of changes made to the database 56 | since it was opened. 57 | - `changes: number` - The number of changes made to the database by last 58 | executed statement. 59 | - `lastInsertRowId: number` - The rowid of the last inserted row. 60 | - `autocommit: boolean` - Whether the database is in autocommit mode. This is 61 | `true` when not in a transaction, and `false` when in a transaction. 62 | - `enableLoadExtension: boolean` - Enables the loading of SQLite extensions from 63 | a dynamic library, this needs to be set to true for the method `loadExtension` 64 | to work. Defaults to `false`. 65 | 66 | ## Loading extensions 67 | 68 | Loading SQLite3 extensions is enabled through the `enableLoadExtension` property 69 | and config option. For security reasons it is disabled by default. If enabled it 70 | is used with the `loadExtension` method on the database, it will attempt to load 71 | the specified file as specified by the 72 | [SQLite documentation](https://www.sqlite.org/c3ref/load_extension.html). 73 | Optionally a second argument can be passed to the method specifying the 74 | entrypoint name. 75 | 76 | ```ts 77 | const db = new Database("test.db", { enableLoadExtension: true }); 78 | 79 | db.loadExtension("mod_spatialite"); 80 | ``` 81 | 82 | It is also possible to load an extension directly from SQL using the 83 | `load_extension` functions as specified by the 84 | [SQLite documentation](https://www.sqlite.org/lang_corefunc.html#load_extension). 85 | 86 | ```ts 87 | db.exec("SELECT load_extension('mod_spatialite')"); 88 | ``` 89 | 90 | ## Closing Database 91 | 92 | To close the database connection, call the `close()` method. This will close the 93 | database connection and free all resources associated with it. Calling it more 94 | than once will be a no-op. 95 | 96 | ```ts 97 | db.close(); 98 | ``` 99 | 100 | ## Executing SQL 101 | 102 | To execute SQL statements, use the `exec()` method. This method will execute all 103 | statements in the SQL string, and return the number of changes made by the last 104 | statement. This method is useful for executing DDL statements, such as `CREATE`, 105 | `DROP`, `ALTER`, and even pragma statements that do not return any data. 106 | 107 | ```ts 108 | const changes = db.exec( 109 | "CREATE TABLE foo (bar TEXT); INSERT INTO foo VALUES ('baz');", 110 | ); 111 | 112 | console.log(changes); // 1 113 | 114 | // Executing pragma statements 115 | db.exec("pragma journal_mode = WAL"); 116 | db.exec("pragma synchronous = normal"); 117 | db.exec("pragma temp_store = memory"); 118 | ``` 119 | 120 | Any parameters past the first argument will be bound to the statement. When you 121 | pass parameters, the function under the hood instead uses the prepared statement 122 | API. 123 | 124 | Note that when the prepared statement API is used, this method only supports one 125 | statement at a time. You cannot execute multiple statements AND pass parameters 126 | at the same time. 127 | 128 | See [Binding Parameters](#binding-parameters) for more details. 129 | 130 | Alternatively, use the `.sql` tagged template to safely execute SQL with given 131 | parameters. It will execute the given SQL with parameters bounded and returns 132 | all rows with `.all()`. 133 | 134 | ```ts 135 | const minimum = 20; 136 | const results = db.sql` 137 | SELECT 138 | id, 139 | name, 140 | age 141 | FROM students 142 | WHERE age > ${minimum}`; 143 | 144 | console.log(results); // [ [ 2, "Brian", 30 ] ] 145 | ``` 146 | 147 | ## Creating Prepared Statements 148 | 149 | To prepare a statement, use the `prepare()` method. This method will return a 150 | `Statement` object, which can be used to execute it, bind the parameters, 151 | retrieve the results, and more. 152 | 153 | ```ts 154 | const stmt = db.prepare("SELECT * FROM foo WHERE bar = ? AND baz = ?"); 155 | 156 | // or with a using statement 157 | 158 | { 159 | using stmt = db.prepare("SELECT * FROM foo WHERE bar = ? AND baz = ?"); 160 | // use stmt 161 | } 162 | 163 | // automatically disposed 164 | ``` 165 | 166 | For more details on binding parameters, see 167 | [Binding Parameters](#binding-parameters). 168 | 169 | ## Properties of `Statement` 170 | 171 | - `db: Database` - The database the statement belongs to. 172 | - `expandedSql: string` - The SQL string with all bound parameters expanded. 173 | - `sql: string` - The SQL string used to prepare the statement. 174 | - `readonly: boolean` - Whether the statement is read-only. 175 | - `bindParameterCount: number` - The number of parameters the statement expects. 176 | 177 | You can use `enableInt64`, `enableParseJson`, `disableInt64`, 178 | `disableParseJson`, `defaultInt64`, `defaultParseJson` at statement level to 179 | override the behavior just for this statement and not whole database. Default 180 | falls back to database, not the default value for these options. 181 | 182 | ## Executing Statement 183 | 184 | To execute a statement, use the `run()` method. This method will execute the 185 | statement, and return the number of changes made by the statement. 186 | 187 | ```ts 188 | const changes = stmt.run(...params); 189 | ``` 190 | 191 | ## Retrieving Rows 192 | 193 | To retrieve rows from a statement, use the `all()` method. This method will 194 | execute the statement, and return an array of rows as objects. 195 | 196 | ```ts 197 | const rows = stmt.all(...params); 198 | ``` 199 | 200 | To get rows in array form, use `values()` method. 201 | 202 | ```ts 203 | const rows = stmt.values(...params); 204 | ``` 205 | 206 | To get only the first row as object, use the `get()` method. 207 | 208 | ```ts 209 | const row = stmt.get(...params); 210 | ``` 211 | 212 | To get only the first row as array, use the `value()` method. 213 | 214 | ```ts 215 | const row = stmt.value(...params); 216 | ``` 217 | 218 | `all`/`values`/`get`/`value` methods also support a generic type parameter to 219 | specify the type of the returned object. 220 | 221 | ```ts 222 | interface Foo { 223 | bar: string; 224 | baz: number; 225 | } 226 | 227 | const rows = stmt.all(...params); 228 | // rows is Foo[] 229 | 230 | const row = stmt.get(...params); 231 | // row is Foo | undefined 232 | 233 | const values = stmt.values<[string, number]>(...params); 234 | // values is [string, number][] 235 | 236 | const row = stmt.value<[string, number]>(...params); 237 | // row is [string, number] | undefined 238 | ``` 239 | 240 | ## SQLite functions that return JSON 241 | 242 | When using [SQLite's builtin JSON functions](https://www.sqlite.org/json1.html), 243 | `sqlite3` will detect when a value has a "subtype" of JSON. If so, it will 244 | attempt to `JSON.parse()` the text value and return the parsed JavaScript object 245 | or array. 246 | 247 | ```ts 248 | const [list] = db 249 | .prepare("SELECT json_array(1, 2, 3) as list") 250 | .value<[number[]]>()!; 251 | // list = [ 1, 2, 3 ] 252 | 253 | const [object] = db 254 | .prepare("SELECT json_object('name', 'Peter') as object") 255 | .value<[{ name: string }]>()!; 256 | 257 | // object = { name: "Peter" } 258 | ``` 259 | 260 | Use the builtin [`json()`](https://www.sqlite.org/json1.html#jmini) SQL function 261 | to convert your text values into JSON. 262 | 263 | ## Freeing Prepared Statements 264 | 265 | Though the `Statement` object is automatically freed once it is no longer used, 266 | that is it's caught by the garbage collector, you can also free it manually by 267 | calling the `finalize()` method. Do not use the `Statement` object after calling 268 | this method. 269 | 270 | ```ts 271 | stmt.finalize(); 272 | ``` 273 | 274 | You can also use `using` statement to automatically free the statement once the 275 | scope ends. 276 | 277 | ```ts 278 | { 279 | using stmt = db.prepare("SELECT * FROM foo WHERE bar = ? AND baz = ?"); 280 | stmt.run("bar", "baz"); 281 | } 282 | 283 | // stmt is automatically finalized here 284 | ``` 285 | 286 | ## Setting fixed parameters 287 | 288 | To set fixed parameters for a statement, use the `bind()` method. This method 289 | will set the parameters for the statement, and return the statement itself. 290 | 291 | It can only be called once and once it is called, changing the parameters is not 292 | possible. It's merely an optimization to avoid having to bind the parameters 293 | every time the statement is executed. 294 | 295 | ```ts 296 | const stmt = db.prepare("SELECT * FROM foo WHERE bar = ? AND baz = ?"); 297 | stmt.bind("bar", "baz"); 298 | ``` 299 | 300 | ## Iterating over Statement 301 | 302 | If you iterate over the statement object itself, it will iterate over the rows 303 | step by step. This is useful when you don't want to load all the rows into 304 | memory at once. Since it does not accept any parameters, you must bind the 305 | parameters before iterating using `bind` method. 306 | 307 | ```ts 308 | for (const row of stmt) { 309 | console.log(row); 310 | } 311 | ``` 312 | 313 | ## Transactions 314 | 315 | To start a transaction, use the `transaction()` method. This method takes a 316 | JavaScript function that will be called when the transaction is run. This method 317 | itself returns a function that can be called to run the transaction. 318 | 319 | When the transaction function is called, `BEGIN` is automatically called. When 320 | the transaction function returns, `COMMIT` is automatically called. If the 321 | transaction function throws an error, `ROLLBACK` is called. 322 | 323 | If the transaction is called within another transaction, it will use 324 | `SAVEPOINT`/`RELEASE`/`ROLLBACK TO` instead of `BEGIN`/`COMMIT`/`ROLLBACK` to 325 | create a nested transaction. 326 | 327 | The returned function also contains `deferred`/`immediate`/`exclusive` 328 | properties (functions) which can be used to change `BEGIN` to 329 | `BEGIN DEFERRED`/`BEGIN IMMEDIATE`/`BEGIN EXCLUSIVE`. 330 | 331 | ```ts 332 | const stmt = db.prepare("INSERT INTO foo VALUES (?)"); 333 | const runTransaction = db.transaction((data: SomeData[]) => { 334 | for (const item of data) { 335 | stmt.run(item.value); 336 | } 337 | }); 338 | 339 | runTransaction([ 340 | { value: "bar" }, 341 | { value: "baz" }, 342 | ]); 343 | 344 | // Run with BEGIN DEFERRED 345 | 346 | runTransaction.deferred([ 347 | { value: "bar" }, 348 | { value: "baz" }, 349 | ]); 350 | ``` 351 | 352 | ## Binding Parameters 353 | 354 | Parameters can be bound both by name and positiion. To bind by name, just pass 355 | an object mapping the parameter name to the value. To bind by position, pass the 356 | values as rest parameters. 357 | 358 | SQLite supports `:`, `@` and `$` as prefix for named bind parameters. If you 359 | don't have any in the Object's keys, the `:` prefix will be used by default. 360 | 361 | Bind parameters can be passed to `Database#exec` after SQL parameter, or to 362 | `Statement`'s `bind`/`all`/`values`/`run` function. 363 | 364 | ```ts 365 | // Bind by name 366 | db.exec("INSERT INTO foo VALUES (:bar)", { bar: "baz" }); 367 | 368 | // In prepared statements 369 | const stmt = db.prepare("INSERT INTO foo VALUES (:bar)"); 370 | stmt.run({ bar: "baz" }); 371 | 372 | // Bind by position 373 | db.exec("INSERT INTO foo VALUES (?)", "baz"); 374 | 375 | // In prepared statements 376 | const stmt = db.prepare("INSERT INTO foo VALUES (?, ?)"); 377 | stmt.run("baz", "foo"); 378 | ``` 379 | 380 | JavaScript to SQLite type mapping: 381 | 382 | | JavaScript type | SQLite type | 383 | | ----------------------- | --------------------------- | 384 | | `null` | `NULL` | 385 | | `undefined` | `NULL` | 386 | | `number` | `INTEGER`/`FLOAT` | 387 | | `bigint` | `INTEGER` | 388 | | `string` | `TEXT` | 389 | | `boolean` | `INTEGER` | 390 | | `Date` | `TEXT` (ISO) | 391 | | `Uint8Array` | `BLOB` | 392 | | JSON-serializable value | `TEXT` (`JSON.stringify()`) | 393 | 394 | When retrieving rows, the types are mapped back to JavaScript types: 395 | 396 | | SQLite type | JavaScript type | 397 | | ------------------------ | ------------------------- | 398 | | `NULL` | `null` | 399 | | `INTEGER` | `number`/`bigint` | 400 | | `FLOAT` | `number` | 401 | | `TEXT` | `string` | 402 | | `TEXT` with JSON subtype | `object` (`JSON.parse()`) | 403 | | `BLOB` | `Uint8Array` | 404 | 405 | Note 1: The `int64` option allows you to return `BigInt` for integers bigger 406 | than max safe integer in JavaScript. It is disabled by default, and precision 407 | may not be accurate for bigger numbers. When enabled, the library can return 408 | both `number | bigint`, but when disabled (default), it will only return 409 | `number`. In the former case, `bigint` is returned ONLY if its too big to fit in 410 | a JavaScript Number. 411 | 412 | Note 2: We only support `Uint8Array` for the `BLOB` type as V8 Fast API will 413 | optimize for it instead of other arrays like `Uint16Array`. And it is also to 414 | stay consistent: we only support passing `Uint8Array` and we consistently return 415 | `Uint8Array` when we return a `BLOB` to JS. It is easy to support passing all 416 | typed arrays with good performance, but then at the time we have to retreive 417 | again we don't know what the original typed array type was, as the only type 418 | into in the column is that it is a `BLOB`. 419 | 420 | You can easily convert between other typed arrays and `Uint8Array` like this: 421 | 422 | ```ts 423 | const f32 = new Float32Array(1); 424 | const u8 = new Uint8Array(f32.buffer); // no copy, can pass this 425 | 426 | const u8FromSqlite = new Uint8Array(4); 427 | const f32FromSqlite = new Float32Array(u8FromSqlite.buffer); // safely convert back when retrieved from sqlite, no copy 428 | ``` 429 | 430 | Note 3: The `parseJson` option allows you to disable JSON parsing which is 431 | enabled by default. 432 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export { 2 | type AggregateFunctionOptions, 3 | Database, 4 | type DatabaseOpenOptions, 5 | type FunctionOptions, 6 | isComplete, 7 | SQLITE_SOURCEID, 8 | SQLITE_VERSION, 9 | type Transaction, 10 | } from "./src/database.ts"; 11 | export { type BlobOpenOptions, SQLBlob } from "./src/blob.ts"; 12 | export { 13 | type BindParameters, 14 | type BindValue, 15 | type RestBindParameters, 16 | Statement, 17 | } from "./src/statement.ts"; 18 | export { SqliteError } from "./src/util.ts"; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlite3", 3 | "version": "1.0.0", 4 | "description": "[![Tags](https://img.shields.io/github/release/denodrivers/sqlite3)](https://github.com/denodrivers/sqlite3/releases) [![Doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/sqlite3@0.4.3/mod.ts) [![Checks](https://github.com/denodrivers/sqlite3/actions/workflows/ci.yml/badge.svg)](https://github.com/denodrivers/sqlite3/actions/workflows/ci.yml) [![License](https://img.shields.io/github/license/denodrivers/sqlite3)](https://github.com/denodrivers/sqlite3/blob/master/LICENSE)", 5 | "main": "bunbench.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/denodrivers/sqlite3.git" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/denodrivers/sqlite3/issues" 21 | }, 22 | "homepage": "https://github.com/denodrivers/sqlite3#readme", 23 | "dependencies": { 24 | "better-sqlite3": "^7.6.2", 25 | "mitata": "^0.1.6" 26 | }, 27 | "devDependencies": { 28 | "bun-types": "^1.2.14" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | const ARCH = Deno.env.get("TARGET_ARCH") || Deno.build.arch; 2 | 3 | const COMPILE_OPTIONS: Record = { 4 | SQLITE_DQS: "0", 5 | SQLITE_DEFAULT_MEMSTATUS: "0", 6 | SQLITE_DEFAULT_WAL_SYNCHRONOUS: "1", 7 | SQLITE_OMIT_DEPRECATED: "1", 8 | SQLITE_OMIT_PROGRESS_CALLBACK: "1", 9 | SQLITE_OMIT_SHARED_CACHE: "1", 10 | SQLITE_OMIT_AUTOINIT: "1", 11 | SQLITE_LIKE_DOESNT_MATCH_BLOBS: "1", 12 | SQLITE_DEFAULT_CACHE_SIZE: "-16000", 13 | SQLITE_ENABLE_DESERIALIZE: "1", 14 | SQLITE_ENABLE_FTS3: "1", 15 | SQLITE_ENABLE_FTS3_PARENTHESIS: "1", 16 | SQLITE_ENABLE_FTS4: "1", 17 | SQLITE_ENABLE_FTS5: "1", 18 | SQLITE_ENABLE_GEOPOLY: "1", 19 | SQLITE_ENABLE_JSON1: "1", 20 | SQLITE_ENABLE_MATH_FUNCTIONS: "1", 21 | SQLITE_ENABLE_RTREE: "1", 22 | SQLITE_ENABLE_STAT4: "1", 23 | SQLITE_ENABLE_UPDATE_DELETE_LIMIT: "1", 24 | SQLITE_OMIT_TCL_VARIABLE: "1", 25 | SQLITE_OMIT_GET_TABLE: "1", 26 | SQLITE_SOUNDEX: "1", 27 | SQLITE_TRACE_SIZE_LIMIT: "32", 28 | SQLITE_ENABLE_COLUMN_METADATA: "1", 29 | SQLITE_DEFAULT_FOREIGN_KEYS: "1", 30 | }; 31 | 32 | const prefix = Deno.build.os === "windows" ? "" : "lib"; 33 | const ext = Deno.build.os === "windows" 34 | ? "dll" 35 | : Deno.build.os === "darwin" 36 | ? "dylib" 37 | : "so"; 38 | const lib = `${prefix}sqlite3.${ext}`; 39 | const libWithArch = `${prefix}sqlite3${ 40 | ARCH !== "x86_64" ? `_${ARCH}` : "" 41 | }.${ext}`; 42 | 43 | const SLICE_WIN = Deno.build.os === "windows" ? 1 : 0; 44 | 45 | const $ = (cmd: string | URL, ...args: string[]) => { 46 | console.log(`%c$ ${cmd.toString()} ${args.join(" ")}`, "color: #888"); 47 | const c = typeof cmd === "string" ? cmd : cmd.pathname.slice(SLICE_WIN); 48 | new Deno.Command(c, { 49 | args, 50 | stdin: "null", 51 | stdout: "inherit", 52 | stderr: "inherit", 53 | }).outputSync(); 54 | }; 55 | 56 | await Deno.remove(new URL("../sqlite/build", import.meta.url), { 57 | recursive: true, 58 | }) 59 | .catch(() => {}); 60 | await Deno.mkdir(new URL("../build", import.meta.url)).catch(() => {}); 61 | await Deno.mkdir(new URL("../sqlite/build", import.meta.url)); 62 | 63 | if (Deno.build.os !== "windows") { 64 | COMPILE_OPTIONS["SQLITE_OS_UNIX"] = "1"; 65 | } 66 | 67 | const CFLAGS = `${ 68 | Deno.build.os === "windows" ? "OPT_FEATURE_FLAGS" : "CFLAGS" 69 | }=${Deno.build.os === "windows" ? "" : "-g -O3 -fPIC "}${ 70 | Object.entries( 71 | COMPILE_OPTIONS, 72 | ) 73 | .map(([k, v]) => `-D${k}=${v}`) 74 | .join(" ") 75 | }`; 76 | 77 | if (Deno.build.os === "windows") { 78 | Deno.chdir(new URL("../sqlite/build", import.meta.url)); 79 | $( 80 | "nmake", 81 | "/f", 82 | "..\\Makefile.msc", 83 | "sqlite3.dll", 84 | "TOP=..\\", 85 | CFLAGS, 86 | ); 87 | await Deno.copyFile( 88 | new URL(`../sqlite/build/${lib}`, import.meta.url), 89 | new URL(`../build/${libWithArch}`, import.meta.url), 90 | ); 91 | } else { 92 | Deno.chdir(new URL("../sqlite/build", import.meta.url)); 93 | $( 94 | new URL("../sqlite/configure", import.meta.url), 95 | "--enable-releasemode", 96 | "--enable-update-limit", 97 | ...(Deno.build.arch === ARCH || Deno.build.os !== "linux" 98 | ? [] 99 | : ["--disable-tcl", "--host=arm-linux"]), 100 | ); 101 | $( 102 | "make", 103 | "-j", 104 | "8", 105 | CFLAGS, 106 | ); 107 | await Deno.copyFile( 108 | new URL(`../sqlite/build/.libs/${lib}`, import.meta.url), 109 | new URL(`../build/${libWithArch}`, import.meta.url), 110 | ); 111 | } 112 | 113 | console.log(`%c${libWithArch} built`, "color: #0f0"); 114 | -------------------------------------------------------------------------------- /src/blob.ts: -------------------------------------------------------------------------------- 1 | import type { Database } from "./database.ts"; 2 | import ffi from "./ffi.ts"; 3 | import { toCString, unwrap } from "./util.ts"; 4 | 5 | const { 6 | sqlite3_blob_open, 7 | sqlite3_blob_bytes, 8 | sqlite3_blob_close, 9 | sqlite3_blob_read, 10 | sqlite3_blob_write, 11 | } = ffi; 12 | 13 | /** Various options that can be configured when opening a Blob via `Database#openBlob`. */ 14 | export interface BlobOpenOptions { 15 | /** Whether to open Blob in readonly mode. True by default. */ 16 | readonly?: boolean; 17 | /** Database to open Blob from, "main" by default. */ 18 | db?: string; 19 | /** Table the Blob is in */ 20 | table: string; 21 | /** Column name of the Blob Field */ 22 | column: string; 23 | /** Row ID of which column to select */ 24 | row: number; 25 | } 26 | 27 | /** 28 | * Enumerates SQLite3 Blob opened for streamed I/O. 29 | * 30 | * BLOB columns still return a `Uint8Array` of the data. 31 | * You can instead open this from `Database.openBlob()`. 32 | * 33 | * @see https://www.sqlite.org/c3ref/blob_open.html 34 | */ 35 | export class SQLBlob { 36 | #handle: Deno.PointerValue; 37 | 38 | constructor(db: Database, options: BlobOpenOptions) { 39 | options = Object.assign({ 40 | readonly: true, 41 | db: "main", 42 | }, options); 43 | const pHandle = new BigUint64Array(1); 44 | unwrap(sqlite3_blob_open( 45 | db.unsafeHandle, 46 | toCString(options.db ?? "main"), 47 | toCString(options.table), 48 | toCString(options.column), 49 | BigInt(options.row), 50 | options.readonly === false ? 1 : 0, 51 | pHandle, 52 | )); 53 | this.#handle = Deno.UnsafePointer.create(pHandle[0]); 54 | } 55 | 56 | /** Byte size of the Blob */ 57 | get byteLength(): number { 58 | return sqlite3_blob_bytes(this.#handle); 59 | } 60 | 61 | /** Read from the Blob at given offset into a buffer (Uint8Array) */ 62 | readSync(offset: number, p: Uint8Array): void { 63 | unwrap(sqlite3_blob_read(this.#handle, p, p.byteLength, offset)); 64 | } 65 | 66 | /** Write a buffer (Uint8Array) at given offset in the Blob */ 67 | writeSync(offset: number, p: Uint8Array): void { 68 | unwrap(sqlite3_blob_write(this.#handle, p, p.byteLength, offset)); 69 | } 70 | 71 | /** Close the Blob. It **must** be called to prevent leaks. */ 72 | close(): void { 73 | unwrap(sqlite3_blob_close(this.#handle)); 74 | } 75 | 76 | /** Obtains Web Stream for reading the Blob */ 77 | get readable(): ReadableStream { 78 | const length = this.byteLength; 79 | let offset = 0; 80 | return new ReadableStream({ 81 | type: "bytes", 82 | pull: (ctx) => { 83 | try { 84 | const byob = ctx.byobRequest; 85 | if (byob) { 86 | const toRead = Math.min( 87 | length - offset, 88 | byob.view!.byteLength, 89 | ); 90 | this.readSync( 91 | offset, 92 | (byob.view as Uint8Array).subarray(0, toRead), 93 | ); 94 | offset += toRead; 95 | byob.respond(toRead); 96 | } else { 97 | const toRead = Math.min( 98 | length - offset, 99 | ctx.desiredSize || 1024 * 16, 100 | ); 101 | if (toRead === 0) { 102 | ctx.close(); 103 | return; 104 | } 105 | const buffer = new Uint8Array(toRead); 106 | this.readSync(offset, buffer); 107 | offset += toRead; 108 | ctx.enqueue(buffer); 109 | } 110 | } catch (e) { 111 | ctx.error(e); 112 | ctx.byobRequest?.respond(0); 113 | } 114 | }, 115 | }); 116 | } 117 | 118 | /** Obtains Web Stream for writing to the Blob */ 119 | get writable(): WritableStream { 120 | const length = this.byteLength; 121 | let offset = 0; 122 | return new WritableStream({ 123 | write: (chunk, ctx) => { 124 | if (offset + chunk.byteLength > length) { 125 | ctx.error(new Error("Write exceeds blob length")); 126 | return; 127 | } 128 | this.writeSync(offset, chunk); 129 | offset += chunk.byteLength; 130 | }, 131 | }); 132 | } 133 | 134 | *[Symbol.iterator](): IterableIterator { 135 | const length = this.byteLength; 136 | let offset = 0; 137 | while (offset < length) { 138 | const toRead = Math.min(length - offset, 1024 * 16); 139 | const buffer = new Uint8Array(toRead); 140 | this.readSync(offset, buffer); 141 | offset += toRead; 142 | yield buffer; 143 | } 144 | } 145 | 146 | [Symbol.for("Deno.customInspect")](): string { 147 | return `SQLite3.Blob(0x${this.byteLength.toString(16)})`; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | // Result Codes 2 | export const SQLITE3_OK = 0; 3 | export const SQLITE3_ERROR = 1; 4 | export const SQLITE3_INTERNAL = 2; 5 | export const SQLITE3_PERM = 3; 6 | export const SQLITE3_ABORT = 4; 7 | export const SQLITE3_BUSY = 5; 8 | export const SQLITE3_LOCKED = 6; 9 | export const SQLITE3_NOMEM = 7; 10 | export const SQLITE3_READONLY = 8; 11 | export const SQLITE3_INTERRUPT = 9; 12 | export const SQLITE3_IOERR = 10; 13 | export const SQLITE3_CORRUPT = 11; 14 | export const SQLITE3_NOTFOUND = 12; 15 | export const SQLITE3_FULL = 13; 16 | export const SQLITE3_CANTOPEN = 14; 17 | export const SQLITE3_PROTOCOL = 15; 18 | export const SQLITE3_EMPTY = 16; 19 | export const SQLITE3_SCHEMA = 17; 20 | export const SQLITE3_TOOBIG = 18; 21 | export const SQLITE3_CONSTRAINT = 19; 22 | export const SQLITE3_MISMATCH = 20; 23 | export const SQLITE3_MISUSE = 21; 24 | export const SQLITE3_NOLFS = 22; 25 | export const SQLITE3_AUTH = 23; 26 | export const SQLITE3_FORMAT = 24; 27 | export const SQLITE3_RANGE = 25; 28 | export const SQLITE3_NOTADB = 26; 29 | export const SQLITE3_NOTICE = 27; 30 | export const SQLITE3_WARNING = 28; 31 | export const SQLITE3_ROW = 100; 32 | export const SQLITE3_DONE = 101; 33 | 34 | // Open Flags 35 | export const SQLITE3_OPEN_READONLY = 0x00000001; 36 | export const SQLITE3_OPEN_READWRITE = 0x00000002; 37 | export const SQLITE3_OPEN_CREATE = 0x00000004; 38 | export const SQLITE3_OPEN_DELETEONCLOSE = 0x00000008; 39 | export const SQLITE3_OPEN_EXCLUSIVE = 0x00000010; 40 | export const SQLITE3_OPEN_AUTOPROXY = 0x00000020; 41 | export const SQLITE3_OPEN_URI = 0x00000040; 42 | export const SQLITE3_OPEN_MEMORY = 0x00000080; 43 | export const SQLITE3_OPEN_MAIN_DB = 0x00000100; 44 | export const SQLITE3_OPEN_TEMP_DB = 0x00000200; 45 | export const SQLITE3_OPEN_TRANSIENT_DB = 0x00000400; 46 | export const SQLITE3_OPEN_MAIN_JOURNAL = 0x00000800; 47 | export const SQLITE3_OPEN_TEMP_JOURNAL = 0x00001000; 48 | export const SQLITE3_OPEN_SUBJOURNAL = 0x00002000; 49 | export const SQLITE3_OPEN_SUPER_JOURNAL = 0x00004000; 50 | export const SQLITE3_OPEN_NONMUTEX = 0x00008000; 51 | export const SQLITE3_OPEN_FULLMUTEX = 0x00010000; 52 | export const SQLITE3_OPEN_SHAREDCACHE = 0x00020000; 53 | export const SQLITE3_OPEN_PRIVATECACHE = 0x00040000; 54 | export const SQLITE3_OPEN_WAL = 0x00080000; 55 | export const SQLITE3_OPEN_NOFOLLOW = 0x01000000; 56 | 57 | // Prepare Flags 58 | export const SQLITE3_PREPARE_PERSISTENT = 0x00000001; 59 | export const SQLITE3_PREPARE_NORMALIZE = 0x00000002; 60 | export const SQLITE3_PREPARE_NO_VTAB = 0x00000004; 61 | 62 | // Fundamental Datatypes 63 | export const SQLITE_INTEGER = 1; 64 | export const SQLITE_FLOAT = 2; 65 | export const SQLITE_TEXT = 3; 66 | export const SQLITE_BLOB = 4; 67 | export const SQLITE_NULL = 5; 68 | -------------------------------------------------------------------------------- /src/database.ts: -------------------------------------------------------------------------------- 1 | import ffi from "./ffi.ts"; 2 | import { fromFileUrl } from "../deps.ts"; 3 | import { 4 | SQLITE3_OPEN_CREATE, 5 | SQLITE3_OPEN_MEMORY, 6 | SQLITE3_OPEN_READONLY, 7 | SQLITE3_OPEN_READWRITE, 8 | SQLITE_BLOB, 9 | SQLITE_FLOAT, 10 | SQLITE_INTEGER, 11 | SQLITE_NULL, 12 | SQLITE_TEXT, 13 | } from "./constants.ts"; 14 | import { readCstr, toCString, unwrap } from "./util.ts"; 15 | import { 16 | type RestBindParameters, 17 | Statement, 18 | STATEMENTS_TO_DB, 19 | } from "./statement.ts"; 20 | import { type BlobOpenOptions, SQLBlob } from "./blob.ts"; 21 | 22 | /** Various options that can be configured when opening Database connection. */ 23 | export interface DatabaseOpenOptions { 24 | /** Whether to open database only in read-only mode. By default, this is false. */ 25 | readonly?: boolean; 26 | /** Whether to create a new database file at specified path if one does not exist already. By default this is true. */ 27 | create?: boolean; 28 | /** Raw SQLite C API flags. Specifying this ignores all other options. */ 29 | flags?: number; 30 | /** Opens an in-memory database. */ 31 | memory?: boolean; 32 | /** Whether to support BigInt columns. False by default, integers larger than 32 bit will be inaccurate. */ 33 | int64?: boolean; 34 | /** Apply agressive optimizations that are not possible with concurrent clients. */ 35 | unsafeConcurrency?: boolean; 36 | /** Enable or disable extension loading */ 37 | enableLoadExtension?: boolean; 38 | /** Whether to parse JSON columns as JS objects. True by default. */ 39 | parseJson?: boolean; 40 | } 41 | 42 | /** Transaction function created using `Database#transaction`. */ 43 | export type Transaction void> = 44 | & ((...args: Parameters) => ReturnType) 45 | & { 46 | /** BEGIN */ 47 | default: Transaction; 48 | /** BEGIN DEFERRED */ 49 | deferred: Transaction; 50 | /** BEGIN IMMEDIATE */ 51 | immediate: Transaction; 52 | /** BEGIN EXCLUSIVE */ 53 | exclusive: Transaction; 54 | database: Database; 55 | }; 56 | 57 | /** 58 | * Options for user-defined functions. 59 | * 60 | * @link https://www.sqlite.org/c3ref/c_deterministic.html 61 | */ 62 | export interface FunctionOptions { 63 | varargs?: boolean; 64 | deterministic?: boolean; 65 | directOnly?: boolean; 66 | innocuous?: boolean; 67 | subtype?: boolean; 68 | } 69 | 70 | /** 71 | * Options for user-defined aggregate functions. 72 | */ 73 | export interface AggregateFunctionOptions extends FunctionOptions { 74 | start: any | (() => any); 75 | step: (aggregate: any, ...args: any[]) => void; 76 | final?: (aggregate: any) => any; 77 | } 78 | 79 | const { 80 | sqlite3_open_v2, 81 | sqlite3_close_v2, 82 | sqlite3_changes, 83 | sqlite3_total_changes, 84 | sqlite3_last_insert_rowid, 85 | sqlite3_get_autocommit, 86 | sqlite3_exec, 87 | sqlite3_free, 88 | sqlite3_libversion, 89 | sqlite3_sourceid, 90 | sqlite3_complete, 91 | sqlite3_finalize, 92 | sqlite3_result_blob, 93 | sqlite3_result_double, 94 | sqlite3_result_error, 95 | sqlite3_result_int64, 96 | sqlite3_result_null, 97 | sqlite3_result_text, 98 | sqlite3_value_blob, 99 | sqlite3_value_bytes, 100 | sqlite3_value_double, 101 | sqlite3_value_int64, 102 | sqlite3_value_text, 103 | sqlite3_value_type, 104 | sqlite3_create_function, 105 | sqlite3_result_int, 106 | sqlite3_aggregate_context, 107 | sqlite3_enable_load_extension, 108 | sqlite3_load_extension, 109 | sqlite3_backup_init, 110 | sqlite3_backup_step, 111 | sqlite3_backup_finish, 112 | sqlite3_errcode, 113 | } = ffi; 114 | 115 | /** SQLite version string */ 116 | export const SQLITE_VERSION: string = readCstr(sqlite3_libversion()!); 117 | /** SQLite source ID string */ 118 | export const SQLITE_SOURCEID: string = readCstr(sqlite3_sourceid()!); 119 | 120 | /** 121 | * Whether the given SQL statement is complete. 122 | * 123 | * @param statement SQL statement string 124 | */ 125 | export function isComplete(statement: string): boolean { 126 | return Boolean(sqlite3_complete(toCString(statement))); 127 | } 128 | 129 | const BIG_MAX = BigInt(Number.MAX_SAFE_INTEGER); 130 | 131 | /** 132 | * Represents a SQLite3 database connection. 133 | * 134 | * Example: 135 | * ```ts 136 | * // Open a database from file, creates if doesn't exist. 137 | * const db = new Database("myfile.db"); 138 | * 139 | * // Open an in-memory database. 140 | * const db = new Database(":memory:"); 141 | * 142 | * // Open a read-only database. 143 | * const db = new Database("myfile.db", { readonly: true }); 144 | * 145 | * // Or open using File URL 146 | * const db = new Database(new URL("./myfile.db", import.meta.url)); 147 | * ``` 148 | */ 149 | export class Database { 150 | #path: string; 151 | #handle: Deno.PointerValue; 152 | #open = true; 153 | #enableLoadExtension = false; 154 | 155 | /** Whether to support BigInt columns. False by default, integers larger than 32 bit will be inaccurate. */ 156 | int64: boolean; 157 | 158 | /** Whether to parse JSON columns as JS objects. True by default. */ 159 | parseJson: boolean; 160 | 161 | unsafeConcurrency: boolean; 162 | 163 | /** Whether DB connection is open */ 164 | get open(): boolean { 165 | return this.#open; 166 | } 167 | 168 | /** Unsafe Raw (pointer) to the sqlite object */ 169 | get unsafeHandle(): Deno.PointerValue { 170 | return this.#handle; 171 | } 172 | 173 | /** Path of the database file. */ 174 | get path(): string { 175 | return this.#path; 176 | } 177 | 178 | /** Number of rows changed by the last executed statement. */ 179 | get changes(): number { 180 | return sqlite3_changes(this.#handle); 181 | } 182 | 183 | /** Number of rows changed since the database connection was opened. */ 184 | get totalChanges(): number { 185 | return sqlite3_total_changes(this.#handle); 186 | } 187 | 188 | /** Gets last inserted Row ID */ 189 | get lastInsertRowId(): number { 190 | return Number(sqlite3_last_insert_rowid(this.#handle)); 191 | } 192 | 193 | /** Whether autocommit is enabled. Enabled by default, can be disabled using BEGIN statement. */ 194 | get autocommit(): boolean { 195 | return sqlite3_get_autocommit(this.#handle) === 1; 196 | } 197 | 198 | /** Whether DB is in mid of a transaction */ 199 | get inTransaction(): boolean { 200 | return this.#open && !this.autocommit; 201 | } 202 | 203 | get enableLoadExtension(): boolean { 204 | return this.#enableLoadExtension; 205 | } 206 | 207 | set enableLoadExtension(enabled: boolean) { 208 | if (sqlite3_enable_load_extension === null) { 209 | throw new Error( 210 | "Extension loading is not supported by the shared library that was used.", 211 | ); 212 | } 213 | const result = sqlite3_enable_load_extension(this.#handle, Number(enabled)); 214 | unwrap(result, this.#handle); 215 | this.#enableLoadExtension = enabled; 216 | } 217 | 218 | constructor(path: string | URL, options: DatabaseOpenOptions = {}) { 219 | this.#path = path instanceof URL ? fromFileUrl(path) : path; 220 | let flags = 0; 221 | this.int64 = options.int64 ?? false; 222 | this.parseJson = options.parseJson ?? true; 223 | this.unsafeConcurrency = options.unsafeConcurrency ?? false; 224 | if (options.flags !== undefined) { 225 | flags = options.flags; 226 | } else { 227 | if (options.memory) { 228 | flags |= SQLITE3_OPEN_MEMORY; 229 | } 230 | 231 | if (options.readonly ?? false) { 232 | flags |= SQLITE3_OPEN_READONLY; 233 | } else { 234 | flags |= SQLITE3_OPEN_READWRITE; 235 | } 236 | 237 | if ((options.create ?? true) && !options.readonly) { 238 | flags |= SQLITE3_OPEN_CREATE; 239 | } 240 | } 241 | 242 | const pHandle = new BigUint64Array(1); 243 | const result = sqlite3_open_v2(toCString(this.#path), pHandle, flags, null); 244 | this.#handle = Deno.UnsafePointer.create(pHandle[0]); 245 | if (result !== 0) sqlite3_close_v2(this.#handle); 246 | unwrap(result); 247 | 248 | if (options.enableLoadExtension) { 249 | this.enableLoadExtension = options.enableLoadExtension; 250 | } 251 | } 252 | 253 | /** 254 | * Creates a new Prepared Statement from the given SQL statement. 255 | * 256 | * Example: 257 | * ```ts 258 | * const stmt = db.prepare("SELECT * FROM mytable WHERE id = ?"); 259 | * 260 | * for (const row of stmt.all(1)) { 261 | * console.log(row); 262 | * } 263 | * ``` 264 | * 265 | * Bind parameters can be either provided as an array of values, or as an object 266 | * mapping the parameter name to the value. 267 | * 268 | * Example: 269 | * ```ts 270 | * const stmt = db.prepare("SELECT * FROM mytable WHERE id = ?"); 271 | * const row = stmt.get(1); 272 | * 273 | * // or 274 | * 275 | * const stmt = db.prepare("SELECT * FROM mytable WHERE id = :id"); 276 | * const row = stmt.get({ id: 1 }); 277 | * ``` 278 | * 279 | * Statements are automatically freed once GC catches them, however 280 | * you can also manually free using `finalize` method. 281 | * 282 | * @param sql SQL statement string 283 | * @returns Statement object 284 | */ 285 | prepare>(sql: string): Statement { 286 | return new Statement(this, sql); 287 | } 288 | 289 | /** 290 | * Open a Blob for incremental I/O. 291 | * 292 | * Make sure to close the blob after you are done with it, 293 | * otherwise you will have memory leaks. 294 | */ 295 | openBlob(options: BlobOpenOptions): SQLBlob { 296 | return new SQLBlob(this, options); 297 | } 298 | 299 | /** 300 | * Simply executes the SQL statement (supports multiple statements separated by semicolon). 301 | * Returns the number of changes made by last statement. 302 | * 303 | * Example: 304 | * ```ts 305 | * // Create table 306 | * db.exec("create table users (id integer not null, username varchar(20) not null)"); 307 | * 308 | * // Inserts 309 | * db.exec("insert into users (id, username) values(?, ?)", id, username); 310 | * 311 | * // Insert with named parameters 312 | * db.exec("insert into users (id, username) values(:id, :username)", { id, username }); 313 | * 314 | * // Pragma statements 315 | * db.exec("pragma journal_mode = WAL"); 316 | * db.exec("pragma synchronous = normal"); 317 | * db.exec("pragma temp_store = memory"); 318 | * ``` 319 | * 320 | * Under the hood, it uses `sqlite3_exec` if no parameters are given to bind 321 | * with the SQL statement, a prepared statement otherwise. 322 | */ 323 | exec(sql: string, ...params: RestBindParameters): number { 324 | if (params.length === 0) { 325 | const pErr = new BigUint64Array(1); 326 | sqlite3_exec( 327 | this.#handle, 328 | toCString(sql), 329 | null, 330 | null, 331 | new Uint8Array(pErr.buffer), 332 | ); 333 | const errPtr = Deno.UnsafePointer.create(pErr[0]); 334 | if (errPtr !== null) { 335 | const err = readCstr(errPtr); 336 | sqlite3_free(errPtr); 337 | throw new Error(err); 338 | } 339 | return sqlite3_changes(this.#handle); 340 | } 341 | 342 | const stmt = this.prepare(sql); 343 | stmt.run(...params); 344 | return sqlite3_changes(this.#handle); 345 | } 346 | 347 | /** Alias for `exec`. */ 348 | run(sql: string, ...params: RestBindParameters): number { 349 | return this.exec(sql, ...params); 350 | } 351 | 352 | /** Safely execute SQL with parameters using a tagged template */ 353 | sql = Record>( 354 | strings: TemplateStringsArray, 355 | ...parameters: RestBindParameters 356 | ): T[] { 357 | const sql = strings.join("?"); 358 | const stmt = this.prepare(sql); 359 | return stmt.all(...parameters); 360 | } 361 | 362 | /** 363 | * Wraps a callback function in a transaction. 364 | * 365 | * - When function is called, the transaction is started. 366 | * - When function returns, the transaction is committed. 367 | * - When function throws an error, the transaction is rolled back. 368 | * 369 | * Example: 370 | * ```ts 371 | * const stmt = db.prepare("insert into users (id, username) values(?, ?)"); 372 | * 373 | * interface User { 374 | * id: number; 375 | * username: string; 376 | * } 377 | * 378 | * const insertUsers = db.transaction((data: User[]) => { 379 | * for (const user of data) { 380 | * stmt.run(user); 381 | * } 382 | * }); 383 | * 384 | * insertUsers([ 385 | * { id: 1, username: "alice" }, 386 | * { id: 2, username: "bob" }, 387 | * ]); 388 | * 389 | * // May also use `insertUsers.deferred`, `immediate`, or `exclusive`. 390 | * // They corresspond to using `BEGIN DEFERRED`, `BEGIN IMMEDIATE`, and `BEGIN EXCLUSIVE`. 391 | * // For eg. 392 | * 393 | * insertUsers.deferred([ 394 | * { id: 1, username: "alice" }, 395 | * { id: 2, username: "bob" }, 396 | * ]); 397 | * ``` 398 | */ 399 | transaction, ...args: any[]) => void>( 400 | fn: T, 401 | ): Transaction { 402 | // Based on https://github.com/WiseLibs/better-sqlite3/blob/master/lib/methods/transaction.js 403 | const controller = getController(this); 404 | 405 | // Each version of the transaction function has these same properties 406 | const properties = { 407 | default: { value: wrapTransaction(fn, this, controller.default) }, 408 | deferred: { value: wrapTransaction(fn, this, controller.deferred) }, 409 | immediate: { value: wrapTransaction(fn, this, controller.immediate) }, 410 | exclusive: { value: wrapTransaction(fn, this, controller.exclusive) }, 411 | database: { value: this, enumerable: true }, 412 | }; 413 | 414 | Object.defineProperties(properties.default.value, properties); 415 | Object.defineProperties(properties.deferred.value, properties); 416 | Object.defineProperties(properties.immediate.value, properties); 417 | Object.defineProperties(properties.exclusive.value, properties); 418 | 419 | // Return the default version of the transaction function 420 | return properties.default.value as Transaction; 421 | } 422 | 423 | #callbacks = new Set(); 424 | 425 | /** 426 | * Creates a new user-defined function. 427 | * 428 | * Example: 429 | * ```ts 430 | * db.function("add", (a: number, b: number) => a + b); 431 | * db.prepare("select add(1, 2)").value<[number]>()!; // [3] 432 | * ``` 433 | */ 434 | function( 435 | name: string, 436 | fn: CallableFunction, 437 | options?: FunctionOptions, 438 | ): void { 439 | if (sqlite3_create_function === null) { 440 | throw new Error( 441 | "User-defined functions are not supported by the shared library that was used.", 442 | ); 443 | } 444 | 445 | const cb = new Deno.UnsafeCallback( 446 | { 447 | parameters: ["pointer", "i32", "pointer"], 448 | result: "void", 449 | } as const, 450 | (ctx, nArgs, pArgs) => { 451 | const argptr = new Deno.UnsafePointerView(pArgs!); 452 | const args: any[] = []; 453 | for (let i = 0; i < nArgs; i++) { 454 | const arg = Deno.UnsafePointer.create( 455 | argptr.getBigUint64(i * 8), 456 | ); 457 | const type = sqlite3_value_type(arg); 458 | switch (type) { 459 | case SQLITE_INTEGER: { 460 | const value = sqlite3_value_int64(arg); 461 | if (value < -BIG_MAX || value > BIG_MAX) { 462 | args.push(value); 463 | } else { 464 | args.push(Number(value)); 465 | } 466 | break; 467 | } 468 | case SQLITE_FLOAT: 469 | args.push(sqlite3_value_double(arg)); 470 | break; 471 | case SQLITE_TEXT: 472 | args.push( 473 | new TextDecoder().decode( 474 | new Uint8Array( 475 | Deno.UnsafePointerView.getArrayBuffer( 476 | sqlite3_value_text(arg)!, 477 | sqlite3_value_bytes(arg), 478 | ), 479 | ), 480 | ), 481 | ); 482 | break; 483 | case SQLITE_BLOB: 484 | args.push( 485 | new Uint8Array( 486 | Deno.UnsafePointerView.getArrayBuffer( 487 | sqlite3_value_blob(arg)!, 488 | sqlite3_value_bytes(arg), 489 | ), 490 | ), 491 | ); 492 | break; 493 | case SQLITE_NULL: 494 | args.push(null); 495 | break; 496 | default: 497 | throw new Error(`Unknown type: ${type}`); 498 | } 499 | } 500 | 501 | let result: any; 502 | try { 503 | result = fn(...args); 504 | } catch (err) { 505 | const buf = new TextEncoder().encode( 506 | err instanceof Error ? err.message : String(err), 507 | ); 508 | sqlite3_result_error(ctx, buf, buf.byteLength); 509 | return; 510 | } 511 | 512 | if (result === undefined || result === null) { 513 | sqlite3_result_null(ctx); 514 | } else if (typeof result === "boolean") { 515 | sqlite3_result_int(ctx, result ? 1 : 0); 516 | } else if (typeof result === "number") { 517 | if (Number.isSafeInteger(result)) { 518 | sqlite3_result_int64(ctx, BigInt(result)); 519 | } else sqlite3_result_double(ctx, result); 520 | } else if (typeof result === "bigint") { 521 | sqlite3_result_int64(ctx, result); 522 | } else if (typeof result === "string") { 523 | const buffer = new TextEncoder().encode(result); 524 | sqlite3_result_text(ctx, buffer, buffer.byteLength, 0n); 525 | } else if (result instanceof Uint8Array) { 526 | sqlite3_result_blob(ctx, result, result.length, -1n); 527 | } else { 528 | const buffer = new TextEncoder().encode( 529 | `Invalid return value: ${Deno.inspect(result)}`, 530 | ); 531 | sqlite3_result_error(ctx, buffer, buffer.byteLength); 532 | } 533 | }, 534 | ); 535 | 536 | let flags = 1; 537 | 538 | if (options?.deterministic) { 539 | flags |= 0x000000800; 540 | } 541 | 542 | if (options?.directOnly) { 543 | flags |= 0x000080000; 544 | } 545 | 546 | if (options?.subtype) { 547 | flags |= 0x000100000; 548 | } 549 | 550 | if (options?.directOnly) { 551 | flags |= 0x000200000; 552 | } 553 | 554 | const err = sqlite3_create_function( 555 | this.#handle, 556 | toCString(name), 557 | options?.varargs ? -1 : fn.length, 558 | flags, 559 | null, 560 | cb.pointer, 561 | null, 562 | null, 563 | ); 564 | 565 | unwrap(err, this.#handle); 566 | 567 | this.#callbacks.add(cb as Deno.UnsafeCallback); 568 | } 569 | 570 | /** 571 | * Creates a new user-defined aggregate function. 572 | */ 573 | aggregate(name: string, options: AggregateFunctionOptions): void { 574 | if ( 575 | sqlite3_aggregate_context === null || sqlite3_create_function === null 576 | ) { 577 | throw new Error( 578 | "User-defined functions are not supported by the shared library that was used.", 579 | ); 580 | } 581 | 582 | const contexts = new Map(); 583 | 584 | const cb = new Deno.UnsafeCallback( 585 | { 586 | parameters: ["pointer", "i32", "pointer"], 587 | result: "void", 588 | } as const, 589 | (ctx, nArgs, pArgs) => { 590 | const aggrCtx = sqlite3_aggregate_context(ctx, 8); 591 | const aggrPtr = Deno.UnsafePointer.value(aggrCtx); 592 | let aggregate; 593 | if (contexts.has(aggrPtr)) { 594 | aggregate = contexts.get(aggrPtr); 595 | } else { 596 | aggregate = typeof options.start === "function" 597 | ? options.start() 598 | : options.start; 599 | contexts.set(aggrPtr, aggregate); 600 | } 601 | const argptr = new Deno.UnsafePointerView(pArgs!); 602 | const args: any[] = []; 603 | for (let i = 0; i < nArgs; i++) { 604 | const arg = Deno.UnsafePointer.create( 605 | argptr.getBigUint64(i * 8), 606 | ); 607 | const type = sqlite3_value_type(arg); 608 | switch (type) { 609 | case SQLITE_INTEGER: { 610 | const value = sqlite3_value_int64(arg); 611 | if (value < -BIG_MAX || value > BIG_MAX) { 612 | args.push(value); 613 | } else { 614 | args.push(Number(value)); 615 | } 616 | break; 617 | } 618 | case SQLITE_FLOAT: 619 | args.push(sqlite3_value_double(arg)); 620 | break; 621 | case SQLITE_TEXT: 622 | args.push( 623 | new TextDecoder().decode( 624 | new Uint8Array( 625 | Deno.UnsafePointerView.getArrayBuffer( 626 | sqlite3_value_text(arg)!, 627 | sqlite3_value_bytes(arg), 628 | ), 629 | ), 630 | ), 631 | ); 632 | break; 633 | case SQLITE_BLOB: 634 | args.push( 635 | new Uint8Array( 636 | Deno.UnsafePointerView.getArrayBuffer( 637 | sqlite3_value_blob(arg)!, 638 | sqlite3_value_bytes(arg), 639 | ), 640 | ), 641 | ); 642 | break; 643 | case SQLITE_NULL: 644 | args.push(null); 645 | break; 646 | default: 647 | throw new Error(`Unknown type: ${type}`); 648 | } 649 | } 650 | 651 | let result: any; 652 | try { 653 | result = options.step(aggregate, ...args); 654 | } catch (err) { 655 | const buf = new TextEncoder().encode( 656 | err instanceof Error ? err.message : String(err), 657 | ); 658 | sqlite3_result_error(ctx, buf, buf.byteLength); 659 | return; 660 | } 661 | 662 | contexts.set(aggrPtr, result); 663 | }, 664 | ); 665 | 666 | const cbFinal = new Deno.UnsafeCallback( 667 | { 668 | parameters: ["pointer"], 669 | result: "void", 670 | } as const, 671 | (ctx) => { 672 | const aggrCtx = sqlite3_aggregate_context(ctx, 0); 673 | const aggrPtr = Deno.UnsafePointer.value(aggrCtx); 674 | const aggregate = contexts.get(aggrPtr); 675 | contexts.delete(aggrPtr); 676 | let result: any; 677 | try { 678 | result = options.final ? options.final(aggregate) : aggregate; 679 | } catch (err) { 680 | const buf = new TextEncoder().encode( 681 | err instanceof Error ? err.message : String(err), 682 | ); 683 | sqlite3_result_error(ctx, buf, buf.byteLength); 684 | return; 685 | } 686 | 687 | if (result === undefined || result === null) { 688 | sqlite3_result_null(ctx); 689 | } else if (typeof result === "boolean") { 690 | sqlite3_result_int(ctx, result ? 1 : 0); 691 | } else if (typeof result === "number") { 692 | if (Number.isSafeInteger(result)) { 693 | sqlite3_result_int64(ctx, BigInt(result)); 694 | } else sqlite3_result_double(ctx, result); 695 | } else if (typeof result === "bigint") { 696 | sqlite3_result_int64(ctx, result); 697 | } else if (typeof result === "string") { 698 | const buffer = new TextEncoder().encode(result); 699 | sqlite3_result_text(ctx, buffer, buffer.byteLength, 0n); 700 | } else if (result instanceof Uint8Array) { 701 | sqlite3_result_blob(ctx, result, result.length, -1n); 702 | } else { 703 | const buffer = new TextEncoder().encode( 704 | `Invalid return value: ${Deno.inspect(result)}`, 705 | ); 706 | sqlite3_result_error(ctx, buffer, buffer.byteLength); 707 | } 708 | }, 709 | ); 710 | 711 | let flags = 1; 712 | 713 | if (options?.deterministic) { 714 | flags |= 0x000000800; 715 | } 716 | 717 | if (options?.directOnly) { 718 | flags |= 0x000080000; 719 | } 720 | 721 | if (options?.subtype) { 722 | flags |= 0x000100000; 723 | } 724 | 725 | if (options?.directOnly) { 726 | flags |= 0x000200000; 727 | } 728 | 729 | const err = sqlite3_create_function( 730 | this.#handle, 731 | toCString(name), 732 | options?.varargs ? -1 : options.step.length - 1, 733 | flags, 734 | null, 735 | null, 736 | cb.pointer, 737 | cbFinal.pointer, 738 | ); 739 | 740 | unwrap(err, this.#handle); 741 | 742 | this.#callbacks.add(cb as Deno.UnsafeCallback); 743 | this.#callbacks.add(cbFinal as Deno.UnsafeCallback); 744 | } 745 | 746 | /** 747 | * Loads an SQLite extension library from the named file. 748 | */ 749 | loadExtension(file: string, entryPoint?: string): void { 750 | if (sqlite3_load_extension === null) { 751 | throw new Error( 752 | "Extension loading is not supported by the shared library that was used.", 753 | ); 754 | } 755 | 756 | if (!this.enableLoadExtension) { 757 | throw new Error("Extension loading is not enabled"); 758 | } 759 | 760 | const pzErrMsg = new BigUint64Array(1); 761 | 762 | const result = sqlite3_load_extension( 763 | this.#handle, 764 | toCString(file), 765 | entryPoint ? toCString(entryPoint) : null, 766 | pzErrMsg, 767 | ); 768 | 769 | const pzErrPtr = Deno.UnsafePointer.create( 770 | pzErrMsg[0], 771 | ); 772 | if (pzErrPtr !== null) { 773 | const pzErr = readCstr(pzErrPtr); 774 | sqlite3_free(pzErrPtr); 775 | throw new Error(pzErr); 776 | } 777 | 778 | unwrap(result, this.#handle); 779 | } 780 | 781 | /** 782 | * Closes the database connection. 783 | * 784 | * Calling this method more than once is no-op. 785 | */ 786 | close(): void { 787 | if (!this.#open) return; 788 | for (const [stmt, db] of STATEMENTS_TO_DB) { 789 | if (db === this.#handle) { 790 | sqlite3_finalize(stmt); 791 | STATEMENTS_TO_DB.delete(stmt); 792 | } 793 | } 794 | for (const cb of this.#callbacks) { 795 | cb.close(); 796 | } 797 | unwrap(sqlite3_close_v2(this.#handle)); 798 | this.#open = false; 799 | } 800 | 801 | /** 802 | * @param dest The destination database connection. 803 | * @param name Destination database name. "main" for main database, "temp" for temporary database, or the name specified after the AS keyword in an ATTACH statement for an attached database. 804 | * @param pages The number of pages to copy. If it is negative, all remaining pages are copied (default). 805 | */ 806 | backup(dest: Database, name = "main", pages = -1): void { 807 | const backup = sqlite3_backup_init( 808 | dest.#handle, 809 | toCString(name), 810 | this.#handle, 811 | toCString("main"), 812 | ); 813 | if (backup) { 814 | unwrap(sqlite3_backup_step(backup, pages)); 815 | unwrap(sqlite3_backup_finish(backup)); 816 | } else { 817 | unwrap(sqlite3_errcode(dest.#handle), dest.#handle); 818 | } 819 | } 820 | 821 | [Symbol.for("Deno.customInspect")](): string { 822 | return `SQLite3.Database { path: ${this.path} }`; 823 | } 824 | } 825 | 826 | const controllers = new WeakMap(); 827 | 828 | // Return the database's cached transaction controller, or create a new one 829 | const getController = (db: Database) => { 830 | let controller = controllers.get(db); 831 | if (!controller) { 832 | const shared = { 833 | commit: db.prepare("COMMIT"), 834 | rollback: db.prepare("ROLLBACK"), 835 | savepoint: db.prepare("SAVEPOINT `\t_bs3.\t`"), 836 | release: db.prepare("RELEASE `\t_bs3.\t`"), 837 | rollbackTo: db.prepare("ROLLBACK TO `\t_bs3.\t`"), 838 | }; 839 | 840 | controllers.set( 841 | db, 842 | controller = { 843 | default: Object.assign( 844 | { begin: db.prepare("BEGIN") }, 845 | shared, 846 | ), 847 | deferred: Object.assign( 848 | { begin: db.prepare("BEGIN DEFERRED") }, 849 | shared, 850 | ), 851 | immediate: Object.assign( 852 | { begin: db.prepare("BEGIN IMMEDIATE") }, 853 | shared, 854 | ), 855 | exclusive: Object.assign( 856 | { begin: db.prepare("BEGIN EXCLUSIVE") }, 857 | shared, 858 | ), 859 | }, 860 | ); 861 | } 862 | return controller; 863 | }; 864 | 865 | // Return a new transaction function by wrapping the given function 866 | const wrapTransaction = void>( 867 | fn: T, 868 | db: Database, 869 | { begin, commit, rollback, savepoint, release, rollbackTo }: any, 870 | ) => 871 | function sqliteTransaction(...args: Parameters): ReturnType { 872 | const { apply } = Function.prototype; 873 | let before, after, undo; 874 | if (db.inTransaction) { 875 | before = savepoint; 876 | after = release; 877 | undo = rollbackTo; 878 | } else { 879 | before = begin; 880 | after = commit; 881 | undo = rollback; 882 | } 883 | before.run(); 884 | try { 885 | // @ts-ignore An outer value of 'this' is shadowed by this container. 886 | const result = apply.call(fn, this, args); 887 | after.run(); 888 | return result; 889 | } catch (ex) { 890 | if (!db.autocommit) { 891 | undo.run(); 892 | if (undo !== rollback) after.run(); 893 | } 894 | throw ex; 895 | } 896 | }; 897 | -------------------------------------------------------------------------------- /src/ffi.ts: -------------------------------------------------------------------------------- 1 | import meta from "../deno.json" with { type: "json" }; 2 | import { dlopen } from "../deps.ts"; 3 | 4 | const symbols = { 5 | sqlite3_open_v2: { 6 | parameters: [ 7 | "buffer", // const char *filename 8 | "buffer", // sqlite3 **ppDb 9 | "i32", // int flags 10 | "pointer", // const char *zVfs 11 | ], 12 | result: "i32", 13 | }, 14 | 15 | sqlite3_close_v2: { 16 | parameters: [ 17 | "pointer", // sqlite3 *db 18 | ], 19 | result: "i32", 20 | }, 21 | 22 | sqlite3_changes: { 23 | parameters: [ 24 | "pointer", // sqlite3 *db 25 | ], 26 | result: "i32", 27 | }, 28 | 29 | sqlite3_total_changes: { 30 | parameters: [ 31 | "pointer", // sqlite3 *db 32 | ], 33 | result: "i32", 34 | }, 35 | 36 | sqlite3_last_insert_rowid: { 37 | parameters: [ 38 | "pointer", // sqlite3 *db 39 | ], 40 | result: "i32", 41 | }, 42 | 43 | sqlite3_get_autocommit: { 44 | parameters: [ 45 | "pointer", // sqlite3 *db 46 | ], 47 | result: "i32", 48 | }, 49 | 50 | sqlite3_prepare_v2: { 51 | parameters: [ 52 | "pointer", // sqlite3 *db 53 | "buffer", // const char *zSql 54 | "i32", // int nByte 55 | "buffer", // sqlite3_stmt **ppStmt 56 | "pointer", // const char **pzTail 57 | ], 58 | result: "i32", 59 | }, 60 | 61 | sqlite3_reset: { 62 | parameters: [ 63 | "pointer", // sqlite3_stmt *pStmt 64 | ], 65 | result: "i32", 66 | }, 67 | 68 | sqlite3_clear_bindings: { 69 | parameters: [ 70 | "pointer", // sqlite3_stmt *pStmt 71 | ], 72 | result: "i32", 73 | }, 74 | 75 | sqlite3_step: { 76 | parameters: [ 77 | "pointer", // sqlite3_stmt *pStmt 78 | ], 79 | result: "i32", 80 | }, 81 | 82 | sqlite3_column_count: { 83 | parameters: [ 84 | "pointer", // sqlite3_stmt *pStmt 85 | ], 86 | result: "i32", 87 | }, 88 | 89 | sqlite3_column_type: { 90 | parameters: [ 91 | "pointer", // sqlite3_stmt *pStmt 92 | "i32", // int iCol 93 | ], 94 | result: "i32", 95 | }, 96 | 97 | sqlite3_column_text: { 98 | parameters: [ 99 | "pointer", // sqlite3_stmt *pStmt 100 | "i32", // int iCol 101 | ], 102 | result: "pointer", 103 | }, 104 | sqlite3_column_value: { 105 | parameters: [ 106 | "pointer", // sqlite3_stmt *pStmt 107 | "i32", // int iCol 108 | ], 109 | result: "pointer", 110 | }, 111 | 112 | sqlite3_finalize: { 113 | parameters: [ 114 | "pointer", // sqlite3_stmt *pStmt 115 | ], 116 | result: "i32", 117 | }, 118 | 119 | sqlite3_exec: { 120 | parameters: [ 121 | "pointer", // sqlite3 *db 122 | "buffer", // const char *sql 123 | "pointer", // sqlite3_callback callback 124 | "pointer", // void *arg 125 | "buffer", // char **errmsg 126 | ], 127 | result: "i32", 128 | }, 129 | 130 | sqlite3_free: { 131 | parameters: [ 132 | "pointer", // void *p 133 | ], 134 | result: "void", 135 | }, 136 | 137 | sqlite3_column_int: { 138 | parameters: [ 139 | "pointer", // sqlite3_stmt *pStmt 140 | "i32", // int iCol 141 | ], 142 | result: "i32", 143 | }, 144 | 145 | sqlite3_column_double: { 146 | parameters: [ 147 | "pointer", // sqlite3_stmt *pStmt 148 | "i32", // int iCol 149 | ], 150 | result: "f64", 151 | }, 152 | 153 | sqlite3_column_blob: { 154 | parameters: [ 155 | "pointer", // sqlite3_stmt *pStmt 156 | "i32", // int iCol 157 | ], 158 | result: "pointer", 159 | }, 160 | 161 | sqlite3_column_bytes: { 162 | parameters: [ 163 | "pointer", // sqlite3_stmt *pStmt 164 | "i32", // int iCol 165 | ], 166 | result: "i32", 167 | }, 168 | 169 | sqlite3_column_name: { 170 | parameters: [ 171 | "pointer", // sqlite3_stmt *pStmt 172 | "i32", // int iCol 173 | ], 174 | result: "pointer", 175 | }, 176 | 177 | sqlite3_column_decltype: { 178 | parameters: [ 179 | "pointer", // sqlite3_stmt *pStmt 180 | "i32", // int iCol 181 | ], 182 | result: "u64", 183 | }, 184 | 185 | sqlite3_bind_parameter_index: { 186 | parameters: [ 187 | "pointer", // sqlite3_stmt *pStmt 188 | "buffer", // const char *zName 189 | ], 190 | result: "i32", 191 | }, 192 | 193 | sqlite3_bind_text: { 194 | parameters: [ 195 | "pointer", // sqlite3_stmt *pStmt 196 | "i32", // int iCol 197 | "buffer", // const char *zData 198 | "i32", // int nData 199 | "pointer", // void (*xDel)(void*) 200 | ], 201 | result: "i32", 202 | }, 203 | 204 | sqlite3_bind_blob: { 205 | parameters: [ 206 | "pointer", // sqlite3_stmt *pStmt 207 | "i32", // int iCol 208 | "buffer", // const void *zData 209 | "i32", // int nData 210 | "pointer", // void (*xDel)(void*) 211 | ], 212 | result: "i32", 213 | }, 214 | 215 | sqlite3_bind_double: { 216 | parameters: [ 217 | "pointer", // sqlite3_stmt *pStmt 218 | "i32", // int iCol 219 | "f64", // double rValue 220 | ], 221 | result: "i32", 222 | }, 223 | 224 | sqlite3_bind_int: { 225 | parameters: [ 226 | "pointer", // sqlite3_stmt *pStmt 227 | "i32", // int iCol 228 | "i32", // int iValue 229 | ], 230 | result: "i32", 231 | }, 232 | 233 | sqlite3_bind_int64: { 234 | parameters: [ 235 | "pointer", // sqlite3_stmt *pStmt 236 | "i32", // int iCol 237 | "i64", // i64 iValue 238 | ], 239 | result: "i32", 240 | }, 241 | 242 | sqlite3_bind_null: { 243 | parameters: [ 244 | "pointer", // sqlite3_stmt *pStmt 245 | "i32", // int iCol 246 | ], 247 | result: "i32", 248 | }, 249 | 250 | sqlite3_expanded_sql: { 251 | parameters: [ 252 | "pointer", // sqlite3_stmt *pStmt 253 | ], 254 | result: "pointer", 255 | }, 256 | 257 | sqlite3_bind_parameter_count: { 258 | parameters: [ 259 | "pointer", // sqlite3_stmt *pStmt 260 | ], 261 | result: "i32", 262 | }, 263 | 264 | sqlite3_complete: { 265 | parameters: [ 266 | "buffer", // const char *sql 267 | ], 268 | result: "i32", 269 | }, 270 | 271 | sqlite3_sourceid: { 272 | parameters: [], 273 | result: "pointer", 274 | }, 275 | 276 | sqlite3_libversion: { 277 | parameters: [], 278 | result: "pointer", 279 | }, 280 | 281 | sqlite3_blob_open: { 282 | parameters: [ 283 | "pointer", /* sqlite3 *db */ 284 | "buffer", /* const char *zDb */ 285 | "buffer", /* const char *zTable */ 286 | "buffer", /* const char *zColumn */ 287 | "i64", /* sqlite3_int64 iRow */ 288 | "i32", /* int flags */ 289 | "buffer", /* sqlite3_blob **ppBlob */ 290 | ], 291 | result: "i32", 292 | }, 293 | 294 | sqlite3_blob_read: { 295 | parameters: [ 296 | "pointer", /* sqlite3_blob *blob */ 297 | "buffer", /* void *Z */ 298 | "i32", /* int N */ 299 | "i32", /* int iOffset */ 300 | ], 301 | result: "i32", 302 | }, 303 | 304 | sqlite3_blob_write: { 305 | parameters: [ 306 | "pointer", /* sqlite3_blob *blob */ 307 | "buffer", /* const void *z */ 308 | "i32", /* int n */ 309 | "i32", /* int iOffset */ 310 | ], 311 | result: "i32", 312 | }, 313 | 314 | sqlite3_blob_bytes: { 315 | parameters: ["pointer" /* sqlite3_blob *blob */], 316 | result: "i32", 317 | }, 318 | 319 | sqlite3_blob_close: { 320 | parameters: ["pointer" /* sqlite3_blob *blob */], 321 | result: "i32", 322 | }, 323 | 324 | sqlite3_sql: { 325 | parameters: [ 326 | "pointer", // sqlite3_stmt *pStmt 327 | ], 328 | result: "pointer", 329 | }, 330 | 331 | sqlite3_stmt_readonly: { 332 | parameters: [ 333 | "pointer", // sqlite3_stmt *pStmt 334 | ], 335 | result: "i32", 336 | }, 337 | 338 | sqlite3_bind_parameter_name: { 339 | parameters: [ 340 | "pointer", // sqlite3_stmt *pStmt 341 | "i32", // int iCol 342 | ], 343 | result: "pointer", 344 | }, 345 | 346 | sqlite3_errcode: { 347 | parameters: [ 348 | "pointer", // sqlite3 *db 349 | ], 350 | result: "i32", 351 | }, 352 | 353 | sqlite3_errmsg: { 354 | parameters: [ 355 | "pointer", // sqlite3 *db 356 | ], 357 | result: "pointer", 358 | }, 359 | 360 | sqlite3_errstr: { 361 | parameters: [ 362 | "i32", // int rc 363 | ], 364 | result: "pointer", 365 | }, 366 | 367 | sqlite3_column_int64: { 368 | parameters: [ 369 | "pointer", // sqlite3_stmt *pStmt 370 | "i32", // int iCol 371 | ], 372 | result: "i64", 373 | }, 374 | 375 | sqlite3_backup_init: { 376 | parameters: [ 377 | "pointer", // sqlite3 *pDest 378 | "buffer", // const char *zDestName 379 | "pointer", // sqlite3 *pSource 380 | "buffer", // const char *zSourceName 381 | ], 382 | result: "pointer", 383 | }, 384 | 385 | sqlite3_backup_step: { 386 | parameters: [ 387 | "pointer", // sqlite3_backup *p 388 | "i32", // int nPage 389 | ], 390 | result: "i32", 391 | }, 392 | 393 | sqlite3_backup_finish: { 394 | parameters: [ 395 | "pointer", // sqlite3_backup *p 396 | ], 397 | result: "i32", 398 | }, 399 | 400 | sqlite3_backup_remaining: { 401 | parameters: [ 402 | "pointer", // sqlite3_backup *p 403 | ], 404 | result: "i32", 405 | }, 406 | 407 | sqlite3_backup_pagecount: { 408 | parameters: [ 409 | "pointer", // sqlite3_backup *p 410 | ], 411 | result: "i32", 412 | }, 413 | 414 | sqlite3_create_function: { 415 | parameters: [ 416 | "pointer", // sqlite3 *db 417 | "buffer", // const char *zFunctionName 418 | "i32", // int nArg 419 | "i32", // int eTextRep 420 | "pointer", // void *pApp 421 | "pointer", // void (*xFunc)(sqlite3_context*,int,sqlite3_value**) 422 | "pointer", // void (*xStep)(sqlite3_context*,int,sqlite3_value**) 423 | "pointer", // void (*xFinal)(sqlite3_context*) 424 | ], 425 | result: "i32", 426 | optional: true, 427 | }, 428 | 429 | sqlite3_result_blob: { 430 | parameters: [ 431 | "pointer", // sqlite3_context *p 432 | "buffer", // const void *z 433 | "i32", // int n 434 | "isize", // void (*xDel)(void*) 435 | ], 436 | result: "void", 437 | }, 438 | 439 | sqlite3_result_double: { 440 | parameters: [ 441 | "pointer", // sqlite3_context *p 442 | "f64", // double rVal 443 | ], 444 | result: "void", 445 | }, 446 | 447 | sqlite3_result_error: { 448 | parameters: [ 449 | "pointer", // sqlite3_context *p 450 | "buffer", // const char *z 451 | "i32", // int n 452 | ], 453 | result: "void", 454 | }, 455 | 456 | sqlite3_result_int: { 457 | parameters: [ 458 | "pointer", // sqlite3_context *p 459 | "i32", // int iVal 460 | ], 461 | result: "void", 462 | }, 463 | 464 | sqlite3_result_int64: { 465 | parameters: [ 466 | "pointer", // sqlite3_context *p 467 | "i64", // sqlite3_int64 iVal 468 | ], 469 | result: "void", 470 | }, 471 | 472 | sqlite3_result_null: { 473 | parameters: [ 474 | "pointer", // sqlite3_context *p 475 | ], 476 | result: "void", 477 | }, 478 | 479 | sqlite3_result_text: { 480 | parameters: [ 481 | "pointer", // sqlite3_context *p 482 | "buffer", // const char *z 483 | "i32", // int n 484 | "isize", // void (*xDel)(void*) 485 | ], 486 | result: "void", 487 | }, 488 | 489 | sqlite3_value_type: { 490 | parameters: [ 491 | "pointer", // sqlite3_value *pVal 492 | ], 493 | result: "i32", 494 | }, 495 | sqlite3_value_subtype: { 496 | parameters: [ 497 | "pointer", // sqlite3_value *pVal 498 | ], 499 | result: "i32", 500 | }, 501 | 502 | sqlite3_value_blob: { 503 | parameters: [ 504 | "pointer", // sqlite3_value *pVal 505 | ], 506 | result: "pointer", 507 | }, 508 | 509 | sqlite3_value_double: { 510 | parameters: [ 511 | "pointer", // sqlite3_value *pVal 512 | ], 513 | result: "f64", 514 | }, 515 | 516 | sqlite3_value_int: { 517 | parameters: [ 518 | "pointer", // sqlite3_value *pVal 519 | ], 520 | result: "i32", 521 | }, 522 | 523 | sqlite3_value_int64: { 524 | parameters: [ 525 | "pointer", // sqlite3_value *pVal 526 | ], 527 | result: "i64", 528 | }, 529 | 530 | sqlite3_value_text: { 531 | parameters: [ 532 | "pointer", // sqlite3_value *pVal 533 | ], 534 | result: "pointer", 535 | }, 536 | 537 | sqlite3_value_bytes: { 538 | parameters: [ 539 | "pointer", // sqlite3_value *pVal 540 | ], 541 | result: "i32", 542 | }, 543 | 544 | sqlite3_aggregate_context: { 545 | parameters: [ 546 | "pointer", // sqlite3_context *p 547 | "i32", // int nBytes 548 | ], 549 | result: "pointer", 550 | optional: true, 551 | }, 552 | 553 | sqlite3_enable_load_extension: { 554 | parameters: [ 555 | "pointer", // sqlite3 *db 556 | "i32", // int onoff 557 | ], 558 | result: "i32", 559 | optional: true, 560 | }, 561 | 562 | sqlite3_load_extension: { 563 | parameters: [ 564 | "pointer", // sqlite3 *db 565 | "buffer", // const char *zFile 566 | "buffer", // const char *zProc 567 | "buffer", // const char **pzErrMsg 568 | ], 569 | result: "i32", 570 | optional: true, 571 | }, 572 | 573 | sqlite3_initialize: { 574 | parameters: [], 575 | result: "i32", 576 | }, 577 | 578 | sqlite3_update_hook: { 579 | parameters: [ 580 | "pointer", // sqlite3 *db 581 | "pointer", // void *pArg 582 | "pointer", // void (*xUpdate)(sqlite3_context*,int,sqlite3_value**,sqlite3_value**) 583 | ], 584 | result: "pointer", 585 | }, 586 | } as const satisfies Deno.ForeignLibraryInterface; 587 | 588 | let lib: Deno.DynamicLibrary["symbols"]; 589 | 590 | function tryGetEnv(key: string): string | undefined { 591 | try { 592 | return Deno.env.get(key); 593 | } catch (e) { 594 | if (e instanceof Deno.errors.PermissionDenied) { 595 | return undefined; 596 | } 597 | throw e; 598 | } 599 | } 600 | 601 | try { 602 | const customPath = tryGetEnv("DENO_SQLITE_PATH"); 603 | const sqliteLocal = tryGetEnv("DENO_SQLITE_LOCAL"); 604 | 605 | if (sqliteLocal === "1") { 606 | lib = Deno.dlopen( 607 | new URL( 608 | `../build/${Deno.build.os === "windows" ? "" : "lib"}sqlite3${ 609 | Deno.build.arch !== "x86_64" ? `_${Deno.build.arch}` : "" 610 | }.${ 611 | Deno.build.os === "windows" 612 | ? "dll" 613 | : Deno.build.os === "darwin" 614 | ? "dylib" 615 | : "so" 616 | }`, 617 | import.meta.url, 618 | ), 619 | symbols, 620 | ).symbols; 621 | } else if (customPath) { 622 | lib = Deno.dlopen(customPath, symbols).symbols; 623 | } else { 624 | lib = ( 625 | await dlopen( 626 | { 627 | name: "sqlite3", 628 | url: `${meta.github}/releases/download/${meta.version}/`, 629 | suffixes: { 630 | aarch64: "_aarch64", 631 | }, 632 | }, 633 | symbols, 634 | ) 635 | ).symbols; 636 | } 637 | } catch (e) { 638 | if (e instanceof Deno.errors.PermissionDenied) { 639 | throw e; 640 | } 641 | 642 | throw new Error("Failed to load SQLite3 Dynamic Library", { cause: e }); 643 | } 644 | 645 | const init = lib.sqlite3_initialize(); 646 | if (init !== 0) { 647 | throw new Error(`Failed to initialize SQLite3: ${init}`); 648 | } 649 | 650 | export default lib; 651 | -------------------------------------------------------------------------------- /src/statement.ts: -------------------------------------------------------------------------------- 1 | import type { Database } from "./database.ts"; 2 | import { readCstr, toCString, unwrap } from "./util.ts"; 3 | import ffi from "./ffi.ts"; 4 | import { 5 | SQLITE3_DONE, 6 | SQLITE3_ROW, 7 | SQLITE_BLOB, 8 | SQLITE_FLOAT, 9 | SQLITE_INTEGER, 10 | SQLITE_TEXT, 11 | } from "./constants.ts"; 12 | 13 | const { 14 | sqlite3_prepare_v2, 15 | sqlite3_reset, 16 | sqlite3_clear_bindings, 17 | sqlite3_step, 18 | sqlite3_column_count, 19 | sqlite3_column_type, 20 | sqlite3_column_value, 21 | sqlite3_value_subtype, 22 | sqlite3_column_text, 23 | sqlite3_finalize, 24 | sqlite3_column_int64, 25 | sqlite3_column_double, 26 | sqlite3_column_blob, 27 | sqlite3_column_bytes, 28 | sqlite3_column_name, 29 | sqlite3_expanded_sql, 30 | sqlite3_bind_parameter_count, 31 | sqlite3_bind_int, 32 | sqlite3_bind_int64, 33 | sqlite3_bind_text, 34 | sqlite3_bind_blob, 35 | sqlite3_bind_double, 36 | sqlite3_bind_parameter_index, 37 | sqlite3_sql, 38 | sqlite3_stmt_readonly, 39 | sqlite3_bind_parameter_name, 40 | sqlite3_changes, 41 | sqlite3_column_int, 42 | } = ffi; 43 | 44 | /** Types that can be possibly serialized as SQLite bind values */ 45 | export type BindValue = 46 | | number 47 | | string 48 | | symbol 49 | | bigint 50 | | boolean 51 | | null 52 | | undefined 53 | | Date 54 | | Uint8Array 55 | | BindValue[] 56 | | { [key: string]: BindValue }; 57 | 58 | export type BindParameters = BindValue[] | Record; 59 | export type RestBindParameters = BindValue[] | [BindParameters]; 60 | 61 | /** Maps sqlite_stmt* pointers to sqlite* db pointers. */ 62 | export const STATEMENTS_TO_DB = new Map(); 63 | 64 | const emptyStringBuffer = new Uint8Array(1); 65 | 66 | const statementFinalizer = new FinalizationRegistry( 67 | (ptr: Deno.PointerValue) => { 68 | if (STATEMENTS_TO_DB.has(ptr)) { 69 | sqlite3_finalize(ptr); 70 | STATEMENTS_TO_DB.delete(ptr); 71 | } 72 | }, 73 | ); 74 | 75 | // https://github.com/sqlite/sqlite/blob/195611d8e6fc0bba559a49e91e6ceb42e4bdd6ba/src/json.c#L125-L126 76 | const JSON_SUBTYPE = 74; 77 | 78 | const BIG_MAX = BigInt(Number.MAX_SAFE_INTEGER); 79 | 80 | function getColumn( 81 | handle: Deno.PointerValue, 82 | i: number, 83 | int64: boolean, 84 | parseJson: boolean, 85 | ): any { 86 | const ty = sqlite3_column_type(handle, i); 87 | 88 | if (ty === SQLITE_INTEGER && !int64) return sqlite3_column_int(handle, i); 89 | 90 | switch (ty) { 91 | case SQLITE_TEXT: { 92 | const ptr = sqlite3_column_text(handle, i); 93 | if (ptr === null) return null; 94 | const text = readCstr(ptr, 0); 95 | const value = sqlite3_column_value(handle, i); 96 | const subtype = sqlite3_value_subtype(value); 97 | if (subtype === JSON_SUBTYPE && parseJson) { 98 | try { 99 | return JSON.parse(text); 100 | } catch (_error) { 101 | return text; 102 | } 103 | } 104 | return text; 105 | } 106 | 107 | case SQLITE_INTEGER: { 108 | const val = sqlite3_column_int64(handle, i); 109 | if (val < -BIG_MAX || val > BIG_MAX) { 110 | return val; 111 | } 112 | return Number(val); 113 | } 114 | 115 | case SQLITE_FLOAT: { 116 | return sqlite3_column_double(handle, i); 117 | } 118 | 119 | case SQLITE_BLOB: { 120 | const ptr = sqlite3_column_blob(handle, i); 121 | 122 | if (ptr === null) { 123 | return new Uint8Array(); 124 | } 125 | 126 | const bytes = sqlite3_column_bytes(handle, i); 127 | return new Uint8Array( 128 | Deno.UnsafePointerView.getArrayBuffer(ptr, bytes).slice(0), 129 | ); 130 | } 131 | 132 | default: { 133 | return null; 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * Represents a prepared statement. 140 | * 141 | * See `Database#prepare` for more information. 142 | */ 143 | export class Statement> { 144 | #handle: Deno.PointerValue; 145 | #finalizerToken: { handle: Deno.PointerValue }; 146 | #bound = false; 147 | #hasNoArgs = false; 148 | #unsafeConcurrency; 149 | 150 | /** Unsafe Raw (pointer) to the sqlite object */ 151 | get unsafeHandle(): Deno.PointerValue { 152 | return this.#handle; 153 | } 154 | 155 | /** SQL string including bindings */ 156 | get expandedSql(): string { 157 | return readCstr(sqlite3_expanded_sql(this.#handle)!); 158 | } 159 | 160 | /** The SQL string that we passed when creating statement */ 161 | get sql(): string { 162 | return readCstr(sqlite3_sql(this.#handle)!); 163 | } 164 | 165 | /** Whether this statement doesn't make any direct changes to the DB */ 166 | get readonly(): boolean { 167 | return sqlite3_stmt_readonly(this.#handle) !== 0; 168 | } 169 | 170 | /** Simply run the query without retrieving any output there may be. */ 171 | run(...args: RestBindParameters): number { 172 | return this.#runWithArgs(...args); 173 | } 174 | 175 | /** 176 | * Run the query and return the resulting rows where rows are array of columns. 177 | */ 178 | values(...args: RestBindParameters): T[] { 179 | return this.#valuesWithArgs(...args); 180 | } 181 | 182 | /** 183 | * Run the query and return the resulting rows where rows are objects 184 | * mapping column name to their corresponding values. 185 | */ 186 | all( 187 | ...args: RestBindParameters 188 | ): T[] { 189 | return this.#allWithArgs(...args); 190 | } 191 | 192 | #bindParameterCount: number; 193 | 194 | /** Number of parameters (to be) bound */ 195 | get bindParameterCount(): number { 196 | return this.#bindParameterCount; 197 | } 198 | 199 | int64?: boolean; 200 | parseJson?: boolean; 201 | 202 | enableInt64(): this { 203 | this.int64 = true; 204 | return this; 205 | } 206 | 207 | enableParseJson(): this { 208 | this.parseJson = true; 209 | return this; 210 | } 211 | 212 | disableInt64(): this { 213 | this.int64 = false; 214 | return this; 215 | } 216 | 217 | disableParseJson(): this { 218 | this.parseJson = false; 219 | return this; 220 | } 221 | 222 | defaultInt64(): this { 223 | this.int64 = undefined; 224 | return this; 225 | } 226 | 227 | defaultParseJson(): this { 228 | this.parseJson = undefined; 229 | return this; 230 | } 231 | 232 | constructor(public db: Database, sql: string) { 233 | const pHandle = new BigUint64Array(1); 234 | const cString = toCString(sql); 235 | unwrap( 236 | sqlite3_prepare_v2( 237 | db.unsafeHandle, 238 | cString, 239 | cString.byteLength, 240 | pHandle, 241 | null, 242 | ), 243 | db.unsafeHandle, 244 | ); 245 | this.#handle = Deno.UnsafePointer.create(pHandle[0]); 246 | STATEMENTS_TO_DB.set(this.#handle, db.unsafeHandle); 247 | this.#unsafeConcurrency = db.unsafeConcurrency; 248 | this.#finalizerToken = { handle: this.#handle }; 249 | statementFinalizer.register(this, this.#handle, this.#finalizerToken); 250 | 251 | if ( 252 | (this.#bindParameterCount = sqlite3_bind_parameter_count( 253 | this.#handle, 254 | )) === 0 255 | ) { 256 | this.#hasNoArgs = true; 257 | this.all = this.#allNoArgs; 258 | this.values = this.#valuesNoArgs; 259 | this.run = this.#runNoArgs; 260 | this.value = this.#valueNoArgs; 261 | this.get = this.#getNoArgs; 262 | } 263 | } 264 | 265 | /** Get bind parameter name by index */ 266 | bindParameterName(i: number): string { 267 | return readCstr(sqlite3_bind_parameter_name(this.#handle, i)!); 268 | } 269 | 270 | /** Get bind parameter index by name */ 271 | bindParameterIndex(name: string): number { 272 | if (name[0] !== ":" && name[0] !== "@" && name[0] !== "$") { 273 | name = ":" + name; 274 | } 275 | return sqlite3_bind_parameter_index(this.#handle, toCString(name)); 276 | } 277 | 278 | #begin(): void { 279 | sqlite3_reset(this.#handle); 280 | if (!this.#bound && !this.#hasNoArgs) { 281 | sqlite3_clear_bindings(this.#handle); 282 | this.#bindRefs.clear(); 283 | } 284 | } 285 | 286 | #bindRefs: Set = new Set(); 287 | 288 | #bind(i: number, param: BindValue): void { 289 | switch (typeof param) { 290 | case "number": { 291 | if (Number.isInteger(param)) { 292 | if ( 293 | Number.isSafeInteger(param) && param >= -(2 ** 31) && 294 | param < 2 ** 31 295 | ) { 296 | unwrap(sqlite3_bind_int(this.#handle, i + 1, param)); 297 | } else { 298 | unwrap(sqlite3_bind_int64(this.#handle, i + 1, BigInt(param))); 299 | } 300 | } else { 301 | unwrap(sqlite3_bind_double(this.#handle, i + 1, param)); 302 | } 303 | break; 304 | } 305 | case "string": { 306 | if (param === "") { 307 | // Empty string is encoded as empty buffer in Deno. And as of 308 | // right now (Deno 1.29.1), ffi layer converts it to NULL pointer, 309 | // which causes sqlite3_bind_text to bind the NULL value instead 310 | // of an empty string. As a workaround let's use a special 311 | // non-empty buffer, but specify zero length. 312 | unwrap( 313 | sqlite3_bind_text(this.#handle, i + 1, emptyStringBuffer, 0, null), 314 | ); 315 | } else { 316 | const str = new TextEncoder().encode(param); 317 | this.#bindRefs.add(str); 318 | unwrap( 319 | sqlite3_bind_text(this.#handle, i + 1, str, str.byteLength, null), 320 | ); 321 | } 322 | break; 323 | } 324 | case "object": { 325 | if (param === null) { 326 | // pass 327 | } else if (param instanceof Uint8Array) { 328 | this.#bindRefs.add(param); 329 | unwrap( 330 | sqlite3_bind_blob( 331 | this.#handle, 332 | i + 1, 333 | param.byteLength === 0 ? emptyStringBuffer : param, 334 | param.byteLength, 335 | null, 336 | ), 337 | ); 338 | } else if (param instanceof Date) { 339 | const cstring = toCString(param.toISOString()); 340 | this.#bindRefs.add(cstring); 341 | unwrap( 342 | sqlite3_bind_text( 343 | this.#handle, 344 | i + 1, 345 | cstring, 346 | -1, 347 | null, 348 | ), 349 | ); 350 | } else { 351 | const cstring = toCString(JSON.stringify(param)); 352 | this.#bindRefs.add(cstring); 353 | unwrap( 354 | sqlite3_bind_text( 355 | this.#handle, 356 | i + 1, 357 | cstring, 358 | -1, 359 | null, 360 | ), 361 | ); 362 | } 363 | break; 364 | } 365 | 366 | case "bigint": { 367 | unwrap(sqlite3_bind_int64(this.#handle, i + 1, param)); 368 | break; 369 | } 370 | 371 | case "boolean": 372 | unwrap(sqlite3_bind_int( 373 | this.#handle, 374 | i + 1, 375 | param ? 1 : 0, 376 | )); 377 | break; 378 | 379 | case "undefined": 380 | // pass 381 | break; 382 | 383 | default: { 384 | throw new Error(`Value of unsupported type: ${Deno.inspect(param)}`); 385 | } 386 | } 387 | } 388 | 389 | /** 390 | * Bind parameters to the statement. This method can only be called once 391 | * to set the parameters to be same throughout the statement. You cannot 392 | * change the parameters after this method is called. 393 | * 394 | * This method is merely just for optimization to avoid binding parameters 395 | * each time in prepared statement. 396 | */ 397 | bind(...params: RestBindParameters): this { 398 | this.#bindAll(params); 399 | this.#bound = true; 400 | return this; 401 | } 402 | 403 | #bindAll(params: RestBindParameters | BindParameters): void { 404 | if (this.#bound) throw new Error("Statement already bound to values"); 405 | if ( 406 | typeof params[0] === "object" && params[0] !== null && 407 | !(params[0] instanceof Uint8Array) && !(params[0] instanceof Date) 408 | ) { 409 | params = params[0]; 410 | } 411 | if (Array.isArray(params)) { 412 | for (let i = 0; i < params.length; i++) { 413 | this.#bind(i, (params as BindValue[])[i]); 414 | } 415 | } else { 416 | for (const [name, param] of Object.entries(params)) { 417 | const i = this.bindParameterIndex(name); 418 | if (i === 0) { 419 | throw new Error(`No such parameter "${name}"`); 420 | } 421 | this.#bind(i - 1, param as BindValue); 422 | } 423 | } 424 | } 425 | 426 | #runNoArgs(): number { 427 | const handle = this.#handle; 428 | this.#begin(); 429 | const status = sqlite3_step(handle); 430 | if (status !== SQLITE3_ROW && status !== SQLITE3_DONE) { 431 | unwrap(status, this.db.unsafeHandle); 432 | } 433 | sqlite3_reset(handle); 434 | return sqlite3_changes(this.db.unsafeHandle); 435 | } 436 | 437 | #runWithArgs(...params: RestBindParameters): number { 438 | const handle = this.#handle; 439 | this.#begin(); 440 | this.#bindAll(params); 441 | const status = sqlite3_step(handle); 442 | if (!this.#hasNoArgs && !this.#bound && params.length) { 443 | this.#bindRefs.clear(); 444 | } 445 | if (status !== SQLITE3_ROW && status !== SQLITE3_DONE) { 446 | unwrap(status, this.db.unsafeHandle); 447 | } 448 | sqlite3_reset(handle); 449 | return sqlite3_changes(this.db.unsafeHandle); 450 | } 451 | 452 | #valuesNoArgs>(): T[] { 453 | const handle = this.#handle; 454 | this.#begin(); 455 | const columnCount = sqlite3_column_count(handle); 456 | const result: T[] = []; 457 | const getRowArray = new Function( 458 | "getColumn", 459 | ` 460 | return function(h, int64, parseJson) { 461 | return [${ 462 | Array.from({ length: columnCount }).map((_, i) => 463 | `getColumn(h, ${i}, int64, parseJson)` 464 | ) 465 | .join(", ") 466 | }]; 467 | }; 468 | `, 469 | )(getColumn); 470 | let status = sqlite3_step(handle); 471 | while (status === SQLITE3_ROW) { 472 | result.push( 473 | getRowArray( 474 | handle, 475 | this.int64 ?? this.db.int64, 476 | this.parseJson ?? this.db.parseJson, 477 | ), 478 | ); 479 | status = sqlite3_step(handle); 480 | } 481 | if (status !== SQLITE3_DONE) { 482 | unwrap(status, this.db.unsafeHandle); 483 | } 484 | sqlite3_reset(handle); 485 | return result as T[]; 486 | } 487 | 488 | #valuesWithArgs>( 489 | ...params: RestBindParameters 490 | ): T[] { 491 | const handle = this.#handle; 492 | this.#begin(); 493 | this.#bindAll(params); 494 | const columnCount = sqlite3_column_count(handle); 495 | const result: T[] = []; 496 | const getRowArray = new Function( 497 | "getColumn", 498 | ` 499 | return function(h, int64, parseJson) { 500 | return [${ 501 | Array.from({ length: columnCount }).map((_, i) => 502 | `getColumn(h, ${i}, int64, parseJson)` 503 | ) 504 | .join(", ") 505 | }]; 506 | }; 507 | `, 508 | )(getColumn); 509 | let status = sqlite3_step(handle); 510 | while (status === SQLITE3_ROW) { 511 | result.push( 512 | getRowArray( 513 | handle, 514 | this.int64 ?? this.db.int64, 515 | this.parseJson ?? this.db.parseJson, 516 | ), 517 | ); 518 | status = sqlite3_step(handle); 519 | } 520 | if (!this.#hasNoArgs && !this.#bound && params.length) { 521 | this.#bindRefs.clear(); 522 | } 523 | if (status !== SQLITE3_DONE) { 524 | unwrap(status, this.db.unsafeHandle); 525 | } 526 | sqlite3_reset(handle); 527 | return result as T[]; 528 | } 529 | 530 | #rowObjectFn: 531 | | ((h: Deno.PointerValue, int64: boolean, parseJson: boolean) => any) 532 | | undefined; 533 | 534 | getRowObject(): ( 535 | h: Deno.PointerValue, 536 | int64: boolean, 537 | parseJson: boolean, 538 | ) => any { 539 | if (!this.#rowObjectFn || !this.#unsafeConcurrency) { 540 | const columnNames = this.columnNames(); 541 | const getRowObject = new Function( 542 | "getColumn", 543 | ` 544 | return function(h, int64, parseJson) { 545 | return { 546 | ${ 547 | columnNames.map((name, i) => 548 | `"${name}": getColumn(h, ${i}, int64, parseJson)` 549 | ).join(",\n") 550 | } 551 | }; 552 | }; 553 | `, 554 | )(getColumn); 555 | this.#rowObjectFn = getRowObject; 556 | } 557 | return this.#rowObjectFn!; 558 | } 559 | 560 | #allNoArgs(): T[] { 561 | const handle = this.#handle; 562 | const int64 = this.int64 ?? this.db.int64; 563 | const parseJson = this.parseJson ?? this.db.parseJson; 564 | this.#begin(); 565 | const getRowObject = this.getRowObject(); 566 | const result: T[] = []; 567 | let status = sqlite3_step(handle); 568 | while (status === SQLITE3_ROW) { 569 | result.push(getRowObject(handle, int64, parseJson)); 570 | status = sqlite3_step(handle); 571 | } 572 | if (status !== SQLITE3_DONE) { 573 | unwrap(status, this.db.unsafeHandle); 574 | } 575 | sqlite3_reset(handle); 576 | return result as T[]; 577 | } 578 | 579 | #allWithArgs( 580 | ...params: RestBindParameters 581 | ): T[] { 582 | const handle = this.#handle; 583 | const int64 = this.int64 ?? this.db.int64; 584 | const parseJson = this.parseJson ?? this.db.parseJson; 585 | this.#begin(); 586 | this.#bindAll(params); 587 | const getRowObject = this.getRowObject(); 588 | const result: T[] = []; 589 | let status = sqlite3_step(handle); 590 | while (status === SQLITE3_ROW) { 591 | result.push(getRowObject(handle, int64, parseJson)); 592 | status = sqlite3_step(handle); 593 | } 594 | if (!this.#hasNoArgs && !this.#bound && params.length) { 595 | this.#bindRefs.clear(); 596 | } 597 | if (status !== SQLITE3_DONE) { 598 | unwrap(status, this.db.unsafeHandle); 599 | } 600 | sqlite3_reset(handle); 601 | return result as T[]; 602 | } 603 | 604 | /** Fetch only first row as an array, if any. */ 605 | value>( 606 | ...params: RestBindParameters 607 | ): T | undefined { 608 | const handle = this.#handle; 609 | const int64 = this.int64 ?? this.db.int64; 610 | const parseJson = this.parseJson ?? this.db.parseJson; 611 | const arr = new Array(sqlite3_column_count(handle)); 612 | sqlite3_reset(handle); 613 | if (!this.#hasNoArgs && !this.#bound) { 614 | sqlite3_clear_bindings(handle); 615 | this.#bindRefs.clear(); 616 | if (params.length) { 617 | this.#bindAll(params); 618 | } 619 | } 620 | 621 | const status = sqlite3_step(handle); 622 | 623 | if (!this.#hasNoArgs && !this.#bound && params.length) { 624 | this.#bindRefs.clear(); 625 | } 626 | 627 | if (status === SQLITE3_ROW) { 628 | for (let i = 0; i < arr.length; i++) { 629 | arr[i] = getColumn(handle, i, int64, parseJson); 630 | } 631 | sqlite3_reset(this.#handle); 632 | return arr as T; 633 | } else if (status === SQLITE3_DONE) { 634 | return; 635 | } else { 636 | unwrap(status, this.db.unsafeHandle); 637 | } 638 | } 639 | 640 | #valueNoArgs>(): T | undefined { 641 | const handle = this.#handle; 642 | const int64 = this.int64 ?? this.db.int64; 643 | const parseJson = this.parseJson ?? this.db.parseJson; 644 | const cc = sqlite3_column_count(handle); 645 | const arr = new Array(cc); 646 | sqlite3_reset(handle); 647 | const status = sqlite3_step(handle); 648 | if (status === SQLITE3_ROW) { 649 | for (let i = 0; i < cc; i++) { 650 | arr[i] = getColumn(handle, i, int64, parseJson); 651 | } 652 | sqlite3_reset(this.#handle); 653 | return arr as T; 654 | } else if (status === SQLITE3_DONE) { 655 | return; 656 | } else { 657 | unwrap(status, this.db.unsafeHandle); 658 | } 659 | } 660 | 661 | #columnNames: string[] | undefined; 662 | #rowObject: Record = {}; 663 | 664 | columnNames(): string[] { 665 | if (!this.#columnNames || !this.#unsafeConcurrency) { 666 | const columnCount = sqlite3_column_count(this.#handle); 667 | const columnNames = new Array(columnCount); 668 | for (let i = 0; i < columnCount; i++) { 669 | columnNames[i] = readCstr(sqlite3_column_name(this.#handle, i)!); 670 | } 671 | this.#columnNames = columnNames; 672 | this.#rowObject = {}; 673 | for (const name of columnNames) { 674 | this.#rowObject![name] = undefined; 675 | } 676 | } 677 | return this.#columnNames!; 678 | } 679 | 680 | /** Fetch only first row as an object, if any. */ 681 | get( 682 | ...params: RestBindParameters 683 | ): T | undefined { 684 | const handle = this.#handle; 685 | const int64 = this.int64 ?? this.db.int64; 686 | const parseJson = this.parseJson ?? this.db.parseJson; 687 | const columnNames = this.columnNames(); 688 | 689 | const row: Record = {}; 690 | sqlite3_reset(handle); 691 | if (!this.#hasNoArgs && !this.#bound) { 692 | sqlite3_clear_bindings(handle); 693 | this.#bindRefs.clear(); 694 | if (params.length) { 695 | this.#bindAll(params); 696 | } 697 | } 698 | 699 | const status = sqlite3_step(handle); 700 | 701 | if (!this.#hasNoArgs && !this.#bound && params.length) { 702 | this.#bindRefs.clear(); 703 | } 704 | 705 | if (status === SQLITE3_ROW) { 706 | for (let i = 0; i < columnNames.length; i++) { 707 | row[columnNames[i]] = getColumn(handle, i, int64, parseJson); 708 | } 709 | sqlite3_reset(this.#handle); 710 | return row as T; 711 | } else if (status === SQLITE3_DONE) { 712 | return; 713 | } else { 714 | unwrap(status, this.db.unsafeHandle); 715 | } 716 | } 717 | 718 | #getNoArgs(): T | undefined { 719 | const handle = this.#handle; 720 | const int64 = this.int64 ?? this.db.int64; 721 | const parseJson = this.parseJson ?? this.db.parseJson; 722 | const columnNames = this.columnNames(); 723 | const row: Record = this.#rowObject; 724 | sqlite3_reset(handle); 725 | const status = sqlite3_step(handle); 726 | if (status === SQLITE3_ROW) { 727 | for (let i = 0; i < columnNames?.length; i++) { 728 | row[columnNames[i]] = getColumn(handle, i, int64, parseJson); 729 | } 730 | sqlite3_reset(handle); 731 | return row as T; 732 | } else if (status === SQLITE3_DONE) { 733 | return; 734 | } else { 735 | unwrap(status, this.db.unsafeHandle); 736 | } 737 | } 738 | 739 | /** Free up the statement object. */ 740 | finalize(): void { 741 | if (!STATEMENTS_TO_DB.has(this.#handle)) return; 742 | this.#bindRefs.clear(); 743 | statementFinalizer.unregister(this.#finalizerToken); 744 | STATEMENTS_TO_DB.delete(this.#handle); 745 | unwrap(sqlite3_finalize(this.#handle)); 746 | } 747 | 748 | /** Coerces the statement to a string, which in this case is expanded SQL. */ 749 | toString(): string { 750 | return readCstr(sqlite3_expanded_sql(this.#handle)!); 751 | } 752 | 753 | /** Iterate over resultant rows from query. */ 754 | *iter(...params: RestBindParameters): IterableIterator { 755 | this.#begin(); 756 | this.#bindAll(params); 757 | const getRowObject = this.getRowObject(); 758 | const int64 = this.int64 ?? this.db.int64; 759 | const parseJson = this.parseJson ?? this.db.parseJson; 760 | let status = sqlite3_step(this.#handle); 761 | while (status === SQLITE3_ROW) { 762 | yield getRowObject(this.#handle, int64, parseJson); 763 | status = sqlite3_step(this.#handle); 764 | } 765 | if (status !== SQLITE3_DONE) { 766 | unwrap(status, this.db.unsafeHandle); 767 | } 768 | sqlite3_reset(this.#handle); 769 | } 770 | 771 | [Symbol.iterator](): IterableIterator { 772 | return this.iter(); 773 | } 774 | 775 | [Symbol.dispose](): void { 776 | this.finalize(); 777 | } 778 | 779 | [Symbol.for("Deno.customInspect")](): string { 780 | return `Statement { ${this.expandedSql} }`; 781 | } 782 | } 783 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { SQLITE3_DONE, SQLITE3_MISUSE, SQLITE3_OK } from "./constants.ts"; 2 | import ffi from "./ffi.ts"; 3 | 4 | const { 5 | sqlite3_errmsg, 6 | sqlite3_errstr, 7 | } = ffi; 8 | 9 | export const encoder = new TextEncoder(); 10 | 11 | export function toCString(str: string): Uint8Array { 12 | return encoder.encode(str + "\0"); 13 | } 14 | 15 | export function isObject(value: unknown): boolean { 16 | return typeof value === "object" && value !== null; 17 | } 18 | 19 | export class SqliteError extends Error { 20 | override name = "SqliteError"; 21 | 22 | constructor( 23 | public code: number = 1, 24 | message: string = "Unknown Error", 25 | ) { 26 | super(`${code}: ${message}`); 27 | } 28 | } 29 | 30 | export function unwrap(code: number, db?: Deno.PointerValue): void { 31 | if (code === SQLITE3_OK || code === SQLITE3_DONE) return; 32 | if (code === SQLITE3_MISUSE) { 33 | throw new SqliteError(code, "SQLite3 API misuse"); 34 | } else if (db !== undefined) { 35 | const errmsg = sqlite3_errmsg(db); 36 | if (errmsg === null) throw new SqliteError(code); 37 | throw new Error(Deno.UnsafePointerView.getCString(errmsg)); 38 | } else { 39 | throw new SqliteError( 40 | code, 41 | Deno.UnsafePointerView.getCString(sqlite3_errstr(code)!), 42 | ); 43 | } 44 | } 45 | 46 | export const buf = Deno.UnsafePointerView.getArrayBuffer; 47 | 48 | export const readCstr = Deno.UnsafePointerView.getCString; 49 | -------------------------------------------------------------------------------- /test/deps.ts: -------------------------------------------------------------------------------- 1 | export * from "jsr:@std/assert"; 2 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Database, 3 | isComplete, 4 | SQLITE_SOURCEID, 5 | SQLITE_VERSION, 6 | SqliteError, 7 | } from "../mod.ts"; 8 | import { assert, assertEquals, assertThrows } from "./deps.ts"; 9 | 10 | console.log("sqlite version:", SQLITE_VERSION); 11 | 12 | Deno.test("sqlite", async (t) => { 13 | await t.step("sourceid", () => { 14 | assert(SQLITE_SOURCEID.length > 0); 15 | }); 16 | 17 | await t.step("is complete", () => { 18 | assert(!isComplete("")); 19 | assert(!isComplete("select sqlite_version()")); 20 | 21 | assert(isComplete("select x from y;")); 22 | assert(isComplete("select sqlite_version();")); 23 | }); 24 | 25 | const DB_URL = new URL("./test.db", import.meta.url); 26 | 27 | // Remove any existing test.db. 28 | await Deno.remove(DB_URL).catch(() => {}); 29 | 30 | await t.step("open (expect error)", () => { 31 | assertThrows( 32 | () => new Database(DB_URL, { create: false }), 33 | SqliteError, 34 | "14:", 35 | ); 36 | }); 37 | 38 | await t.step("open (path string)", () => { 39 | const db = new Database("test-path.db"); 40 | db.close(); 41 | Deno.removeSync("test-path.db"); 42 | }); 43 | 44 | await t.step("open (readonly)", () => { 45 | const db = new Database(":memory:", { readonly: true }); 46 | db.close(); 47 | }); 48 | 49 | let db!: Database; 50 | await t.step("open (url)", () => { 51 | db = new Database(DB_URL, { int64: true }); 52 | }); 53 | 54 | if (typeof db !== "object") throw new Error("db open failed"); 55 | 56 | await t.step("execute pragma", () => { 57 | db.exec("pragma journal_mode = WAL"); 58 | db.exec("pragma synchronous = normal"); 59 | assertEquals(db.exec("pragma temp_store = memory"), 0); 60 | }); 61 | 62 | await t.step("select version (row as array)", () => { 63 | const [version] = db.prepare("select sqlite_version()").value<[string]>()!; 64 | assertEquals(version, SQLITE_VERSION); 65 | }); 66 | 67 | await t.step("select version (row as object)", () => { 68 | const { version } = db.prepare("select sqlite_version() as version").get< 69 | { version: string } 70 | >()!; 71 | assertEquals(version, SQLITE_VERSION); 72 | }); 73 | 74 | await t.step("autocommit", () => { 75 | assertEquals(db.autocommit, true); 76 | }); 77 | 78 | await t.step("last insert row id", () => { 79 | assertEquals(db.lastInsertRowId, 0); 80 | }); 81 | 82 | await t.step("create table", () => { 83 | db.exec(`create table test ( 84 | integer integer, 85 | text text not null, 86 | double double, 87 | blob blob not null, 88 | nullable integer 89 | )`); 90 | }); 91 | 92 | await t.step("insert one", () => { 93 | db.exec( 94 | `insert into test (integer, text, double, blob, nullable) 95 | values (?, ?, ?, ?, ?)`, 96 | 0, 97 | "hello world", 98 | 3.14, 99 | new Uint8Array([1, 2, 3]), 100 | null, 101 | ); 102 | 103 | assertEquals(db.totalChanges, 1); 104 | }); 105 | 106 | await t.step("delete inserted row", () => { 107 | db.exec("delete from test where integer = 0"); 108 | }); 109 | 110 | await t.step("last insert row id (after insert)", () => { 111 | assertEquals(db.lastInsertRowId, 1); 112 | }); 113 | 114 | await t.step("prepared insert", () => { 115 | const SQL = `insert into test (integer, text, double, blob, nullable) 116 | values (?, ?, ?, ?, ?)`; 117 | const stmt = db.prepare(SQL); 118 | assertEquals( 119 | stmt.toString(), 120 | `insert into test (integer, text, double, blob, nullable) 121 | values (NULL, NULL, NULL, NULL, NULL)`, 122 | ); 123 | 124 | const insertMany = db.transaction((data: any[]) => { 125 | for (const row of data) { 126 | stmt.run(row); 127 | } 128 | }); 129 | 130 | const rows = []; 131 | for (let i = 0; i < 10; i++) { 132 | rows.push([ 133 | i, 134 | `hello ${i}`, 135 | 3.14, 136 | new Uint8Array([3, 2, 1]), 137 | null, 138 | ]); 139 | } 140 | 141 | insertMany.default(rows); 142 | 143 | stmt.finalize(); 144 | 145 | assertEquals(db.totalChanges, 12); 146 | }); 147 | 148 | await t.step("query array", () => { 149 | const row = db.prepare("select * from test where integer = 0").values< 150 | [number, string, number, Uint8Array, null] 151 | >()[0]; 152 | 153 | assertEquals(row[0], 0); 154 | assertEquals(row[1], "hello 0"); 155 | assertEquals(row[2], 3.14); 156 | assertEquals(row[3], new Uint8Array([3, 2, 1])); 157 | assertEquals(row[4], null); 158 | }); 159 | 160 | await t.step("query object", () => { 161 | const rows = db.prepare( 162 | "select * from test where integer != ? and text != ?", 163 | ) 164 | .all<{ 165 | integer: number; 166 | text: string; 167 | double: number; 168 | blob: Uint8Array; 169 | nullable: null; 170 | }>( 171 | 1, 172 | "hello world", 173 | ); 174 | 175 | assertEquals(rows.length, 9); 176 | for (const row of rows) { 177 | assertEquals(typeof row.integer, "number"); 178 | assertEquals(row.text, `hello ${row.integer}`); 179 | assertEquals(row.double, 3.14); 180 | assertEquals(row.blob, new Uint8Array([3, 2, 1])); 181 | assertEquals(row.nullable, null); 182 | } 183 | }); 184 | 185 | await t.step("query object (interface type)", () => { 186 | interface Row { 187 | integer: number; 188 | text: string; 189 | double: number; 190 | blob: Uint8Array; 191 | nullable: null; 192 | } 193 | 194 | const rows = db.prepare( 195 | "select * from test where integer != ? and text != ?", 196 | ) 197 | .all( 198 | 1, 199 | "hello world", 200 | ); 201 | 202 | assertEquals(rows.length, 9); 203 | for (const row of rows) { 204 | assertEquals(typeof row.integer, "number"); 205 | assertEquals(row.text, `hello ${row.integer}`); 206 | assertEquals(row.double, 3.14); 207 | assertEquals(row.blob, new Uint8Array([3, 2, 1])); 208 | assertEquals(row.nullable, null); 209 | } 210 | }); 211 | 212 | await t.step("query json", () => { 213 | const row = db 214 | .prepare( 215 | "select json('[1,2,3]'), json_object('name', 'alex'), '{\"no_subtype\": true}'", 216 | ) 217 | .values<[number[], { name: string }, string]>()[0]; 218 | 219 | assertEquals(row[0], [1, 2, 3]); 220 | assertEquals(row[1], { name: "alex" }); 221 | assertEquals(row[2], '{"no_subtype": true}'); 222 | }); 223 | 224 | await t.step("query json (parseJson: false)", () => { 225 | db.parseJson = false; 226 | 227 | const row = db 228 | .prepare( 229 | "select json('[1,2,3]'), json_object('name', 'alex'), '{\"no_subtype\": true}'", 230 | ) 231 | .values<[string, string, string]>()[0]; 232 | 233 | db.parseJson = true; 234 | 235 | assertEquals(row[0], "[1,2,3]"); 236 | assertEquals(row[1], '{"name":"alex"}'); 237 | assertEquals(row[2], '{"no_subtype": true}'); 238 | }); 239 | 240 | await t.step("query json (disableParseJson at statement level)", () => { 241 | const row = db 242 | .prepare( 243 | "select json('[1,2,3]'), json_object('name', 'alex'), '{\"no_subtype\": true}'", 244 | ) 245 | .disableParseJson() 246 | .values<[string, string, string]>()[0]; 247 | 248 | assertEquals(row[0], "[1,2,3]"); 249 | assertEquals(row[1], '{"name":"alex"}'); 250 | assertEquals(row[2], '{"no_subtype": true}'); 251 | }); 252 | 253 | await t.step("query with string param", () => { 254 | const row = db.prepare( 255 | "select * from test where text = ?", 256 | ).values<[number, string, number, Uint8Array, null]>("hello 0")[0]; 257 | 258 | assertEquals(row[0], 0); 259 | assertEquals(row[1], "hello 0"); 260 | assertEquals(row[2], 3.14); 261 | assertEquals(row[3], new Uint8Array([3, 2, 1])); 262 | assertEquals(row[4], null); 263 | }); 264 | 265 | await t.step("query with string param (named)", () => { 266 | const row = db.prepare( 267 | "select * from test where text = :p1", 268 | ).values<[number, string, number, Uint8Array, null]>({ p1: "hello 0" })[0]; 269 | 270 | assertEquals(row[0], 0); 271 | assertEquals(row[1], "hello 0"); 272 | assertEquals(row[2], 3.14); 273 | assertEquals(row[3], new Uint8Array([3, 2, 1])); 274 | assertEquals(row[4], null); 275 | }); 276 | await t.step("query parameters", () => { 277 | const row = db.prepare( 278 | "select ?, ?, ?, ?, ?", 279 | ).values<[number, string, string, string, string]>( 280 | 1, 281 | "alex", 282 | new Date("2023-01-01"), 283 | [1, 2, 3], 284 | { name: "alex" }, 285 | )[0]; 286 | 287 | assertEquals(row[0], 1); 288 | assertEquals(row[1], "alex"); 289 | assertEquals(row[2], "2023-01-01T00:00:00.000Z"); 290 | assertEquals(row[3], "[1,2,3]"); 291 | assertEquals(row[4], '{"name":"alex"}'); 292 | }); 293 | 294 | await t.step(".sql tagged template", () => { 295 | assertEquals(db.sql`select 1, 2, 3`, [{ "1": 1, "2": 2, "3": 3 }]); 296 | assertEquals( 297 | db.sql`select ${1} as a, ${Math.PI} as b, ${new Uint8Array([1, 2])} as c`, 298 | [ 299 | { a: 1, b: 3.141592653589793, c: new Uint8Array([1, 2]) }, 300 | ], 301 | ); 302 | 303 | assertEquals(db.sql`select ${"1; DROP TABLE"}`, [{ "?": "1; DROP TABLE" }]); 304 | }); 305 | 306 | await t.step("more than 32-bit int", () => { 307 | const value = 978307200000; 308 | db.exec( 309 | `insert into test (integer, text, double, blob, nullable) 310 | values (?, ?, ?, ?, ?)`, 311 | value, 312 | "bigint", 313 | 0, 314 | new Uint8Array(1), 315 | null, 316 | ); 317 | const [int] = db.prepare( 318 | "select integer from test where text = ?", 319 | ).values<[number]>("bigint")[0]; 320 | assertEquals(int, value); 321 | }); 322 | 323 | await t.step("more than 32-bit signed int", () => { 324 | const value = -978307200000; 325 | db.exec( 326 | `insert into test (integer, text, double, blob, nullable) 327 | values (?, ?, ?, ?, ?)`, 328 | value, 329 | "bigint2", 330 | 0, 331 | new Uint8Array(1), 332 | null, 333 | ); 334 | const [int] = db.prepare( 335 | "select integer from test where text = ?", 336 | ).values<[number]>("bigint2")[0]; 337 | assertEquals(int, value); 338 | }); 339 | 340 | await t.step("max 64-bit signed int", () => { 341 | const value = 0x7fffffffffffffffn; 342 | db.exec( 343 | `insert into test (integer, text, double, blob, nullable) 344 | values (?, ?, ?, ?, ?)`, 345 | value, 346 | "bigint3", 347 | 0, 348 | new Uint8Array(1), 349 | null, 350 | ); 351 | const [int] = db.prepare( 352 | "select integer from test where text = ?", 353 | ).values<[bigint]>("bigint3")[0]; 354 | assertEquals(int, value); 355 | }); 356 | 357 | await t.step("max 64-bit signed int (disableInt64)", () => { 358 | const value = 0x7fffffffffffffffn; 359 | db.exec( 360 | `insert into test (integer, text, double, blob, nullable) 361 | values (?, ?, ?, ?, ?)`, 362 | value, 363 | "bigint3", 364 | 0, 365 | new Uint8Array(1), 366 | null, 367 | ); 368 | const [int] = db.prepare( 369 | "select integer from test where text = ?", 370 | ).disableInt64().values<[bigint]>("bigint3")[0]; 371 | assertEquals(int as unknown, -1); 372 | }); 373 | 374 | await t.step("nan value", () => { 375 | db.exec( 376 | `insert into test (integer, text, double, blob, nullable) 377 | values (?, ?, ?, ?, ?)`, 378 | NaN, 379 | "nan", 380 | NaN, 381 | new Uint8Array(1), 382 | null, 383 | ); 384 | const [int, double] = db.prepare( 385 | "select integer, double from test where text = ?", 386 | ).values<[number, number]>("nan")[0]; 387 | assertEquals(int, null); 388 | assertEquals(double, null); 389 | }); 390 | 391 | await t.step("empty string on not null column", () => { 392 | db.exec(`create table empty_string_not_null ( name text not null )`); 393 | db.exec("insert into empty_string_not_null (name) values (?)", ""); 394 | const s = db.prepare("select * from empty_string_not_null").value< 395 | string[] 396 | >(); 397 | assertEquals(s, [""]); 398 | }); 399 | 400 | await t.step("enable update or delete limit", () => { 401 | db.run(` 402 | create table test_limit ( 403 | id integer primary key autoincrement 404 | ) 405 | `); 406 | 407 | db.run(` 408 | delete from test_limit 409 | order by id 410 | limit 1 411 | `); 412 | 413 | db.run(`drop table test_limit`); 414 | }); 415 | 416 | await t.step("create blob table", () => { 417 | db.exec(` 418 | create table blobs ( 419 | id integer primary key, 420 | data blob 421 | ) 422 | `); 423 | }); 424 | 425 | await t.step("empty blob vs null blob", () => { 426 | db.exec("insert into blobs (id, data) values (?, ?)", 0, new Uint8Array()); 427 | db.exec("insert into blobs (id, data) values (?, ?)", 1, null); 428 | 429 | const [ 430 | [blob1], 431 | [blob2], 432 | ] = db.prepare("select data from blobs").values< 433 | [Uint8Array | null] 434 | >(); 435 | 436 | assertEquals(blob1, new Uint8Array()); 437 | assertEquals(blob2, null); 438 | }); 439 | 440 | await t.step("insert blob", () => { 441 | const blob = new Uint8Array(1024 * 32); 442 | db.exec("insert into blobs (id, data) values (?, ?)", 3, blob); 443 | }); 444 | 445 | await t.step("sql blob", async (t) => { 446 | const blob = db.openBlob({ 447 | table: "blobs", 448 | column: "data", 449 | row: db.lastInsertRowId, 450 | readonly: false, 451 | }); 452 | 453 | await t.step("byte legnth", () => { 454 | assertEquals(blob.byteLength, 1024 * 32); 455 | }); 456 | 457 | await t.step("read from blob", () => { 458 | const data = new Uint8Array(blob.byteLength); 459 | blob.readSync(0, data); 460 | assertEquals(data, new Uint8Array(1024 * 32)); 461 | }); 462 | 463 | await t.step("write to blob", () => { 464 | const data = new Uint8Array(1024 * 32).fill(0x01); 465 | blob.writeSync(0, data); 466 | }); 467 | 468 | await t.step("read from blob (stream)", async () => { 469 | let chunks = 0; 470 | for await (const chunk of blob.readable) { 471 | assertEquals(chunk, new Uint8Array(1024 * 16).fill(0x01)); 472 | chunks++; 473 | } 474 | assertEquals(chunks, 2); 475 | }); 476 | 477 | await t.step("read from blob (iter)", () => { 478 | let chunks = 0; 479 | for (const chunk of blob) { 480 | assertEquals(chunk, new Uint8Array(1024 * 16).fill(0x01)); 481 | chunks++; 482 | } 483 | assertEquals(chunks, 2); 484 | }); 485 | 486 | await t.step("write to blob (stream)", async () => { 487 | const writer = blob.writable.getWriter(); 488 | await writer.write(new Uint8Array(1024 * 16).fill(0x03)); 489 | await writer.write(new Uint8Array(1024 * 16).fill(0x03)); 490 | await writer.close(); 491 | }); 492 | 493 | await t.step("close blob", () => { 494 | blob.close(); 495 | }); 496 | }); 497 | 498 | await t.step({ 499 | name: "define functions", 500 | sanitizeResources: false, 501 | fn(): void { 502 | db.function("deno_add", (a: number, b: number): number => { 503 | return a + b; 504 | }); 505 | 506 | db.function("deno_uppercase", (a: string): string => { 507 | return a.toUpperCase(); 508 | }); 509 | 510 | db.function("deno_buffer_add_1", (a: Uint8Array): Uint8Array => { 511 | const result = new Uint8Array(a.length); 512 | for (let i = 0; i < a.length; i++) { 513 | result[i] = a[i] + 1; 514 | } 515 | return result; 516 | }); 517 | 518 | db.function("regexp", (a: string, b: string): boolean => { 519 | return new RegExp(b).test(a); 520 | }); 521 | 522 | db.aggregate("deno_sum_2x", { 523 | start: 0, 524 | step(sum: number, value: number): number { 525 | return sum + value; 526 | }, 527 | final(sum: number): number { 528 | return sum * 2; 529 | }, 530 | }); 531 | }, 532 | }); 533 | 534 | await t.step("test functions", () => { 535 | const [result] = db 536 | .prepare("select deno_add(?, ?)") 537 | .value<[number]>(1, 2)!; 538 | assertEquals(result, 3); 539 | 540 | const [result2] = db 541 | .prepare("select deno_uppercase(?)") 542 | .value<[string]>("hello")!; 543 | assertEquals(result2, "HELLO"); 544 | 545 | const [result3] = db 546 | .prepare("select deno_buffer_add_1(?)") 547 | .value<[Uint8Array]>(new Uint8Array([1, 2, 3]))!; 548 | assertEquals(result3, new Uint8Array([2, 3, 4])); 549 | 550 | const [result4] = db.prepare("select deno_add(?, ?)").value<[number]>( 551 | 1.5, 552 | 1.5, 553 | )!; 554 | assertEquals(result4, 3); 555 | 556 | const [result5] = db 557 | .prepare("select regexp(?, ?)") 558 | .value<[number]>("hello", "h.*")!; 559 | assertEquals(result5, 1); 560 | 561 | const [result6] = db 562 | .prepare("select regexp(?, ?)") 563 | .value<[number]>("hello", "x.*")!; 564 | assertEquals(result6, 0); 565 | 566 | db.exec("create table aggr_test (value integer)"); 567 | db.exec("insert into aggr_test (value) values (1)"); 568 | db.exec("insert into aggr_test (value) values (2)"); 569 | db.exec("insert into aggr_test (value) values (3)"); 570 | 571 | const stmt = db.prepare("select deno_sum_2x(value) from aggr_test"); 572 | const [result7] = stmt.value<[number]>()!; 573 | assertEquals(result7, 12); 574 | // Releases lock from table. 575 | stmt.finalize(); 576 | 577 | db.exec("drop table aggr_test"); 578 | }); 579 | 580 | await t.step("fts5", () => { 581 | db.exec("create virtual table tbl_fts using fts5(a)"); 582 | db.exec("drop table tbl_fts"); 583 | }); 584 | 585 | await t.step("drop table", () => { 586 | db.exec("drop table test"); 587 | db.exec("drop table blobs"); 588 | }); 589 | 590 | await t.step({ 591 | name: "backup", 592 | fn(): void { 593 | const url = new URL("backup.db", import.meta.url); 594 | const db2 = new Database(url); 595 | db.backup(db2, "main"); 596 | 597 | db2.close(); 598 | 599 | try { 600 | Deno.removeSync(url); 601 | } catch (_) { /* ignore */ } 602 | }, 603 | }); 604 | 605 | await t.step("backup (error)", () => { 606 | // source == destination 607 | assertThrows( 608 | () => db.backup(db, "main"), 609 | Error, 610 | "source and destination must be distinct", 611 | ); 612 | }); 613 | 614 | await t.step("unicode support", () => { 615 | const [value] = db.prepare("select ?").value<[string]>("💩")!; 616 | assertEquals(value, "💩"); 617 | 618 | const [valueInStmt] = db.prepare("select '💩'").value<[string]>()!; 619 | assertEquals(valueInStmt, "💩"); 620 | }); 621 | 622 | await t.step({ 623 | name: "close", 624 | sanitizeResources: false, 625 | fn(): void { 626 | db.close(); 627 | try { 628 | Deno.removeSync(DB_URL); 629 | } catch (_) { /** ignore, already being used */ } 630 | }, 631 | }); 632 | }); 633 | --------------------------------------------------------------------------------