├── .cargo └── config.toml ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── auth.js ├── docs └── api.md ├── examples ├── drizzle │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ ├── schema.sql │ └── tsconfig.json ├── hello │ ├── example.js │ └── package.json ├── offline-writes │ ├── example.js │ └── package.json ├── remote │ ├── example.js │ └── package.json ├── sync │ ├── example.js │ └── package.json └── vector │ ├── package.json │ └── vector.mjs ├── index.js ├── integration-tests ├── package-lock.json ├── package.json └── tests │ ├── async.test.js │ ├── extensions.test.js │ └── sync.test.js ├── package-lock.json ├── package.json ├── perf ├── package.json ├── perf-better-sqlite3.js ├── perf-iterate-better-sqlite3.js ├── perf-iterate-libsql.js └── perf-libsql.js ├── promise.js ├── rust-toolchain.toml ├── sqlite-error.js ├── src ├── auth.rs ├── database.rs ├── errors.rs ├── lib.rs └── statement.rs ├── tsconfig.json └── types ├── auth.d.ts ├── auth.d.ts.map ├── index.d.ts ├── promise.d.ts ├── promise.d.ts.map ├── sqlite-error.d.ts └── sqlite-error.d.ts.map /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-musl] 2 | rustflags = ["-C", "target-feature=-crt-static"] 3 | [target.aarch64-unknown-linux-musl] 4 | rustflags = ["-C", "target-feature=-crt-static"] 5 | [target.arm-unknown-linux-musleabihf] 6 | rustflags = ["-C", "target-feature=-crt-static"] 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | env: 4 | NODE_VERSION: 18.x 5 | NPM_REGISTRY: 'https://registry.npmjs.org' 6 | RUST_VERSION: 1.78 7 | 8 | on: 9 | push: 10 | # Prevent duplicate runs of this workflow on our own internal PRs. 11 | branches: 12 | - main 13 | pull_request: 14 | types: [opened, synchronize, reopened, labeled] 15 | branches: 16 | - main 17 | 18 | jobs: 19 | test: 20 | name: Integration tests 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ env.NODE_VERSION }} 28 | rust-version: ${{ env.RUST_VERSION }} 29 | - run: npm ci 30 | - run: npm run build 31 | - run: cd integration-tests && npm ci && npm run test 32 | 33 | pack: 34 | name: Pack (main) 35 | runs-on: ubuntu-latest 36 | permissions: 37 | contents: write 38 | steps: 39 | - name: Pack 40 | uses: neon-actions/pack@v0.1 41 | with: 42 | node-version: ${{ env.NODE_VERSION }} 43 | rust-version: ${{ env.RUST_VERSION }} 44 | github-release: false 45 | 46 | macos-arm64-build: 47 | name: Builds (macOS arm64) 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | target: [aarch64-apple-darwin] 52 | runs-on: macos-13-xlarge 53 | permissions: 54 | contents: write 55 | steps: 56 | - name: Build 57 | uses: neon-actions/build@v0.1 58 | with: 59 | target: ${{ matrix.target }} 60 | node-version: ${{ env.NODE_VERSION }} 61 | rust-version: ${{ env.RUST_VERSION }} 62 | npm-publish: false 63 | github-release: false 64 | 65 | macos-x64-build: 66 | name: Builds (macOS x64) 67 | strategy: 68 | fail-fast: false 69 | matrix: 70 | target: [x86_64-apple-darwin] 71 | runs-on: macos-13 72 | permissions: 73 | contents: write 74 | steps: 75 | - name: Build 76 | uses: neon-actions/build@v0.1 77 | with: 78 | target: ${{ matrix.target }} 79 | node-version: ${{ env.NODE_VERSION }} 80 | rust-version: ${{ env.RUST_VERSION }} 81 | npm-publish: false 82 | github-release: false 83 | 84 | windows-builds: 85 | name: Builds (Windows) 86 | strategy: 87 | fail-fast: false 88 | matrix: 89 | target: [x86_64-pc-windows-msvc] 90 | runs-on: windows-latest 91 | permissions: 92 | contents: write 93 | steps: 94 | - name: Add msbuild to PATH 95 | uses: microsoft/setup-msbuild@v2 96 | - name: Build 97 | uses: neon-actions/build@v0.1 98 | with: 99 | target: ${{ matrix.target }} 100 | node-version: ${{ env.NODE_VERSION }} 101 | rust-version: ${{ env.RUST_VERSION }} 102 | npm-publish: false 103 | github-release: false 104 | 105 | other-builds: 106 | name: Builds (other platforms) 107 | strategy: 108 | fail-fast: false 109 | matrix: 110 | target: [x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl, aarch64-unknown-linux-gnu, aarch64-unknown-linux-musl, arm-unknown-linux-gnueabihf, arm-unknown-linux-musleabihf] 111 | runs-on: ubuntu-latest 112 | permissions: 113 | contents: write 114 | steps: 115 | - name: Setup cmake 116 | uses: jwlawson/actions-setup-cmake@v1.14 117 | with: 118 | cmake-version: '3.18.x' 119 | - name: Build 120 | uses: neon-actions/build@v0.1 121 | with: 122 | target: ${{ matrix.target }} 123 | node-version: ${{ env.NODE_VERSION }} 124 | rust-version: ${{ env.RUST_VERSION }} 125 | use-cross: true 126 | npm-publish: false 127 | github-release: false 128 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | env: 4 | NODE_VERSION: 18.x 5 | NPM_REGISTRY: 'https://registry.npmjs.org' 6 | RUST_VERSION: 1.78 7 | 8 | on: 9 | push: 10 | tags: 11 | - v* 12 | 13 | jobs: 14 | pack: 15 | name: Pack (main) 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | steps: 20 | - name: Pack 21 | uses: neon-actions/pack@v0.1 22 | with: 23 | node-version: ${{ env.NODE_VERSION }} 24 | rust-version: ${{ env.RUST_VERSION }} 25 | github-release: true 26 | 27 | macos-arm64-build: 28 | name: Builds (macOS arm64) 29 | strategy: 30 | matrix: 31 | target: [aarch64-apple-darwin] 32 | runs-on: macos-13-xlarge 33 | permissions: 34 | contents: write 35 | steps: 36 | - name: Build 37 | uses: neon-actions/build@v0.1 38 | with: 39 | target: ${{ matrix.target }} 40 | node-version: ${{ env.NODE_VERSION }} 41 | rust-version: ${{ env.RUST_VERSION }} 42 | npm-publish: false 43 | github-release: true 44 | 45 | macos-x64-build: 46 | name: Builds (macOS x64) 47 | strategy: 48 | matrix: 49 | target: [x86_64-apple-darwin] 50 | runs-on: macos-13 51 | permissions: 52 | contents: write 53 | steps: 54 | - name: Build 55 | uses: neon-actions/build@v0.1 56 | with: 57 | target: ${{ matrix.target }} 58 | node-version: ${{ env.NODE_VERSION }} 59 | rust-version: ${{ env.RUST_VERSION }} 60 | npm-publish: false 61 | github-release: true 62 | 63 | windows-builds: 64 | name: Builds (Windows) 65 | strategy: 66 | matrix: 67 | target: [x86_64-pc-windows-msvc] 68 | runs-on: windows-latest 69 | permissions: 70 | contents: write 71 | steps: 72 | - name: Add msbuild to PATH 73 | uses: microsoft/setup-msbuild@v2 74 | - name: Build 75 | uses: neon-actions/build@v0.1 76 | with: 77 | target: ${{ matrix.target }} 78 | node-version: ${{ env.NODE_VERSION }} 79 | rust-version: ${{ env.RUST_VERSION }} 80 | npm-publish: false 81 | github-release: true 82 | 83 | other-builds: 84 | name: Builds (other platforms) 85 | strategy: 86 | matrix: 87 | target: [x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl, aarch64-unknown-linux-gnu, aarch64-unknown-linux-musl, arm-unknown-linux-gnueabihf, arm-unknown-linux-musleabihf] 88 | runs-on: ubuntu-latest 89 | permissions: 90 | contents: write 91 | steps: 92 | - name: Setup cmake 93 | uses: jwlawson/actions-setup-cmake@v1.14 94 | with: 95 | cmake-version: '3.18.x' 96 | - name: Build 97 | uses: neon-actions/build@v0.1 98 | with: 99 | target: ${{ matrix.target }} 100 | node-version: ${{ env.NODE_VERSION }} 101 | rust-version: ${{ env.RUST_VERSION }} 102 | use-cross: true 103 | npm-publish: false 104 | github-release: true 105 | 106 | publish: 107 | name: Publish 108 | needs: [pack, macos-arm64-build, macos-x64-build, windows-builds, other-builds] 109 | runs-on: ubuntu-latest 110 | permissions: 111 | contents: write 112 | steps: 113 | - name: Publish 114 | uses: neon-actions/publish@v0.1 115 | env: 116 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 117 | with: 118 | node-version: ${{ env.NODE_VERSION }} 119 | rust-version: ${{ env.RUST_VERSION }} 120 | registry-url: ${{ env.NPM_REGISTRY }} 121 | github-release: "*.tgz" 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.node 3 | **/node_modules 4 | dist 5 | integration-tests/*.db 6 | examples/**/package-lock.json 7 | *.db 8 | **/client_wal_index 9 | bun.lockb 10 | 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | Cargo.lock 3 | Cargo.toml 4 | release 5 | src 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libsql-js" 3 | version = "0.5.12" 4 | description = "" 5 | authors = ["Pekka Enberg "] 6 | license = "MIT" 7 | edition = "2021" 8 | exclude = ["index.node"] 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | [dependencies] 14 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 15 | libsql = { version = "0.9.9", features = ["encryption"] } 16 | tracing = "0.1" 17 | once_cell = "1.18.0" 18 | tokio = { version = "1.29.1", features = [ "rt-multi-thread" ] } 19 | neon = { version = "1.0.0", default-features = false, features = ["napi-6"] } 20 | 21 | [profile.release] 22 | lto = true 23 | codegen-units = 1 24 | debug = false 25 | strip = true 26 | panic = "abort" 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Joshua Wise 4 | Copyright (c) 2023 Pekka Enberg 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libSQL API for JavaScript/TypeScript 2 | 3 | [![npm](https://badge.fury.io/js/libsql.svg)](https://badge.fury.io/js/libsql) 4 | [![Ask AI](https://img.shields.io/badge/Phorm-Ask_AI-%23F2777A.svg?&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNSIgaGVpZ2h0PSI0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxwYXRoIGQ9Ik00LjQzIDEuODgyYTEuNDQgMS40NCAwIDAgMS0uMDk4LjQyNmMtLjA1LjEyMy0uMTE1LjIzLS4xOTIuMzIyLS4wNzUuMDktLjE2LjE2NS0uMjU1LjIyNmExLjM1MyAxLjM1MyAwIDAgMS0uNTk1LjIxMmMtLjA5OS4wMTItLjE5Mi4wMTQtLjI3OS4wMDZsLTEuNTkzLS4xNHYtLjQwNmgxLjY1OGMuMDkuMDAxLjE3LS4xNjkuMjQ2LS4xOTFhLjYwMy42MDMgMCAwIDAgLjItLjEwNi41MjkuNTI5IDAgMCAwIC4xMzgtLjE3LjY1NC42NTQgMCAwIDAgLjA2NS0uMjRsLjAyOC0uMzJhLjkzLjkzIDAgMCAwLS4wMzYtLjI0OS41NjcuNTY3IDAgMCAwLS4xMDMtLjIuNTAyLjUwMiAwIDAgMC0uMTY4LS4xMzguNjA4LjYwOCAwIDAgMC0uMjQtLjA2N0wyLjQzNy43MjkgMS42MjUuNjcxYS4zMjIuMzIyIDAgMCAwLS4yMzIuMDU4LjM3NS4zNzUgMCAwIDAtLjExNi4yMzJsLS4xMTYgMS40NS0uMDU4LjY5Ny0uMDU4Ljc1NEwuNzA1IDRsLS4zNTctLjA3OUwuNjAyLjkwNkMuNjE3LjcyNi42NjMuNTc0LjczOS40NTRhLjk1OC45NTggMCAwIDEgLjI3NC0uMjg1Ljk3MS45NzEgMCAwIDEgLjMzNy0uMTRjLjExOS0uMDI2LjIyNy0uMDM0LjMyNS0uMDI2TDMuMjMyLjE2Yy4xNTkuMDE0LjMzNi4wMy40NTkuMDgyYTEuMTczIDEuMTczIDAgMCAxIC41NDUuNDQ3Yy4wNi4wOTQuMTA5LjE5Mi4xNDQuMjkzYTEuMzkyIDEuMzkyIDAgMCAxIC4wNzguNThsLS4wMjkuMzJaIiBmaWxsPSIjRjI3NzdBIi8+CiAgPHBhdGggZD0iTTQuMDgyIDIuMDA3YTEuNDU1IDEuNDU1IDAgMCAxLS4wOTguNDI3Yy0uMDUuMTI0LS4xMTQuMjMyLS4xOTIuMzI0YTEuMTMgMS4xMyAwIDAgMS0uMjU0LjIyNyAxLjM1MyAxLjM1MyAwIDAgMS0uNTk1LjIxNGMtLjEuMDEyLS4xOTMuMDE0LS4yOC4wMDZsLTEuNTYtLjEwOC4wMzQtLjQwNi4wMy0uMzQ4IDEuNTU5LjE1NGMuMDkgMCAuMTczLS4wMS4yNDgtLjAzM2EuNjAzLjYwMyAwIDAgMCAuMi0uMTA2LjUzMi41MzIgMCAwIDAgLjEzOS0uMTcyLjY2LjY2IDAgMCAwIC4wNjQtLjI0MWwuMDI5LS4zMjFhLjk0Ljk0IDAgMCAwLS4wMzYtLjI1LjU3LjU3IDAgMCAwLS4xMDMtLjIwMi41MDIuNTAyIDAgMCAwLS4xNjgtLjEzOC42MDUuNjA1IDAgMCAwLS4yNC0uMDY3TDEuMjczLjgyN2MtLjA5NC0uMDA4LS4xNjguMDEtLjIyMS4wNTUtLjA1My4wNDUtLjA4NC4xMTQtLjA5Mi4yMDZMLjcwNSA0IDAgMy45MzhsLjI1NS0yLjkxMUExLjAxIDEuMDEgMCAwIDEgLjM5My41NzIuOTYyLjk2MiAwIDAgMSAuNjY2LjI4NmEuOTcuOTcgMCAwIDEgLjMzOC0uMTRDMS4xMjIuMTIgMS4yMy4xMSAxLjMyOC4xMTlsMS41OTMuMTRjLjE2LjAxNC4zLjA0Ny40MjMuMWExLjE3IDEuMTcgMCAwIDEgLjU0NS40NDhjLjA2MS4wOTUuMTA5LjE5My4xNDQuMjk1YTEuNDA2IDEuNDA2IDAgMCAxIC4wNzcuNTgzbC0uMDI4LjMyMloiIGZpbGw9IndoaXRlIi8+CiAgPHBhdGggZD0iTTQuMDgyIDIuMDA3YTEuNDU1IDEuNDU1IDAgMCAxLS4wOTguNDI3Yy0uMDUuMTI0LS4xMTQuMjMyLS4xOTIuMzI0YTEuMTMgMS4xMyAwIDAgMS0uMjU0LjIyNyAxLjM1MyAxLjM1MyAwIDAgMS0uNTk1LjIxNGMtLjEuMDEyLS4xOTMuMDE0LS4yOC4wMDZsLTEuNTYtLjEwOC4wMzQtLjQwNi4wMy0uMzQ4IDEuNTU5LjE1NGMuMDkgMCAuMTczLS4wMS4yNDgtLjAzM2EuNjAzLjYwMyAwIDAgMCAuMi0uMTA2LjUzMi41MzIgMCAwIDAgLjEzOS0uMTcyLjY2LjY2IDAgMCAwIC4wNjQtLjI0MWwuMDI5LS4zMjFhLjk0Ljk0IDAgMCAwLS4wMzYtLjI1LjU3LjU3IDAgMCAwLS4xMDMtLjIwMi41MDIuNTAyIDAgMCAwLS4xNjgtLjEzOC42MDUuNjA1IDAgMCAwLS4yNC0uMDY3TDEuMjczLjgyN2MtLjA5NC0uMDA4LS4xNjguMDEtLjIyMS4wNTUtLjA1My4wNDUtLjA4NC4xMTQtLjA5Mi4yMDZMLjcwNSA0IDAgMy45MzhsLjI1NS0yLjkxMUExLjAxIDEuMDEgMCAwIDEgLjM5My41NzIuOTYyLjk2MiAwIDAgMSAuNjY2LjI4NmEuOTcuOTcgMCAwIDEgLjMzOC0uMTRDMS4xMjIuMTIgMS4yMy4xMSAxLjMyOC4xMTlsMS41OTMuMTRjLjE2LjAxNC4zLjA0Ny40MjMuMWExLjE3IDEuMTcgMCAwIDEgLjU0NS40NDhjLjA2MS4wOTUuMTA5LjE5My4xNDQuMjk1YTEuNDA2IDEuNDA2IDAgMCAxIC4wNzcuNTgzbC0uMDI4LjMyMloiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=)](https://www.phorm.ai/query?projectId=3c9a471f-4a47-469f-81f6-4ea1ff9ab418) 5 | 6 | [libSQL](https://github.com/libsql/libsql) is an open source, open contribution fork of SQLite. 7 | This source repository contains libSQL API bindings for Node, which aims to be compatible with [better-sqlite3](https://github.com/WiseLibs/better-sqlite3/), but with opt-in promise API. 8 | 9 | *Please note that there is also the [libSQL SDK](https://github.com/libsql/libsql-client-ts), which is useful if you don't need `better-sqlite3` compatibility or use libSQL in environments like serverless functions that require `fetch()`-based database access protocol.* 10 | 11 | ## Features 12 | 13 | * In-memory and local libSQL/SQLite databases 14 | * Remote libSQL databases 15 | * Embedded, in-app replica that syncs with a remote libSQL database 16 | * Supports Bun, Deno, and Node on macOS, Linux, and Windows. 17 | 18 | ## Installing 19 | 20 | You can install the package with: 21 | 22 | **Node:** 23 | 24 | ```sh 25 | npm i libsql 26 | ``` 27 | 28 | **Bun:** 29 | 30 | ```sh 31 | bun add libsql 32 | ``` 33 | 34 | **Deno:** 35 | 36 | Use the `npm:` prefix for package import: 37 | 38 | ```typescript 39 | import Database from 'npm:libsql'; 40 | ``` 41 | 42 | ## Documentation 43 | 44 | * [API reference](docs/api.md) 45 | 46 | ## Getting Started 47 | 48 | To try out your first libsql program, type the following in `hello.js`: 49 | 50 | ```javascript 51 | import Database from 'libsql'; 52 | 53 | const db = new Database(':memory:'); 54 | 55 | db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"); 56 | db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); 57 | 58 | const row = db.prepare("SELECT * FROM users WHERE id = ?").get(1); 59 | 60 | console.log(`Name: ${row.name}, email: ${row.email}`); 61 | ``` 62 | 63 | and then run: 64 | 65 | ```shell 66 | $ node hello.js 67 | ``` 68 | 69 | To use the promise API, import `libsql/promise`: 70 | 71 | ```javascript 72 | import Database from 'libsql/promise'; 73 | 74 | const db = new Database(':memory:'); 75 | 76 | await db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"); 77 | await db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); 78 | 79 | const stmt = await db.prepare("SELECT * FROM users WHERE id = ?"); 80 | const row = stmt.get(1); 81 | 82 | console.log(`Name: ${row.name}, email: ${row.email}`); 83 | ``` 84 | 85 | #### Connecting to a local database file 86 | 87 | ```javascript 88 | import Database from 'libsql'; 89 | 90 | const db = new Database('hello.db'); 91 | ```` 92 | 93 | #### Connecting to a Remote libSQL server 94 | 95 | ```javascript 96 | import Database from 'libsql'; 97 | 98 | const url = process.env.LIBSQL_URL; 99 | const authToken = process.env.LIBSQL_AUTH_TOKEN; 100 | 101 | const opts = { 102 | authToken: authToken, 103 | }; 104 | 105 | const db = new Database(url, opts); 106 | ``` 107 | 108 | #### Creating an in-app replica and syncing it 109 | 110 | ```javascript 111 | import libsql 112 | 113 | const opts = { syncUrl: "", authToken: "" }; 114 | const db = new Database('hello.db', opts); 115 | db.sync(); 116 | ``` 117 | 118 | #### Creating a table 119 | 120 | ```javascript 121 | db.exec("CREATE TABLE users (id INTEGER, email TEXT);") 122 | ``` 123 | 124 | #### Inserting rows into a table 125 | 126 | ```javascript 127 | db.exec("INSERT INTO users VALUES (1, 'alice@example.org')") 128 | ``` 129 | 130 | #### Querying rows from a table 131 | 132 | ```javascript 133 | const row = db.prepare("SELECT * FROM users WHERE id = ?").get(1); 134 | ``` 135 | 136 | ## Developing 137 | 138 | To build the `libsql` package, run: 139 | 140 | ```console 141 | LIBSQL_JS_DEV=1 npm run build 142 | ``` 143 | 144 | You can then run the integration tests with: 145 | 146 | ```console 147 | export LIBSQL_JS_DEV=1 148 | npm link 149 | cd integration-tests 150 | npm link libsql 151 | npm test 152 | ``` 153 | 154 | ## License 155 | 156 | This project is licensed under the [MIT license]. 157 | 158 | ### Contribution 159 | 160 | Unless you explicitly state otherwise, any contribution intentionally submitted 161 | for inclusion in libSQL by you, shall be licensed as MIT, without any additional 162 | terms or conditions. 163 | 164 | [MIT license]: https://github.com/libsql/libsql-node/blob/main/LICENSE 165 | -------------------------------------------------------------------------------- /auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Authorization outcome. 3 | * 4 | * @readonly 5 | * @enum {number} 6 | * @property {number} ALLOW - Allow access to a resource. 7 | * @property {number} DENY - Deny access to a resource and throw an error. 8 | */ 9 | const Authorization = { 10 | /** 11 | * Allow access to a resource. 12 | * @type {number} 13 | */ 14 | ALLOW: 0, 15 | 16 | /** 17 | * Deny access to a resource and throw an error in `prepare()`. 18 | * @type {number} 19 | */ 20 | DENY: 1, 21 | }; 22 | module.exports = Authorization; 23 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # class Database 2 | 3 | The `Database` class represents a connection that can prepare and execute SQL statements. 4 | 5 | ## Methods 6 | 7 | ### new Database(path, [options]) ⇒ Database 8 | 9 | Creates a new database connection. 10 | 11 | | Param | Type | Description | 12 | | ------- | ------------------- | ------------------------- | 13 | | path | string | Path to the database file | 14 | | options | object | Options. | 15 | 16 | The `path` parameter points to the SQLite database file to open. If the file pointed to by `path` does not exists, it will be created. 17 | To open an in-memory database, please pass `:memory:` as the `path` parameter. 18 | 19 | You can use the `options` parameter to specify various options. Options supported by the parameter are: 20 | 21 | - `syncUrl`: open the database as embedded replica synchronizing from the provided URL. 22 | - `syncPeriod`: synchronize the database periodically every `syncPeriod` seconds. 23 | - `authToken`: authentication token for the provider URL (optional). 24 | - `timeout`: number of milliseconds to wait on locked database before returning `SQLITE_BUSY` error 25 | 26 | The function returns a `Database` object. 27 | 28 | ### prepare(sql) ⇒ Statement 29 | 30 | Prepares a SQL statement for execution. 31 | 32 | | Param | Type | Description | 33 | | ------ | ------------------- | ------------------------------------ | 34 | | sql | string | The SQL statement string to prepare. | 35 | 36 | The function returns a `Statement` object. 37 | 38 | ### transaction(function) ⇒ function 39 | 40 | Returns a function that runs the given function in a transaction. 41 | 42 | | Param | Type | Description | 43 | | -------- | --------------------- | ------------------------------------- | 44 | | function | function | The function to run in a transaction. | 45 | 46 | ### pragma(string, [options]) ⇒ results 47 | 48 | This function is currently not supported. 49 | 50 | ### backup(destination, [options]) ⇒ promise 51 | 52 | This function is currently not supported. 53 | 54 | ### serialize([options]) ⇒ Buffer 55 | 56 | This function is currently not supported. 57 | 58 | ### function(name, [options], function) ⇒ this 59 | 60 | This function is currently not supported. 61 | 62 | ### aggregate(name, options) ⇒ this 63 | 64 | This function is currently not supported. 65 | 66 | ### table(name, definition) ⇒ this 67 | 68 | This function is currently not supported. 69 | 70 | ### authorizer(rules) ⇒ this 71 | 72 | Configure authorization rules. The `rules` object is a map from table name to 73 | `Authorization` object, which defines if access to table is allowed or denied. 74 | If a table has no authorization rule, access to it is _denied_ by default. 75 | 76 | Example: 77 | 78 | ```javascript 79 | db.authorizer({ 80 | "users": Authorization.ALLOW 81 | }); 82 | 83 | // Access is allowed. 84 | const stmt = db.prepare("SELECT * FROM users"); 85 | 86 | db.authorizer({ 87 | "users": Authorization.DENY 88 | }); 89 | 90 | // Access is denied. 91 | const stmt = db.prepare("SELECT * FROM users"); 92 | ``` 93 | 94 | **Note: This is an experimental API and, therefore, subject to change.** 95 | 96 | ### loadExtension(path, [entryPoint]) ⇒ this 97 | 98 | Loads a SQLite3 extension 99 | 100 | ### exec(sql) ⇒ this 101 | 102 | Executes a SQL statement. 103 | 104 | | Param | Type | Description | 105 | | ------ | ------------------- | ------------------------------------ | 106 | | sql | string | The SQL statement string to execute. | 107 | 108 | ### interrupt() ⇒ this 109 | 110 | Cancel ongoing operations and make them return at earliest opportunity. 111 | 112 | **Note:** This is an extension in libSQL and not available in `better-sqlite3`. 113 | 114 | ### close() ⇒ this 115 | 116 | Closes the database connection. 117 | 118 | # class Statement 119 | 120 | ## Methods 121 | 122 | ### run([...bindParameters]) ⇒ object 123 | 124 | Executes the SQL statement and returns an info object. 125 | 126 | | Param | Type | Description | 127 | | -------------- | ----------------------------- | ------------------------------------------------ | 128 | | bindParameters | array of objects | The bind parameters for executing the statement. | 129 | 130 | The returned info object contains two properties: `changes` that describes the number of modified rows and `info.lastInsertRowid` that represents the `rowid` of the last inserted row. 131 | 132 | ### get([...bindParameters]) ⇒ row 133 | 134 | Executes the SQL statement and returns the first row. 135 | 136 | | Param | Type | Description | 137 | | -------------- | ----------------------------- | ------------------------------------------------ | 138 | | bindParameters | array of objects | The bind parameters for executing the statement. | 139 | 140 | ### all([...bindParameters]) ⇒ array of rows 141 | 142 | Executes the SQL statement and returns an array of the resulting rows. 143 | 144 | | Param | Type | Description | 145 | | -------------- | ----------------------------- | ------------------------------------------------ | 146 | | bindParameters | array of objects | The bind parameters for executing the statement. | 147 | 148 | ### iterate([...bindParameters]) ⇒ iterator 149 | 150 | Executes the SQL statement and returns an iterator to the resulting rows. 151 | 152 | | Param | Type | Description | 153 | | -------------- | ----------------------------- | ------------------------------------------------ | 154 | | bindParameters | array of objects | The bind parameters for executing the statement. | 155 | 156 | ### pluck([toggleState]) ⇒ this 157 | 158 | This function is currently not supported. 159 | 160 | ### expand([toggleState]) ⇒ this 161 | 162 | This function is currently not supported. 163 | 164 | ### raw([rawMode]) ⇒ this 165 | 166 | Toggle raw mode. 167 | 168 | | Param | Type | Description | 169 | | ------- | -------------------- | --------------------------------------------------------------------------------- | 170 | | rawMode | boolean | Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled. | 171 | 172 | This function enables or disables raw mode. Prepared statements return objects by default, but if raw mode is enabled, the functions return arrays instead. 173 | 174 | ### columns() ⇒ array of objects 175 | 176 | Returns the columns in the result set returned by this prepared statement. 177 | 178 | ### bind([...bindParameters]) ⇒ this 179 | 180 | This function is currently not supported. 181 | -------------------------------------------------------------------------------- /examples/drizzle/index.ts: -------------------------------------------------------------------------------- 1 | import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; 2 | import { drizzle } from 'drizzle-orm/better-sqlite3'; 3 | import { migrate } from "drizzle-orm/better-sqlite3/migrator"; 4 | import Database from 'libsql'; 5 | 6 | const users = sqliteTable('users', { 7 | id: integer('id').primaryKey(), // 'id' is the column name 8 | fullName: text('full_name'), 9 | }) 10 | 11 | const sqlite = new Database('drizzle.db'); 12 | 13 | const db = drizzle(sqlite); 14 | 15 | const allUsers = db.select().from(users).all(); 16 | 17 | console.log(allUsers); 18 | -------------------------------------------------------------------------------- /examples/drizzle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "libsql": "^0.1.0", 4 | "drizzle-orm": "^0.27.2" 5 | }, 6 | "devDependencies": { 7 | "ts-node": "^10.9.1", 8 | "typescript": "^5.1.6" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/drizzle/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id INT PRIMARY KEY, 3 | full_name TEXT 4 | ); 5 | 6 | INSERT INTO users VALUES (1, 'Andrew Sherman'); 7 | INSERT INTO users VALUES (2, 'Dan'); 8 | INSERT INTO users VALUES (3, 'Alex Blokh'); 9 | -------------------------------------------------------------------------------- /examples/drizzle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /examples/hello/example.js: -------------------------------------------------------------------------------- 1 | import Database from "libsql"; 2 | 3 | const path = process.env.DATABASE ?? ":memory:"; 4 | 5 | const opts = { 6 | encryptionCipher: process.env.ENCRYPTION_CIPHER, 7 | encryptionKey: process.env.ENCRYPTION_KEY, 8 | }; 9 | 10 | const db = new Database(path, opts); 11 | 12 | db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"); 13 | db.exec( 14 | "INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')" 15 | ); 16 | 17 | const row = db.prepare("SELECT * FROM users WHERE id = ?").get(1); 18 | 19 | console.log(`Name: ${row.name}, email: ${row.email}`); 20 | -------------------------------------------------------------------------------- /examples/hello/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libsql-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "libsql": "^0.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/offline-writes/example.js: -------------------------------------------------------------------------------- 1 | import Database from "libsql"; 2 | import reader from "readline-sync"; 3 | 4 | const dbPath = process.env.LIBSQL_DB_PATH; 5 | if (!dbPath) { 6 | throw new Error("Environment variable LIBSQL_DB_PATH is not set."); 7 | } 8 | const syncUrl = process.env.LIBSQL_SYNC_URL; 9 | if (!syncUrl) { 10 | throw new Error("Environment variable LIBSQL_SYNC_URL is not set."); 11 | } 12 | const authToken = process.env.LIBSQL_AUTH_TOKEN; 13 | 14 | const options = { syncUrl: syncUrl, authToken: authToken, offline: true }; 15 | const db = new Database(dbPath, options); 16 | 17 | db.exec("CREATE TABLE IF NOT EXISTS guest_book_entries (text TEXT)"); 18 | 19 | const comment = reader.question("Enter your comment: "); 20 | 21 | console.log(comment); 22 | 23 | const stmt = db.prepare("INSERT INTO guest_book_entries (text) VALUES (?)"); 24 | stmt.run(comment); 25 | console.log("max write replication index: " + db.maxWriteReplicationIndex()); 26 | 27 | const replicated = db.sync(); 28 | console.log("frames synced: " + replicated.frames_synced); 29 | console.log("frame no: " + replicated.frame_no); 30 | 31 | console.log("Guest book entries:"); 32 | const rows = db.prepare("SELECT * FROM guest_book_entries").all(); 33 | for (const row of rows) { 34 | console.log(" - " + row.text); 35 | } 36 | -------------------------------------------------------------------------------- /examples/offline-writes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libsql-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "libsql": "../..", 15 | "readline-sync": "^1.4.10" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/remote/example.js: -------------------------------------------------------------------------------- 1 | import Database from "libsql"; 2 | 3 | const url = process.env.LIBSQL_URL; 4 | const authToken = process.env.LIBSQL_AUTH_TOKEN; 5 | 6 | const opts = { 7 | authToken: authToken, 8 | }; 9 | 10 | const db = new Database(url, opts); 11 | 12 | db.exec("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, name TEXT, email TEXT)"); 13 | db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); 14 | db.exec("INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@example.com')"); 15 | 16 | const row = db.prepare("SELECT * FROM users WHERE id = ?").get(1); 17 | 18 | console.log(`Name: ${row.name}, email: ${row.email}`); 19 | -------------------------------------------------------------------------------- /examples/remote/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libsql-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "libsql": "^0.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/sync/example.js: -------------------------------------------------------------------------------- 1 | import Database from "libsql"; 2 | import reader from "readline-sync"; 3 | 4 | const url = process.env.LIBSQL_URL; 5 | if (!url) { 6 | throw new Error("Environment variable LIBSQL_URL is not set."); 7 | } 8 | const authToken = process.env.LIBSQL_AUTH_TOKEN; 9 | 10 | const options = { syncUrl: url, authToken: authToken }; 11 | const db = new Database("hello.db", options); 12 | 13 | db.sync(); 14 | 15 | db.exec("CREATE TABLE IF NOT EXISTS guest_book_entries (comment TEXT)"); 16 | 17 | db.sync(); 18 | 19 | const comment = reader.question("Enter your comment: "); 20 | 21 | console.log(comment); 22 | 23 | const stmt = db.prepare("INSERT INTO guest_book_entries (comment) VALUES (?)"); 24 | stmt.run(comment); 25 | console.log("max write replication index: " + db.maxWriteReplicationIndex()); 26 | 27 | const replicated = db.sync(); 28 | console.log("frames synced: " + replicated.frames_synced); 29 | console.log("frame no: " + replicated.frame_no); 30 | 31 | console.log("Guest book entries:"); 32 | const rows = db.prepare("SELECT * FROM guest_book_entries").all(); 33 | for (const row of rows) { 34 | console.log(" - " + row.comment); 35 | } 36 | -------------------------------------------------------------------------------- /examples/sync/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libsql-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "libsql": "^0.1.0", 15 | "readline-sync": "^1.4.10" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/vector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libsql-examples-vector", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@xenova/transformers": "^2.17.1", 13 | "csv-parse": "^5.5.5", 14 | "libsql": "../../" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/vector/vector.mjs: -------------------------------------------------------------------------------- 1 | import { pipeline } from "@xenova/transformers"; 2 | import { createReadStream } from "fs"; 3 | import { parse } from "csv-parse"; 4 | import Database from "libsql"; 5 | 6 | // Create a embeddings generator. 7 | const extractor = await pipeline( 8 | "feature-extraction", 9 | "Xenova/jina-embeddings-v2-small-en", 10 | { quantized: false }, 11 | ); 12 | 13 | // Open a database file. 14 | const db = new Database("movies.db"); 15 | 16 | // Create a table for movies with an embedding as a column. 17 | db.exec("CREATE TABLE movies (title TEXT, year INT, embedding VECTOR(512))"); 18 | 19 | // Create a vector index on the embedding column. 20 | db.exec("CREATE INDEX movies_idx USING vector ON movies (embedding)"); 21 | 22 | // Prepare a SQL `INSERT` statement. 23 | const stmt = db.prepare( 24 | "INSERT INTO movies (title, year, embedding) VALUES (?, ?, vector(?))", 25 | ); 26 | 27 | // Process a CSV file of movies generating embeddings for plot synopsis. 28 | createReadStream("wiki_movie_plots_deduped.csv") 29 | .pipe(parse({ columns: true })) 30 | .on("data", async (data) => { 31 | const title = data.Title; 32 | const year = data.Year; 33 | const plot = data.Plot; 34 | const output = await extractor([plot], { pooling: "mean" }); 35 | const embedding = output[0].data; 36 | stmt.run([title, year, embedding]); 37 | }); 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { load, currentTarget } = require("@neon-rs/load"); 4 | const { familySync, GLIBC, MUSL } = require("detect-libc"); 5 | 6 | function requireNative() { 7 | if (process.env.LIBSQL_JS_DEV) { 8 | return load(__dirname) 9 | } 10 | let target = currentTarget(); 11 | // Workaround for Bun, which reports a musl target, but really wants glibc... 12 | if (familySync() == GLIBC) { 13 | switch (target) { 14 | case "linux-x64-musl": 15 | target = "linux-x64-gnu"; 16 | break; 17 | case "linux-arm64-musl": 18 | target = "linux-arm64-gnu"; 19 | break; 20 | } 21 | } 22 | // @neon-rs/load doesn't detect arm musl 23 | if (target === "linux-arm-gnueabihf" && familySync() == MUSL) { 24 | target = "linux-arm-musleabihf"; 25 | } 26 | return require(`@libsql/${target}`); 27 | } 28 | 29 | const { 30 | databaseOpen, 31 | databaseOpenWithSync, 32 | databaseInTransaction, 33 | databaseInterrupt, 34 | databaseClose, 35 | databaseSyncSync, 36 | databaseSyncUntilSync, 37 | databaseExecSync, 38 | databasePrepareSync, 39 | databaseDefaultSafeIntegers, 40 | databaseAuthorizer, 41 | databaseLoadExtension, 42 | databaseMaxWriteReplicationIndex, 43 | statementRaw, 44 | statementIsReader, 45 | statementGet, 46 | statementRun, 47 | statementInterrupt, 48 | statementRowsSync, 49 | statementColumns, 50 | statementSafeIntegers, 51 | rowsNext, 52 | } = requireNative(); 53 | 54 | const Authorization = require("./auth"); 55 | const SqliteError = require("./sqlite-error"); 56 | 57 | function convertError(err) { 58 | if (err.libsqlError) { 59 | return new SqliteError(err.message, err.code, err.rawCode); 60 | } 61 | return err; 62 | } 63 | 64 | /** 65 | * Database represents a connection that can prepare and execute SQL statements. 66 | */ 67 | class Database { 68 | /** 69 | * Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created. 70 | * 71 | * @constructor 72 | * @param {string} path - Path to the database file. 73 | */ 74 | constructor(path, opts) { 75 | const encryptionCipher = opts?.encryptionCipher ?? "aes256cbc"; 76 | if (opts && opts.syncUrl) { 77 | var authToken = ""; 78 | if (opts.syncAuth) { 79 | console.warn("Warning: The `syncAuth` option is deprecated, please use `authToken` option instead."); 80 | authToken = opts.syncAuth; 81 | } else if (opts.authToken) { 82 | authToken = opts.authToken; 83 | } 84 | const encryptionKey = opts?.encryptionKey ?? ""; 85 | const syncPeriod = opts?.syncPeriod ?? 0.0; 86 | const readYourWrites = opts?.readYourWrites ?? true; 87 | const offline = opts?.offline ?? false; 88 | this.db = databaseOpenWithSync(path, opts.syncUrl, authToken, encryptionCipher, encryptionKey, syncPeriod, readYourWrites, offline); 89 | } else { 90 | const authToken = opts?.authToken ?? ""; 91 | const encryptionKey = opts?.encryptionKey ?? ""; 92 | const timeout = opts?.timeout ?? 0.0; 93 | this.db = databaseOpen(path, authToken, encryptionCipher, encryptionKey, timeout); 94 | } 95 | // TODO: Use a libSQL API for this? 96 | this.memory = path === ":memory:"; 97 | this.readonly = false; 98 | this.name = ""; 99 | this.open = true; 100 | 101 | const db = this.db; 102 | Object.defineProperties(this, { 103 | inTransaction: { 104 | get() { 105 | return databaseInTransaction(db); 106 | } 107 | }, 108 | }); 109 | } 110 | 111 | sync() { 112 | return databaseSyncSync.call(this.db); 113 | } 114 | 115 | syncUntil(replicationIndex) { 116 | return databaseSyncUntilSync.call(this.db, replicationIndex); 117 | } 118 | 119 | /** 120 | * Prepares a SQL statement for execution. 121 | * 122 | * @param {string} sql - The SQL statement string to prepare. 123 | */ 124 | prepare(sql) { 125 | try { 126 | const stmt = databasePrepareSync.call(this.db, sql); 127 | return new Statement(stmt); 128 | } catch (err) { 129 | throw convertError(err); 130 | } 131 | } 132 | 133 | /** 134 | * Returns a function that executes the given function in a transaction. 135 | * 136 | * @param {function} fn - The function to wrap in a transaction. 137 | */ 138 | transaction(fn) { 139 | if (typeof fn !== "function") 140 | throw new TypeError("Expected first argument to be a function"); 141 | 142 | const db = this; 143 | const wrapTxn = (mode) => { 144 | return (...bindParameters) => { 145 | db.exec("BEGIN " + mode); 146 | try { 147 | const result = fn(...bindParameters); 148 | db.exec("COMMIT"); 149 | return result; 150 | } catch (err) { 151 | db.exec("ROLLBACK"); 152 | throw err; 153 | } 154 | }; 155 | }; 156 | const properties = { 157 | default: { value: wrapTxn("") }, 158 | deferred: { value: wrapTxn("DEFERRED") }, 159 | immediate: { value: wrapTxn("IMMEDIATE") }, 160 | exclusive: { value: wrapTxn("EXCLUSIVE") }, 161 | database: { value: this, enumerable: true }, 162 | }; 163 | Object.defineProperties(properties.default.value, properties); 164 | Object.defineProperties(properties.deferred.value, properties); 165 | Object.defineProperties(properties.immediate.value, properties); 166 | Object.defineProperties(properties.exclusive.value, properties); 167 | return properties.default.value; 168 | } 169 | 170 | pragma(source, options) { 171 | if (options == null) options = {}; 172 | if (typeof source !== 'string') throw new TypeError('Expected first argument to be a string'); 173 | if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object'); 174 | const simple = options['simple']; 175 | const stmt = this.prepare(`PRAGMA ${source}`, this, true); 176 | return simple ? stmt.pluck().get() : stmt.all(); 177 | } 178 | 179 | backup(filename, options) { 180 | throw new Error("not implemented"); 181 | } 182 | 183 | serialize(options) { 184 | throw new Error("not implemented"); 185 | } 186 | 187 | function(name, options, fn) { 188 | // Apply defaults 189 | if (options == null) options = {}; 190 | if (typeof options === "function") { 191 | fn = options; 192 | options = {}; 193 | } 194 | 195 | // Validate arguments 196 | if (typeof name !== "string") 197 | throw new TypeError("Expected first argument to be a string"); 198 | if (typeof fn !== "function") 199 | throw new TypeError("Expected last argument to be a function"); 200 | if (typeof options !== "object") 201 | throw new TypeError("Expected second argument to be an options object"); 202 | if (!name) 203 | throw new TypeError( 204 | "User-defined function name cannot be an empty string" 205 | ); 206 | 207 | throw new Error("not implemented"); 208 | } 209 | 210 | aggregate(name, options) { 211 | // Validate arguments 212 | if (typeof name !== "string") 213 | throw new TypeError("Expected first argument to be a string"); 214 | if (typeof options !== "object" || options === null) 215 | throw new TypeError("Expected second argument to be an options object"); 216 | if (!name) 217 | throw new TypeError( 218 | "User-defined function name cannot be an empty string" 219 | ); 220 | 221 | throw new Error("not implemented"); 222 | } 223 | 224 | table(name, factory) { 225 | // Validate arguments 226 | if (typeof name !== "string") 227 | throw new TypeError("Expected first argument to be a string"); 228 | if (!name) 229 | throw new TypeError( 230 | "Virtual table module name cannot be an empty string" 231 | ); 232 | 233 | throw new Error("not implemented"); 234 | } 235 | 236 | authorizer(rules) { 237 | databaseAuthorizer.call(this.db, rules); 238 | } 239 | 240 | loadExtension(...args) { 241 | databaseLoadExtension.call(this.db, ...args); 242 | } 243 | 244 | maxWriteReplicationIndex() { 245 | return databaseMaxWriteReplicationIndex.call(this.db) 246 | } 247 | 248 | /** 249 | * Executes a SQL statement. 250 | * 251 | * @param {string} sql - The SQL statement string to execute. 252 | */ 253 | exec(sql) { 254 | try { 255 | databaseExecSync.call(this.db, sql); 256 | } catch (err) { 257 | throw convertError(err); 258 | } 259 | } 260 | 261 | /** 262 | * Interrupts the database connection. 263 | */ 264 | interrupt() { 265 | databaseInterrupt.call(this.db); 266 | } 267 | 268 | /** 269 | * Closes the database connection. 270 | */ 271 | close() { 272 | databaseClose.call(this.db); 273 | this.open = false; 274 | } 275 | 276 | /** 277 | * Toggle 64-bit integer support. 278 | */ 279 | defaultSafeIntegers(toggle) { 280 | databaseDefaultSafeIntegers.call(this.db, toggle ?? true); 281 | return this; 282 | } 283 | 284 | unsafeMode(...args) { 285 | throw new Error("not implemented"); 286 | } 287 | } 288 | 289 | /** 290 | * Statement represents a prepared SQL statement that can be executed. 291 | */ 292 | class Statement { 293 | constructor(stmt) { 294 | this.stmt = stmt; 295 | this.pluckMode = false; 296 | } 297 | 298 | /** 299 | * Toggle raw mode. 300 | * 301 | * @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled. 302 | */ 303 | raw(raw) { 304 | statementRaw.call(this.stmt, raw ?? true); 305 | return this; 306 | } 307 | 308 | /** 309 | * Toggle pluck mode. 310 | * 311 | * @param pluckMode Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled. 312 | */ 313 | pluck(pluckMode) { 314 | this.pluckMode = pluckMode ?? true; 315 | return this; 316 | } 317 | 318 | get reader() { 319 | return statementIsReader.call(this.stmt); 320 | } 321 | 322 | /** 323 | * Executes the SQL statement and returns an info object. 324 | */ 325 | run(...bindParameters) { 326 | try { 327 | if (bindParameters.length == 1 && typeof bindParameters[0] === "object") { 328 | return statementRun.call(this.stmt, bindParameters[0]); 329 | } else { 330 | return statementRun.call(this.stmt, bindParameters.flat()); 331 | } 332 | } catch (err) { 333 | throw convertError(err); 334 | } 335 | } 336 | 337 | /** 338 | * Executes the SQL statement and returns the first row. 339 | * 340 | * @param bindParameters - The bind parameters for executing the statement. 341 | */ 342 | get(...bindParameters) { 343 | try { 344 | if (bindParameters.length == 1 && typeof bindParameters[0] === "object") { 345 | return statementGet.call(this.stmt, bindParameters[0]); 346 | } else { 347 | return statementGet.call(this.stmt, bindParameters.flat()); 348 | } 349 | } catch (err) { 350 | throw convertError(err); 351 | } 352 | } 353 | 354 | /** 355 | * Executes the SQL statement and returns an iterator to the resulting rows. 356 | * 357 | * @param bindParameters - The bind parameters for executing the statement. 358 | */ 359 | iterate(...bindParameters) { 360 | var rows = undefined; 361 | if (bindParameters.length == 1 && typeof bindParameters[0] === "object") { 362 | rows = statementRowsSync.call(this.stmt, bindParameters[0]); 363 | } else { 364 | rows = statementRowsSync.call(this.stmt, bindParameters.flat()); 365 | } 366 | const iter = { 367 | nextRows: Array(100), 368 | nextRowIndex: 100, 369 | next() { 370 | try { 371 | if (this.nextRowIndex === 100) { 372 | rowsNext.call(rows, this.nextRows); 373 | this.nextRowIndex = 0; 374 | } 375 | const row = this.nextRows[this.nextRowIndex]; 376 | this.nextRows[this.nextRowIndex] = undefined; 377 | if (!row) { 378 | return { done: true }; 379 | } 380 | this.nextRowIndex++; 381 | return { value: row, done: false }; 382 | } catch (err) { 383 | throw convertError(err); 384 | } 385 | }, 386 | [Symbol.iterator]() { 387 | return this; 388 | }, 389 | }; 390 | return iter; 391 | } 392 | 393 | /** 394 | * Executes the SQL statement and returns an array of the resulting rows. 395 | * 396 | * @param bindParameters - The bind parameters for executing the statement. 397 | */ 398 | all(...bindParameters) { 399 | try { 400 | const result = []; 401 | for (const row of this.iterate(...bindParameters)) { 402 | if (this.pluckMode) { 403 | result.push(row[Object.keys(row)[0]]); 404 | } else { 405 | result.push(row); 406 | } 407 | } 408 | return result; 409 | } catch (err) { 410 | throw convertError(err); 411 | } 412 | } 413 | 414 | /** 415 | * Interrupts the statement. 416 | */ 417 | interrupt() { 418 | statementInterrupt.call(this.stmt); 419 | } 420 | 421 | /** 422 | * Returns the columns in the result set returned by this prepared statement. 423 | */ 424 | columns() { 425 | return statementColumns.call(this.stmt); 426 | } 427 | 428 | /** 429 | * Toggle 64-bit integer support. 430 | */ 431 | safeIntegers(toggle) { 432 | statementSafeIntegers.call(this.stmt, toggle ?? true); 433 | return this; 434 | } 435 | } 436 | 437 | module.exports = Database; 438 | module.exports.Authorization = Authorization; 439 | module.exports.SqliteError = SqliteError; 440 | -------------------------------------------------------------------------------- /integration-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libsql-integration-tests", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "test": "PROVIDER=sqlite ava tests/sync.test.js && LIBSQL_JS_DEV=1 PROVIDER=libsql ava tests/sync.test.js && LIBSQL_JS_DEV=1 ava tests/async.test.js && LIBSQL_JS_DEV=1 ava tests/extensions.test.js" 7 | }, 8 | "devDependencies": { 9 | "ava": "^5.3.0" 10 | }, 11 | "dependencies": { 12 | "better-sqlite3": "^8.4.0", 13 | "libsql": ".." 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /integration-tests/tests/async.test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import crypto from 'crypto'; 3 | import fs from 'fs'; 4 | 5 | 6 | test.beforeEach(async (t) => { 7 | const [db, errorType] = await connect(); 8 | await db.exec(` 9 | DROP TABLE IF EXISTS users; 10 | CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT) 11 | `); 12 | await db.exec( 13 | "INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')" 14 | ); 15 | await db.exec( 16 | "INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@example.com')" 17 | ); 18 | t.context = { 19 | db, 20 | errorType 21 | }; 22 | }); 23 | 24 | test.after.always(async (t) => { 25 | if (t.context.db != undefined) { 26 | t.context.db.close(); 27 | } 28 | }); 29 | 30 | test.serial("Open in-memory database", async (t) => { 31 | const [db] = await connect(":memory:"); 32 | t.is(db.memory, true); 33 | }); 34 | 35 | test.serial("Statement.prepare() error", async (t) => { 36 | const db = t.context.db; 37 | 38 | await t.throwsAsync(async () => { 39 | return await db.prepare("SYNTAX ERROR"); 40 | }, { 41 | instanceOf: t.context.errorType, 42 | message: 'near "SYNTAX": syntax error' 43 | }); 44 | }); 45 | 46 | test.serial("Statement.run() [positional]", async (t) => { 47 | const db = t.context.db; 48 | 49 | const stmt = await db.prepare("INSERT INTO users(name, email) VALUES (?, ?)"); 50 | const info = stmt.run(["Carol", "carol@example.net"]); 51 | t.is(info.changes, 1); 52 | t.is(info.lastInsertRowid, 3); 53 | }); 54 | 55 | test.serial("Statement.get() [no parameters]", async (t) => { 56 | const db = t.context.db; 57 | 58 | var stmt = 0; 59 | 60 | stmt = await db.prepare("SELECT * FROM users"); 61 | t.is(stmt.get().name, "Alice"); 62 | t.deepEqual(await stmt.raw().get(), [1, 'Alice', 'alice@example.org']); 63 | }); 64 | 65 | test.serial("Statement.get() [positional]", async (t) => { 66 | const db = t.context.db; 67 | 68 | var stmt = 0; 69 | 70 | stmt = await db.prepare("SELECT * FROM users WHERE id = ?"); 71 | t.is(stmt.get(0), undefined); 72 | t.is(stmt.get([0]), undefined); 73 | t.is(stmt.get(1).name, "Alice"); 74 | t.is(stmt.get(2).name, "Bob"); 75 | 76 | stmt = await db.prepare("SELECT * FROM users WHERE id = ?1"); 77 | t.is(stmt.get({1: 0}), undefined); 78 | t.is(stmt.get({1: 1}).name, "Alice"); 79 | t.is(stmt.get({1: 2}).name, "Bob"); 80 | }); 81 | 82 | test.serial("Statement.get() [named]", async (t) => { 83 | const db = t.context.db; 84 | 85 | var stmt = undefined; 86 | 87 | stmt = await db.prepare("SELECT * FROM users WHERE id = :id"); 88 | t.is(stmt.get({ id: 0 }), undefined); 89 | t.is(stmt.get({ id: 1 }).name, "Alice"); 90 | t.is(stmt.get({ id: 2 }).name, "Bob"); 91 | 92 | stmt = await db.prepare("SELECT * FROM users WHERE id = @id"); 93 | t.is(stmt.get({ id: 0 }), undefined); 94 | t.is(stmt.get({ id: 1 }).name, "Alice"); 95 | t.is(stmt.get({ id: 2 }).name, "Bob"); 96 | 97 | stmt = await db.prepare("SELECT * FROM users WHERE id = $id"); 98 | t.is(stmt.get({ id: 0 }), undefined); 99 | t.is(stmt.get({ id: 1 }).name, "Alice"); 100 | t.is(stmt.get({ id: 2 }).name, "Bob"); 101 | }); 102 | 103 | 104 | test.serial("Statement.get() [raw]", async (t) => { 105 | const db = t.context.db; 106 | 107 | const stmt = await db.prepare("SELECT * FROM users WHERE id = ?"); 108 | t.deepEqual(stmt.raw().get(1), [1, "Alice", "alice@example.org"]); 109 | }); 110 | 111 | test.serial("Statement.iterate() [empty]", async (t) => { 112 | const db = t.context.db; 113 | 114 | const stmt = await db.prepare("SELECT * FROM users WHERE id = 0"); 115 | const it = await stmt.iterate(); 116 | t.is(it.next().done, true); 117 | }); 118 | 119 | test.serial("Statement.iterate()", async (t) => { 120 | const db = t.context.db; 121 | 122 | const stmt = await db.prepare("SELECT * FROM users"); 123 | const expected = [1, 2]; 124 | var idx = 0; 125 | for (const row of await stmt.iterate()) { 126 | t.is(row.id, expected[idx++]); 127 | } 128 | }); 129 | 130 | test.serial("Statement.all()", async (t) => { 131 | const db = t.context.db; 132 | 133 | const stmt = await db.prepare("SELECT * FROM users"); 134 | const expected = [ 135 | { id: 1, name: "Alice", email: "alice@example.org" }, 136 | { id: 2, name: "Bob", email: "bob@example.com" }, 137 | ]; 138 | t.deepEqual(await stmt.all(), expected); 139 | }); 140 | 141 | test.serial("Statement.all() [raw]", async (t) => { 142 | const db = t.context.db; 143 | 144 | const stmt = await db.prepare("SELECT * FROM users"); 145 | const expected = [ 146 | [1, "Alice", "alice@example.org"], 147 | [2, "Bob", "bob@example.com"], 148 | ]; 149 | t.deepEqual(await stmt.raw().all(), expected); 150 | }); 151 | 152 | test.serial("Statement.all() [pluck]", async (t) => { 153 | const db = t.context.db; 154 | 155 | const stmt = await db.prepare("SELECT * FROM users"); 156 | const expected = [ 157 | 1, 158 | 2, 159 | ]; 160 | t.deepEqual(await stmt.pluck().all(), expected); 161 | }); 162 | 163 | test.serial("Statement.all() [default safe integers]", async (t) => { 164 | const db = t.context.db; 165 | db.defaultSafeIntegers(); 166 | const stmt = await db.prepare("SELECT * FROM users"); 167 | const expected = [ 168 | [1n, "Alice", "alice@example.org"], 169 | [2n, "Bob", "bob@example.com"], 170 | ]; 171 | t.deepEqual(await stmt.raw().all(), expected); 172 | }); 173 | 174 | test.serial("Statement.all() [statement safe integers]", async (t) => { 175 | const db = t.context.db; 176 | const stmt = await db.prepare("SELECT * FROM users"); 177 | stmt.safeIntegers(); 178 | const expected = [ 179 | [1n, "Alice", "alice@example.org"], 180 | [2n, "Bob", "bob@example.com"], 181 | ]; 182 | t.deepEqual(await stmt.raw().all(), expected); 183 | }); 184 | 185 | test.serial("Statement.raw() [failure]", async (t) => { 186 | const db = t.context.db; 187 | const stmt = await db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)"); 188 | await t.throws(() => { 189 | stmt.raw() 190 | }, { 191 | message: 'The raw() method is only for statements that return data' 192 | }); 193 | }); 194 | 195 | test.serial("Statement.columns()", async (t) => { 196 | const db = t.context.db; 197 | 198 | var stmt = undefined; 199 | 200 | stmt = await db.prepare("SELECT 1"); 201 | t.deepEqual(stmt.columns(), [ 202 | { 203 | column: null, 204 | database: null, 205 | name: '1', 206 | table: null, 207 | type: null, 208 | }, 209 | ]); 210 | 211 | stmt = await db.prepare("SELECT * FROM users WHERE id = ?"); 212 | t.deepEqual(stmt.columns(), [ 213 | { 214 | column: "id", 215 | database: "main", 216 | name: "id", 217 | table: "users", 218 | type: "INTEGER", 219 | }, 220 | { 221 | column: "name", 222 | database: "main", 223 | name: "name", 224 | table: "users", 225 | type: "TEXT", 226 | }, 227 | { 228 | column: "email", 229 | database: "main", 230 | name: "email", 231 | table: "users", 232 | type: "TEXT", 233 | }, 234 | ]); 235 | }); 236 | 237 | test.serial("Database.transaction()", async (t) => { 238 | const db = t.context.db; 239 | 240 | const insert = await db.prepare( 241 | "INSERT INTO users(name, email) VALUES (:name, :email)" 242 | ); 243 | 244 | const insertMany = db.transaction((users) => { 245 | t.is(db.inTransaction, true); 246 | for (const user of users) insert.run(user); 247 | }); 248 | 249 | t.is(db.inTransaction, false); 250 | await insertMany([ 251 | { name: "Joey", email: "joey@example.org" }, 252 | { name: "Sally", email: "sally@example.org" }, 253 | { name: "Junior", email: "junior@example.org" }, 254 | ]); 255 | t.is(db.inTransaction, false); 256 | 257 | const stmt = await db.prepare("SELECT * FROM users WHERE id = ?"); 258 | t.is(stmt.get(3).name, "Joey"); 259 | t.is(stmt.get(4).name, "Sally"); 260 | t.is(stmt.get(5).name, "Junior"); 261 | }); 262 | 263 | test.serial("Database.transaction().immediate()", async (t) => { 264 | const db = t.context.db; 265 | const insert = await db.prepare( 266 | "INSERT INTO users(name, email) VALUES (:name, :email)" 267 | ); 268 | const insertMany = db.transaction((users) => { 269 | t.is(db.inTransaction, true); 270 | for (const user of users) insert.run(user); 271 | }); 272 | t.is(db.inTransaction, false); 273 | await insertMany.immediate([ 274 | { name: "Joey", email: "joey@example.org" }, 275 | { name: "Sally", email: "sally@example.org" }, 276 | { name: "Junior", email: "junior@example.org" }, 277 | ]); 278 | t.is(db.inTransaction, false); 279 | }); 280 | 281 | test.serial("Database.pragma()", async (t) => { 282 | const db = t.context.db; 283 | await db.pragma("cache_size = 2000"); 284 | t.deepEqual(await db.pragma("cache_size"), [{ "cache_size": 2000 }]); 285 | }); 286 | 287 | test.serial("errors", async (t) => { 288 | const db = t.context.db; 289 | 290 | const syntaxError = await t.throwsAsync(async () => { 291 | await db.exec("SYNTAX ERROR"); 292 | }, { 293 | instanceOf: t.context.errorType, 294 | message: 'near "SYNTAX": syntax error', 295 | code: 'SQLITE_ERROR' 296 | }); 297 | 298 | t.is(syntaxError.rawCode, 1) 299 | const noTableError = await t.throwsAsync(async () => { 300 | await db.exec("SELECT * FROM missing_table"); 301 | }, { 302 | instanceOf: t.context.errorType, 303 | message: "no such table: missing_table", 304 | code: 'SQLITE_ERROR' 305 | }); 306 | t.is(noTableError.rawCode, 1) 307 | }); 308 | 309 | test.serial("Database.prepare() after close()", async (t) => { 310 | const db = t.context.db; 311 | await db.close(); 312 | await t.throwsAsync(async () => { 313 | await db.prepare("SELECT 1"); 314 | }, { 315 | instanceOf: TypeError, 316 | message: "The database connection is not open" 317 | }); 318 | }); 319 | 320 | test.serial("Database.exec() after close()", async (t) => { 321 | const db = t.context.db; 322 | await db.close(); 323 | await t.throwsAsync(async () => { 324 | await db.exec("SELECT 1"); 325 | }, { 326 | instanceOf: TypeError, 327 | message: "The database connection is not open" 328 | }); 329 | }); 330 | 331 | test.serial("Database.interrupt()", async (t) => { 332 | const db = t.context.db; 333 | const stmt = await db.prepare("WITH RECURSIVE infinite_loop(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM infinite_loop) SELECT * FROM infinite_loop;"); 334 | const fut = stmt.all(); 335 | db.interrupt(); 336 | await t.throwsAsync(async () => { 337 | await fut; 338 | }, { 339 | instanceOf: t.context.errorType, 340 | message: 'interrupted', 341 | code: 'SQLITE_INTERRUPT' 342 | }); 343 | }); 344 | 345 | 346 | test.serial("Statement.interrupt()", async (t) => { 347 | const db = t.context.db; 348 | const stmt = await db.prepare("WITH RECURSIVE infinite_loop(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM infinite_loop) SELECT * FROM infinite_loop;"); 349 | const fut = stmt.all(); 350 | stmt.interrupt(); 351 | await t.throwsAsync(async () => { 352 | await fut; 353 | }, { 354 | instanceOf: t.context.errorType, 355 | message: 'interrupted', 356 | code: 'SQLITE_INTERRUPT' 357 | }); 358 | }); 359 | 360 | test.serial("Timeout option", async (t) => { 361 | const timeout = 1000; 362 | const path = genDatabaseFilename(); 363 | const [conn1] = await connect(path); 364 | await conn1.exec("CREATE TABLE t(x)"); 365 | await conn1.exec("BEGIN IMMEDIATE"); 366 | await conn1.exec("INSERT INTO t VALUES (1)") 367 | const options = { timeout }; 368 | const [conn2] = await connect(path, options); 369 | const start = Date.now(); 370 | try { 371 | await conn2.exec("INSERT INTO t VALUES (1)") 372 | } catch (e) { 373 | t.is(e.code, "SQLITE_BUSY"); 374 | const end = Date.now(); 375 | const elapsed = end - start; 376 | // Allow some tolerance for the timeout. 377 | t.is(elapsed > timeout/2, true); 378 | } 379 | fs.unlinkSync(path); 380 | }); 381 | 382 | test.serial("Concurrent writes over same connection", async (t) => { 383 | const db = t.context.db; 384 | await db.exec(` 385 | DROP TABLE IF EXISTS users; 386 | CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT) 387 | `); 388 | const stmt = await db.prepare("INSERT INTO users(name, email) VALUES (:name, :email)"); 389 | const promises = []; 390 | for (let i = 0; i < 1000; i++) { 391 | promises.push(stmt.run({ name: "Alice", email: "alice@example.org" })); 392 | } 393 | await Promise.all(promises); 394 | const stmt2 = await db.prepare("SELECT * FROM users ORDER BY name"); 395 | const rows = await stmt2.all(); 396 | t.is(rows.length, 1000); 397 | }); 398 | 399 | const connect = async (path_opt, options = {}) => { 400 | const path = path_opt ?? "hello.db"; 401 | const provider = process.env.PROVIDER; 402 | const database = process.env.LIBSQL_DATABASE ?? path; 403 | const x = await import("libsql/promise"); 404 | const db = new x.default(database, options); 405 | return [db, x.SqliteError]; 406 | }; 407 | 408 | /// Generate a unique database filename 409 | const genDatabaseFilename = () => { 410 | return `test-${crypto.randomBytes(8).toString('hex')}.db`; 411 | }; 412 | -------------------------------------------------------------------------------- /integration-tests/tests/extensions.test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { Authorization } from "libsql"; 3 | 4 | test.serial("Statement.run() returning duration", async (t) => { 5 | const db = t.context.db; 6 | 7 | const stmt = db.prepare("SELECT 1"); 8 | const info = stmt.run(); 9 | t.not(info.duration, undefined); 10 | t.log(info.duration) 11 | }); 12 | 13 | test.serial("Statement.get() returning duration", async (t) => { 14 | const db = t.context.db; 15 | 16 | const stmt = db.prepare("SELECT ?"); 17 | const info = stmt.get(1); 18 | t.not(info._metadata?.duration, undefined); 19 | t.log(info._metadata?.duration) 20 | }); 21 | 22 | test.serial("Database.authorizer()/allow", async (t) => { 23 | const db = t.context.db; 24 | 25 | db.authorizer({ 26 | "users": Authorization.ALLOW 27 | }); 28 | 29 | const stmt = db.prepare("SELECT * FROM users"); 30 | const users = stmt.all(); 31 | t.is(users.length, 2); 32 | }); 33 | 34 | test.serial("Database.authorizer()/deny", async (t) => { 35 | const db = t.context.db; 36 | 37 | db.authorizer({ 38 | "users": Authorization.DENY 39 | }); 40 | await t.throwsAsync(async () => { 41 | return await db.prepare("SELECT * FROM users"); 42 | }, { 43 | instanceOf: t.context.errorType, 44 | code: "SQLITE_AUTH" 45 | }); 46 | }); 47 | 48 | const connect = async (path_opt) => { 49 | const path = path_opt ?? "hello.db"; 50 | const x = await import("libsql"); 51 | const db = new x.default(process.env.LIBSQL_DATABASE ?? path, {}); 52 | return [db, x.SqliteError, "libsql"]; 53 | }; 54 | 55 | test.beforeEach(async (t) => { 56 | const [db, errorType, provider] = await connect(); 57 | db.exec(` 58 | DROP TABLE IF EXISTS users; 59 | CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT) 60 | `); 61 | db.exec( 62 | "INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')" 63 | ); 64 | db.exec( 65 | "INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@example.com')" 66 | ); 67 | t.context = { 68 | db, 69 | errorType, 70 | provider 71 | }; 72 | }); 73 | -------------------------------------------------------------------------------- /integration-tests/tests/sync.test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import crypto from 'crypto'; 3 | import fs from 'fs'; 4 | 5 | test.beforeEach(async (t) => { 6 | const [db, errorType, provider] = await connect(); 7 | db.exec(` 8 | DROP TABLE IF EXISTS users; 9 | CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT) 10 | `); 11 | db.exec( 12 | "INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')" 13 | ); 14 | db.exec( 15 | "INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@example.com')" 16 | ); 17 | t.context = { 18 | db, 19 | errorType, 20 | provider 21 | }; 22 | }); 23 | 24 | test.after.always(async (t) => { 25 | if (t.context.db != undefined) { 26 | t.context.db.close(); 27 | } 28 | }); 29 | 30 | test.serial("Open in-memory database", async (t) => { 31 | const [db] = await connect(":memory:"); 32 | t.is(db.memory, true); 33 | }); 34 | 35 | test.serial("Statement.prepare() error", async (t) => { 36 | const db = t.context.db; 37 | 38 | t.throws(() => { 39 | return db.prepare("SYNTAX ERROR"); 40 | }, { 41 | instanceOf: t.context.errorType, 42 | message: 'near "SYNTAX": syntax error' 43 | }); 44 | }); 45 | 46 | test.serial("Statement.run() returning rows", async (t) => { 47 | const db = t.context.db; 48 | 49 | const stmt = db.prepare("SELECT 1"); 50 | const info = stmt.run(); 51 | t.is(info.changes, 0); 52 | }); 53 | 54 | test.serial("Statement.run() [positional]", async (t) => { 55 | const db = t.context.db; 56 | 57 | const stmt = db.prepare("INSERT INTO users(name, email) VALUES (?, ?)"); 58 | const info = stmt.run(["Carol", "carol@example.net"]); 59 | t.is(info.changes, 1); 60 | t.is(info.lastInsertRowid, 3); 61 | }); 62 | 63 | test.serial("Statement.run() [named]", async (t) => { 64 | const db = t.context.db; 65 | 66 | const stmt = db.prepare("INSERT INTO users(name, email) VALUES (@name, @email);"); 67 | const info = stmt.run({"name": "Carol", "email": "carol@example.net"}); 68 | t.is(info.changes, 1); 69 | t.is(info.lastInsertRowid, 3); 70 | }); 71 | 72 | test.serial("Statement.get() [no parameters]", async (t) => { 73 | const db = t.context.db; 74 | 75 | var stmt = 0; 76 | 77 | stmt = db.prepare("SELECT * FROM users"); 78 | t.is(stmt.get().name, "Alice"); 79 | t.deepEqual(stmt.raw().get(), [1, 'Alice', 'alice@example.org']); 80 | }); 81 | 82 | test.serial("Statement.get() [positional]", async (t) => { 83 | const db = t.context.db; 84 | 85 | var stmt = 0; 86 | 87 | stmt = db.prepare("SELECT * FROM users WHERE id = ?"); 88 | t.is(stmt.get(0), undefined); 89 | t.is(stmt.get([0]), undefined); 90 | t.is(stmt.get(1).name, "Alice"); 91 | t.is(stmt.get(2).name, "Bob"); 92 | 93 | stmt = db.prepare("SELECT * FROM users WHERE id = ?1"); 94 | t.is(stmt.get({1: 0}), undefined); 95 | t.is(stmt.get({1: 1}).name, "Alice"); 96 | t.is(stmt.get({1: 2}).name, "Bob"); 97 | }); 98 | 99 | test.serial("Statement.get() [named]", async (t) => { 100 | const db = t.context.db; 101 | 102 | var stmt = undefined; 103 | 104 | stmt = db.prepare("SELECT :b, :a"); 105 | t.deepEqual(stmt.raw().get({ a: 'a', b: 'b' }), ['b', 'a']); 106 | 107 | stmt = db.prepare("SELECT * FROM users WHERE id = :id"); 108 | t.is(stmt.get({ id: 0 }), undefined); 109 | t.is(stmt.get({ id: 1 }).name, "Alice"); 110 | t.is(stmt.get({ id: 2 }).name, "Bob"); 111 | 112 | stmt = db.prepare("SELECT * FROM users WHERE id = @id"); 113 | t.is(stmt.get({ id: 0 }), undefined); 114 | t.is(stmt.get({ id: 1 }).name, "Alice"); 115 | t.is(stmt.get({ id: 2 }).name, "Bob"); 116 | 117 | stmt = db.prepare("SELECT * FROM users WHERE id = $id"); 118 | t.is(stmt.get({ id: 0 }), undefined); 119 | t.is(stmt.get({ id: 1 }).name, "Alice"); 120 | t.is(stmt.get({ id: 2 }).name, "Bob"); 121 | }); 122 | 123 | test.serial("Statement.get() [raw]", async (t) => { 124 | const db = t.context.db; 125 | 126 | const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); 127 | t.deepEqual(stmt.raw().get(1), [1, "Alice", "alice@example.org"]); 128 | }); 129 | 130 | test.serial("Statement.iterate() [empty]", async (t) => { 131 | const db = t.context.db; 132 | 133 | const stmt = db.prepare("SELECT * FROM users WHERE id = 0"); 134 | t.is(stmt.iterate().next().done, true); 135 | t.is(stmt.iterate([]).next().done, true); 136 | t.is(stmt.iterate({}).next().done, true); 137 | }); 138 | 139 | test.serial("Statement.iterate()", async (t) => { 140 | const db = t.context.db; 141 | 142 | const stmt = db.prepare("SELECT * FROM users"); 143 | const expected = [1, 2]; 144 | var idx = 0; 145 | for (const row of stmt.iterate()) { 146 | t.is(row.id, expected[idx++]); 147 | } 148 | }); 149 | 150 | test.serial("Statement.all()", async (t) => { 151 | const db = t.context.db; 152 | 153 | const stmt = db.prepare("SELECT * FROM users"); 154 | const expected = [ 155 | { id: 1, name: "Alice", email: "alice@example.org" }, 156 | { id: 2, name: "Bob", email: "bob@example.com" }, 157 | ]; 158 | t.deepEqual(stmt.all(), expected); 159 | }); 160 | 161 | test.serial("Statement.all() [raw]", async (t) => { 162 | const db = t.context.db; 163 | 164 | const stmt = db.prepare("SELECT * FROM users"); 165 | const expected = [ 166 | [1, "Alice", "alice@example.org"], 167 | [2, "Bob", "bob@example.com"], 168 | ]; 169 | t.deepEqual(stmt.raw().all(), expected); 170 | }); 171 | 172 | test.serial("Statement.all() [pluck]", async (t) => { 173 | const db = t.context.db; 174 | 175 | const stmt = db.prepare("SELECT * FROM users"); 176 | const expected = [ 177 | 1, 178 | 2, 179 | ]; 180 | t.deepEqual(stmt.pluck().all(), expected); 181 | }); 182 | 183 | test.serial("Statement.all() [default safe integers]", async (t) => { 184 | const db = t.context.db; 185 | db.defaultSafeIntegers(); 186 | const stmt = db.prepare("SELECT * FROM users"); 187 | const expected = [ 188 | [1n, "Alice", "alice@example.org"], 189 | [2n, "Bob", "bob@example.com"], 190 | ]; 191 | t.deepEqual(stmt.raw().all(), expected); 192 | }); 193 | 194 | test.serial("Statement.all() [statement safe integers]", async (t) => { 195 | const db = t.context.db; 196 | const stmt = db.prepare("SELECT * FROM users"); 197 | stmt.safeIntegers(); 198 | const expected = [ 199 | [1n, "Alice", "alice@example.org"], 200 | [2n, "Bob", "bob@example.com"], 201 | ]; 202 | t.deepEqual(stmt.raw().all(), expected); 203 | }); 204 | 205 | test.serial("Statement.raw() [failure]", async (t) => { 206 | const db = t.context.db; 207 | const stmt = db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)"); 208 | await t.throws(() => { 209 | stmt.raw() 210 | }, { 211 | message: 'The raw() method is only for statements that return data' 212 | }); 213 | }); 214 | 215 | test.serial("Statement.run() with array bind parameter", async (t) => { 216 | const db = t.context.db; 217 | 218 | db.exec(` 219 | DROP TABLE IF EXISTS t; 220 | CREATE TABLE t (value BLOB); 221 | `); 222 | 223 | const array = [1, 2, 3]; 224 | 225 | const insertStmt = db.prepare("INSERT INTO t (value) VALUES (?)"); 226 | await t.throws(() => { 227 | insertStmt.run([array]); 228 | }, { 229 | message: 'SQLite3 can only bind numbers, strings, bigints, buffers, and null' 230 | }); 231 | }); 232 | 233 | test.serial("Statement.run() with Float32Array bind parameter", async (t) => { 234 | const db = t.context.db; 235 | 236 | db.exec(` 237 | DROP TABLE IF EXISTS t; 238 | CREATE TABLE t (value BLOB); 239 | `); 240 | 241 | const array = new Float32Array([1, 2, 3]); 242 | 243 | const insertStmt = db.prepare("INSERT INTO t (value) VALUES (?)"); 244 | insertStmt.run([array]); 245 | 246 | const selectStmt = db.prepare("SELECT value FROM t"); 247 | t.deepEqual(selectStmt.raw().get()[0], Buffer.from(array.buffer)); 248 | }); 249 | 250 | test.serial("Statement.run() for vector feature with Float32Array bind parameter", async (t) => { 251 | if (t.context.provider === 'sqlite') { 252 | // skip this test for sqlite 253 | t.assert(true); 254 | return; 255 | } 256 | const db = t.context.db; 257 | 258 | db.exec(` 259 | DROP TABLE IF EXISTS t; 260 | CREATE TABLE t (embedding FLOAT32(8)); 261 | CREATE INDEX t_idx ON t ( libsql_vector_idx(embedding) ); 262 | `); 263 | 264 | const insertStmt = db.prepare("INSERT INTO t VALUES (?)"); 265 | insertStmt.run([new Float32Array([1,1,1,1,1,1,1,1])]); 266 | insertStmt.run([new Float32Array([-1,-1,-1,-1,-1,-1,-1,-1])]); 267 | 268 | const selectStmt = db.prepare("SELECT embedding FROM vector_top_k('t_idx', vector('[2,2,2,2,2,2,2,2]'), 1) n JOIN t ON n.rowid = t.rowid"); 269 | t.deepEqual(selectStmt.raw().get()[0], Buffer.from(new Float32Array([1,1,1,1,1,1,1,1]).buffer)); 270 | 271 | // we need to explicitly delete this table because later when sqlite-based (not LibSQL) tests will delete table 't' they will leave 't_idx_shadow' table untouched 272 | db.exec(`DROP TABLE t`); 273 | }); 274 | 275 | test.serial("Statement.columns()", async (t) => { 276 | const db = t.context.db; 277 | 278 | var stmt = undefined; 279 | 280 | stmt = db.prepare("SELECT 1"); 281 | t.deepEqual(stmt.columns(), [ 282 | { 283 | column: null, 284 | database: null, 285 | name: '1', 286 | table: null, 287 | type: null, 288 | }, 289 | ]); 290 | 291 | stmt = db.prepare("SELECT * FROM users WHERE id = ?"); 292 | t.deepEqual(stmt.columns(), [ 293 | { 294 | column: "id", 295 | database: "main", 296 | name: "id", 297 | table: "users", 298 | type: "INTEGER", 299 | }, 300 | { 301 | column: "name", 302 | database: "main", 303 | name: "name", 304 | table: "users", 305 | type: "TEXT", 306 | }, 307 | { 308 | column: "email", 309 | database: "main", 310 | name: "email", 311 | table: "users", 312 | type: "TEXT", 313 | }, 314 | ]); 315 | }); 316 | 317 | test.serial("Database.transaction()", async (t) => { 318 | const db = t.context.db; 319 | 320 | const insert = db.prepare( 321 | "INSERT INTO users(name, email) VALUES (:name, :email)" 322 | ); 323 | 324 | const insertMany = db.transaction((users) => { 325 | t.is(db.inTransaction, true); 326 | for (const user of users) insert.run(user); 327 | }); 328 | 329 | t.is(db.inTransaction, false); 330 | insertMany([ 331 | { name: "Joey", email: "joey@example.org" }, 332 | { name: "Sally", email: "sally@example.org" }, 333 | { name: "Junior", email: "junior@example.org" }, 334 | ]); 335 | t.is(db.inTransaction, false); 336 | 337 | const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); 338 | t.is(stmt.get(3).name, "Joey"); 339 | t.is(stmt.get(4).name, "Sally"); 340 | t.is(stmt.get(5).name, "Junior"); 341 | }); 342 | 343 | test.serial("Database.transaction().immediate()", async (t) => { 344 | const db = t.context.db; 345 | const insert = db.prepare( 346 | "INSERT INTO users(name, email) VALUES (:name, :email)" 347 | ); 348 | const insertMany = db.transaction((users) => { 349 | t.is(db.inTransaction, true); 350 | for (const user of users) insert.run(user); 351 | }); 352 | t.is(db.inTransaction, false); 353 | insertMany.immediate([ 354 | { name: "Joey", email: "joey@example.org" }, 355 | { name: "Sally", email: "sally@example.org" }, 356 | { name: "Junior", email: "junior@example.org" }, 357 | ]); 358 | t.is(db.inTransaction, false); 359 | }); 360 | 361 | test.serial("values", async (t) => { 362 | const db = t.context.db; 363 | 364 | const stmt = db.prepare("SELECT ?").raw(); 365 | t.deepEqual(stmt.get(1), [1]); 366 | t.deepEqual(stmt.get(Number.MIN_VALUE), [Number.MIN_VALUE]); 367 | t.deepEqual(stmt.get(Number.MAX_VALUE), [Number.MAX_VALUE]); 368 | t.deepEqual(stmt.get(Number.MAX_SAFE_INTEGER), [Number.MAX_SAFE_INTEGER]); 369 | t.deepEqual(stmt.get(9007199254740991n), [9007199254740991]); 370 | }); 371 | 372 | test.serial("Database.pragma()", async (t) => { 373 | const db = t.context.db; 374 | db.pragma("cache_size = 2000"); 375 | t.deepEqual(db.pragma("cache_size"), [{ "cache_size": 2000 }]); 376 | }); 377 | 378 | test.serial("errors", async (t) => { 379 | const db = t.context.db; 380 | 381 | const syntaxError = await t.throws(() => { 382 | db.exec("SYNTAX ERROR"); 383 | }, { 384 | instanceOf: t.context.errorType, 385 | message: 'near "SYNTAX": syntax error', 386 | code: 'SQLITE_ERROR' 387 | }); 388 | const noTableError = await t.throws(() => { 389 | db.exec("SELECT * FROM missing_table"); 390 | }, { 391 | instanceOf: t.context.errorType, 392 | message: "no such table: missing_table", 393 | code: 'SQLITE_ERROR' 394 | }); 395 | 396 | if (t.context.provider === 'libsql') { 397 | t.is(noTableError.rawCode, 1) 398 | t.is(syntaxError.rawCode, 1) 399 | } 400 | }); 401 | 402 | test.serial("Database.prepare() after close()", async (t) => { 403 | const db = t.context.db; 404 | db.close(); 405 | t.throws(() => { 406 | db.prepare("SELECT 1"); 407 | }, { 408 | instanceOf: TypeError, 409 | message: "The database connection is not open" 410 | }); 411 | }); 412 | 413 | test.serial("Database.exec() after close()", async (t) => { 414 | const db = t.context.db; 415 | db.close(); 416 | t.throws(() => { 417 | db.exec("SELECT 1"); 418 | }, { 419 | instanceOf: TypeError, 420 | message: "The database connection is not open" 421 | }); 422 | }); 423 | 424 | test.serial("Timeout option", async (t) => { 425 | const timeout = 1000; 426 | const path = genDatabaseFilename(); 427 | const [conn1] = await connect(path); 428 | conn1.exec("CREATE TABLE t(x)"); 429 | conn1.exec("BEGIN IMMEDIATE"); 430 | conn1.exec("INSERT INTO t VALUES (1)") 431 | const options = { timeout }; 432 | const [conn2] = await connect(path, options); 433 | const start = Date.now(); 434 | try { 435 | conn2.exec("INSERT INTO t VALUES (1)") 436 | } catch (e) { 437 | t.is(e.code, "SQLITE_BUSY"); 438 | const end = Date.now(); 439 | const elapsed = end - start; 440 | // Allow some tolerance for the timeout. 441 | t.is(elapsed > timeout/2, true); 442 | } 443 | fs.unlinkSync(path); 444 | }); 445 | 446 | const connect = async (path_opt, options = {}) => { 447 | const path = path_opt ?? "hello.db"; 448 | const provider = process.env.PROVIDER; 449 | if (provider === "libsql") { 450 | const database = process.env.LIBSQL_DATABASE ?? path; 451 | const x = await import("libsql"); 452 | const db = new x.default(database, options); 453 | return [db, x.SqliteError, provider]; 454 | } 455 | if (provider == "sqlite") { 456 | const x = await import("better-sqlite3"); 457 | const db = x.default(path, options); 458 | return [db, x.SqliteError, provider]; 459 | } 460 | throw new Error("Unknown provider: " + provider); 461 | }; 462 | 463 | /// Generate a unique database filename 464 | const genDatabaseFilename = () => { 465 | return `test-${crypto.randomBytes(8).toString('hex')}.db`; 466 | }; 467 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libsql", 3 | "version": "0.5.12", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "libsql", 9 | "version": "0.5.12", 10 | "cpu": [ 11 | "x64", 12 | "arm64", 13 | "wasm32", 14 | "arm" 15 | ], 16 | "license": "MIT", 17 | "os": [ 18 | "darwin", 19 | "linux", 20 | "win32" 21 | ], 22 | "dependencies": { 23 | "@neon-rs/load": "^0.0.4", 24 | "detect-libc": "2.0.2" 25 | }, 26 | "devDependencies": { 27 | "@neon-rs/cli": "^0.0.165", 28 | "typescript": "^5.4.5" 29 | } 30 | }, 31 | "node_modules/@cargo-messages/android-arm-eabi": { 32 | "version": "0.0.165", 33 | "resolved": "https://registry.npmjs.org/@cargo-messages/android-arm-eabi/-/android-arm-eabi-0.0.165.tgz", 34 | "integrity": "sha512-J8jnzObmdSOurNrhtN+9atKoEQHJBHmFpORJoFlus2RggPPqilAJsL4a073qlpLCnNbcAps23Ccayuj11uHlkg==", 35 | "cpu": [ 36 | "arm" 37 | ], 38 | "dev": true, 39 | "optional": true, 40 | "os": [ 41 | "android" 42 | ] 43 | }, 44 | "node_modules/@cargo-messages/darwin-arm64": { 45 | "version": "0.0.165", 46 | "resolved": "https://registry.npmjs.org/@cargo-messages/darwin-arm64/-/darwin-arm64-0.0.165.tgz", 47 | "integrity": "sha512-KwP7NryO8KRGuSmoE/PaB/1M1W3iKUKZ3PQSNNfF9G/qUpqHLZH41eLSZx1FvD1q8Kvpv5ysY3Obwd8cTDDJ1A==", 48 | "cpu": [ 49 | "arm64" 50 | ], 51 | "dev": true, 52 | "optional": true, 53 | "os": [ 54 | "darwin" 55 | ] 56 | }, 57 | "node_modules/@cargo-messages/darwin-x64": { 58 | "version": "0.0.165", 59 | "resolved": "https://registry.npmjs.org/@cargo-messages/darwin-x64/-/darwin-x64-0.0.165.tgz", 60 | "integrity": "sha512-reudoNbbnVqUSWb5lLqdOExYRkY7tdEopgp/ut9PgkOTkAzEoc36u2G+6GLm42uIU6CFzvzUvC5Vl4bSMLQ95A==", 61 | "cpu": [ 62 | "x64" 63 | ], 64 | "dev": true, 65 | "optional": true, 66 | "os": [ 67 | "darwin" 68 | ] 69 | }, 70 | "node_modules/@cargo-messages/linux-arm-gnueabihf": { 71 | "version": "0.0.165", 72 | "resolved": "https://registry.npmjs.org/@cargo-messages/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.0.165.tgz", 73 | "integrity": "sha512-GVbdZQ8rwQF7kxTGasiW5N86fJKHaHuSKBJj/9GJj8OacRwpfdSYKdkTBu3AbNQfrqUQIGbzsjiiLed1gYtiSA==", 74 | "cpu": [ 75 | "arm" 76 | ], 77 | "dev": true, 78 | "optional": true, 79 | "os": [ 80 | "linux" 81 | ] 82 | }, 83 | "node_modules/@cargo-messages/linux-x64-gnu": { 84 | "version": "0.0.165", 85 | "resolved": "https://registry.npmjs.org/@cargo-messages/linux-x64-gnu/-/linux-x64-gnu-0.0.165.tgz", 86 | "integrity": "sha512-CwbNW3ijMItDaKeX2SHag9G0uCmRDcTajh+fR01I5rNA49KNf0TrK92qWmOoj1Uql5RYe9uDfoYQRHwxfantrA==", 87 | "cpu": [ 88 | "x64" 89 | ], 90 | "dev": true, 91 | "optional": true, 92 | "os": [ 93 | "linux" 94 | ] 95 | }, 96 | "node_modules/@cargo-messages/win32-arm64-msvc": { 97 | "version": "0.0.165", 98 | "resolved": "https://registry.npmjs.org/@cargo-messages/win32-arm64-msvc/-/win32-arm64-msvc-0.0.165.tgz", 99 | "integrity": "sha512-YjBCi0aS+ZVig4oVfs3VEefaY2RIK6KDMdF3ma0ccRxaAsebzOr7hTGwKC9qmGKZ2+B9RfK6m5bRn7W/uIhaWw==", 100 | "cpu": [ 101 | "arm64" 102 | ], 103 | "dev": true, 104 | "optional": true, 105 | "os": [ 106 | "win32" 107 | ] 108 | }, 109 | "node_modules/@cargo-messages/win32-x64-msvc": { 110 | "version": "0.0.165", 111 | "resolved": "https://registry.npmjs.org/@cargo-messages/win32-x64-msvc/-/win32-x64-msvc-0.0.165.tgz", 112 | "integrity": "sha512-8aLHAdVWQdfFVMsQMIQHJHlFWmtLehYN/71xQo3x17NIY3eJjf+VejNLluKujRykjVx2/klc1KO2Jj1vzycifA==", 113 | "cpu": [ 114 | "x64" 115 | ], 116 | "dev": true, 117 | "optional": true, 118 | "os": [ 119 | "win32" 120 | ] 121 | }, 122 | "node_modules/@neon-rs/cli": { 123 | "version": "0.0.165", 124 | "resolved": "https://registry.npmjs.org/@neon-rs/cli/-/cli-0.0.165.tgz", 125 | "integrity": "sha512-jGIWtlQf6GzomurFnbM9txr26uY4MuPJEQaB+wP2jOKywrdYXzAed0NyjZuwR6RZJH+PphSKmG/U0Fhxw9XxTA==", 126 | "dev": true, 127 | "bin": { 128 | "neon": "index.js" 129 | }, 130 | "optionalDependencies": { 131 | "@cargo-messages/android-arm-eabi": "0.0.165", 132 | "@cargo-messages/darwin-arm64": "0.0.165", 133 | "@cargo-messages/darwin-x64": "0.0.165", 134 | "@cargo-messages/linux-arm-gnueabihf": "0.0.165", 135 | "@cargo-messages/linux-x64-gnu": "0.0.165", 136 | "@cargo-messages/win32-arm64-msvc": "0.0.165", 137 | "@cargo-messages/win32-x64-msvc": "0.0.165" 138 | } 139 | }, 140 | "node_modules/@neon-rs/load": { 141 | "version": "0.0.4", 142 | "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", 143 | "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==" 144 | }, 145 | "node_modules/detect-libc": { 146 | "version": "2.0.2", 147 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", 148 | "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", 149 | "engines": { 150 | "node": ">=8" 151 | } 152 | }, 153 | "node_modules/typescript": { 154 | "version": "5.4.5", 155 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", 156 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", 157 | "dev": true, 158 | "bin": { 159 | "tsc": "bin/tsc", 160 | "tsserver": "bin/tsserver" 161 | }, 162 | "engines": { 163 | "node": ">=14.17" 164 | } 165 | } 166 | }, 167 | "dependencies": { 168 | "@cargo-messages/android-arm-eabi": { 169 | "version": "0.0.165", 170 | "resolved": "https://registry.npmjs.org/@cargo-messages/android-arm-eabi/-/android-arm-eabi-0.0.165.tgz", 171 | "integrity": "sha512-J8jnzObmdSOurNrhtN+9atKoEQHJBHmFpORJoFlus2RggPPqilAJsL4a073qlpLCnNbcAps23Ccayuj11uHlkg==", 172 | "dev": true, 173 | "optional": true 174 | }, 175 | "@cargo-messages/darwin-arm64": { 176 | "version": "0.0.165", 177 | "resolved": "https://registry.npmjs.org/@cargo-messages/darwin-arm64/-/darwin-arm64-0.0.165.tgz", 178 | "integrity": "sha512-KwP7NryO8KRGuSmoE/PaB/1M1W3iKUKZ3PQSNNfF9G/qUpqHLZH41eLSZx1FvD1q8Kvpv5ysY3Obwd8cTDDJ1A==", 179 | "dev": true, 180 | "optional": true 181 | }, 182 | "@cargo-messages/darwin-x64": { 183 | "version": "0.0.165", 184 | "resolved": "https://registry.npmjs.org/@cargo-messages/darwin-x64/-/darwin-x64-0.0.165.tgz", 185 | "integrity": "sha512-reudoNbbnVqUSWb5lLqdOExYRkY7tdEopgp/ut9PgkOTkAzEoc36u2G+6GLm42uIU6CFzvzUvC5Vl4bSMLQ95A==", 186 | "dev": true, 187 | "optional": true 188 | }, 189 | "@cargo-messages/linux-arm-gnueabihf": { 190 | "version": "0.0.165", 191 | "resolved": "https://registry.npmjs.org/@cargo-messages/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.0.165.tgz", 192 | "integrity": "sha512-GVbdZQ8rwQF7kxTGasiW5N86fJKHaHuSKBJj/9GJj8OacRwpfdSYKdkTBu3AbNQfrqUQIGbzsjiiLed1gYtiSA==", 193 | "dev": true, 194 | "optional": true 195 | }, 196 | "@cargo-messages/linux-x64-gnu": { 197 | "version": "0.0.165", 198 | "resolved": "https://registry.npmjs.org/@cargo-messages/linux-x64-gnu/-/linux-x64-gnu-0.0.165.tgz", 199 | "integrity": "sha512-CwbNW3ijMItDaKeX2SHag9G0uCmRDcTajh+fR01I5rNA49KNf0TrK92qWmOoj1Uql5RYe9uDfoYQRHwxfantrA==", 200 | "dev": true, 201 | "optional": true 202 | }, 203 | "@cargo-messages/win32-arm64-msvc": { 204 | "version": "0.0.165", 205 | "resolved": "https://registry.npmjs.org/@cargo-messages/win32-arm64-msvc/-/win32-arm64-msvc-0.0.165.tgz", 206 | "integrity": "sha512-YjBCi0aS+ZVig4oVfs3VEefaY2RIK6KDMdF3ma0ccRxaAsebzOr7hTGwKC9qmGKZ2+B9RfK6m5bRn7W/uIhaWw==", 207 | "dev": true, 208 | "optional": true 209 | }, 210 | "@cargo-messages/win32-x64-msvc": { 211 | "version": "0.0.165", 212 | "resolved": "https://registry.npmjs.org/@cargo-messages/win32-x64-msvc/-/win32-x64-msvc-0.0.165.tgz", 213 | "integrity": "sha512-8aLHAdVWQdfFVMsQMIQHJHlFWmtLehYN/71xQo3x17NIY3eJjf+VejNLluKujRykjVx2/klc1KO2Jj1vzycifA==", 214 | "dev": true, 215 | "optional": true 216 | }, 217 | "@neon-rs/cli": { 218 | "version": "0.0.165", 219 | "resolved": "https://registry.npmjs.org/@neon-rs/cli/-/cli-0.0.165.tgz", 220 | "integrity": "sha512-jGIWtlQf6GzomurFnbM9txr26uY4MuPJEQaB+wP2jOKywrdYXzAed0NyjZuwR6RZJH+PphSKmG/U0Fhxw9XxTA==", 221 | "dev": true, 222 | "requires": { 223 | "@cargo-messages/android-arm-eabi": "0.0.165", 224 | "@cargo-messages/darwin-arm64": "0.0.165", 225 | "@cargo-messages/darwin-x64": "0.0.165", 226 | "@cargo-messages/linux-arm-gnueabihf": "0.0.165", 227 | "@cargo-messages/linux-x64-gnu": "0.0.165", 228 | "@cargo-messages/win32-arm64-msvc": "0.0.165", 229 | "@cargo-messages/win32-x64-msvc": "0.0.165" 230 | } 231 | }, 232 | "@neon-rs/load": { 233 | "version": "0.0.4", 234 | "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", 235 | "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==" 236 | }, 237 | "detect-libc": { 238 | "version": "2.0.2", 239 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", 240 | "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" 241 | }, 242 | "typescript": { 243 | "version": "5.4.5", 244 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", 245 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", 246 | "dev": true 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libsql", 3 | "version": "0.5.12", 4 | "description": "A better-sqlite3 compatible API for libSQL that supports Bun, Deno, and Node", 5 | "os": [ 6 | "darwin", 7 | "linux", 8 | "win32" 9 | ], 10 | "cpu": [ 11 | "x64", 12 | "arm64", 13 | "wasm32", 14 | "arm" 15 | ], 16 | "main": "index.js", 17 | "types": "types/index.d.ts", 18 | "files": [ 19 | "auth.js", 20 | "index.js", 21 | "sqlite-error.js", 22 | "promise.js", 23 | "types/index.d.ts", 24 | "types/promise.d.ts" 25 | ], 26 | "exports": { 27 | ".": { 28 | "types": "./types/index.d.ts", 29 | "default": "./index.js" 30 | }, 31 | "./promise": { 32 | "types": "./types/promise.d.ts", 33 | "default": "./promise.js" 34 | } 35 | }, 36 | "scripts": { 37 | "test": "cargo test", 38 | "debug": "cargo build --message-format=json | npm exec neon dist", 39 | "build": "npx tsc && cargo build --message-format=json --release | npm exec neon dist -- --name libsql-js", 40 | "cross": "cross build --message-format=json --release | npm exec neon dist -- --name libsql-js -m /target", 41 | "pack-build": "neon pack-build", 42 | "prepack": "neon install-builds", 43 | "postversion": "git push --follow-tags" 44 | }, 45 | "author": "Pekka Enberg ", 46 | "license": "MIT", 47 | "neon": { 48 | "targets": { 49 | "aarch64-apple-darwin": "@libsql/darwin-arm64", 50 | "aarch64-unknown-linux-gnu": "@libsql/linux-arm64-gnu", 51 | "aarch64-unknown-linux-musl": "@libsql/linux-arm64-musl", 52 | "x86_64-apple-darwin": "@libsql/darwin-x64", 53 | "x86_64-pc-windows-msvc": "@libsql/win32-x64-msvc", 54 | "x86_64-unknown-linux-gnu": "@libsql/linux-x64-gnu", 55 | "x86_64-unknown-linux-musl": "@libsql/linux-x64-musl", 56 | "arm-unknown-linux-gnueabihf": "@libsql/linux-arm-gnueabihf", 57 | "arm-unknown-linux-musleabihf": "@libsql/linux-arm-musleabihf" 58 | } 59 | }, 60 | "repository": { 61 | "type": "git", 62 | "url": "git+https://github.com/tursodatabase/libsql-js.git" 63 | }, 64 | "keywords": [ 65 | "libsql" 66 | ], 67 | "bugs": { 68 | "url": "https://github.com/tursodatabase/libsql-js/issues" 69 | }, 70 | "homepage": "https://github.com/tursodatabase/libsql-js", 71 | "devDependencies": { 72 | "@neon-rs/cli": "^0.0.165", 73 | "typescript": "^5.4.5" 74 | }, 75 | "dependencies": { 76 | "@neon-rs/load": "^0.0.4", 77 | "detect-libc": "2.0.2" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /perf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libsql-perf", 3 | "type": "module", 4 | "private": true, 5 | "dependencies": { 6 | "better-sqlite3": "^9.5.0", 7 | "libsql": "..", 8 | "mitata": "^0.1.11" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /perf/perf-better-sqlite3.js: -------------------------------------------------------------------------------- 1 | import { run, bench, group, baseline } from 'mitata'; 2 | 3 | import Database from 'better-sqlite3'; 4 | 5 | const db = new Database(':memory:'); 6 | 7 | db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"); 8 | db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); 9 | 10 | const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); 11 | 12 | group('Statement', () => { 13 | bench('get(1)', () => { 14 | stmt.get(1); 15 | }); 16 | }); 17 | 18 | await run({ 19 | units: false, // print small units cheatsheet 20 | silent: false, // enable/disable stdout output 21 | avg: true, // enable/disable avg column (default: true) 22 | json: false, // enable/disable json output (default: false) 23 | colors: true, // enable/disable colors (default: true) 24 | min_max: true, // enable/disable min/max column (default: true) 25 | percentiles: true, // enable/disable percentiles column (default: true) 26 | }); 27 | -------------------------------------------------------------------------------- /perf/perf-iterate-better-sqlite3.js: -------------------------------------------------------------------------------- 1 | import { run, bench, group, baseline } from 'mitata'; 2 | 3 | import Database from 'better-sqlite3'; 4 | 5 | const db = new Database(':memory:'); 6 | 7 | db.exec(`CREATE TABLE users ( 8 | field1 TEXT, 9 | field2 TEXT, 10 | field3 TEXT, 11 | field4 TEXT, 12 | field5 TEXT, 13 | field6 TEXT, 14 | field7 TEXT, 15 | field8 TEXT, 16 | field9 TEXT, 17 | field10 TEXT, 18 | field11 TEXT, 19 | field12 TEXT, 20 | field13 TEXT, 21 | field14 TEXT, 22 | field15 TEXT, 23 | field16 TEXT, 24 | field17 TEXT, 25 | field18 TEXT, 26 | field19 TEXT, 27 | field20 TEXT, 28 | field21 TEXT, 29 | field22 TEXT, 30 | field23 TEXT, 31 | field24 TEXT, 32 | field25 TEXT, 33 | field26 TEXT, 34 | field27 TEXT, 35 | field28 TEXT, 36 | field29 TEXT, 37 | field30 TEXT, 38 | field31 TEXT, 39 | field32 TEXT, 40 | field33 TEXT, 41 | field34 TEXT, 42 | field35 TEXT, 43 | field36 TEXT, 44 | field37 TEXT, 45 | field38 TEXT, 46 | field39 TEXT, 47 | field40 TEXT, 48 | field41 TEXT, 49 | field42 TEXT, 50 | field43 TEXT, 51 | field44 TEXT, 52 | field45 TEXT, 53 | field46 TEXT, 54 | field47 TEXT, 55 | field48 TEXT, 56 | field49 TEXT, 57 | field50 TEXT, 58 | field51 INTEGER, 59 | field52 INTEGER, 60 | field53 INTEGER, 61 | field54 INTEGER, 62 | field55 INTEGER, 63 | field56 INTEGER, 64 | field57 INTEGER, 65 | field58 INTEGER, 66 | field59 INTEGER, 67 | field60 INTEGER, 68 | field61 INTEGER, 69 | field62 INTEGER, 70 | field63 INTEGER, 71 | field64 INTEGER, 72 | field65 INTEGER, 73 | field66 INTEGER, 74 | field67 INTEGER, 75 | field68 INTEGER, 76 | field69 INTEGER, 77 | field70 INTEGER 78 | )`); 79 | for (let id = 0; id < 500; id++) { 80 | db.exec(`INSERT INTO users VALUES ( 81 | 'some string here', 82 | 'some string here', 83 | 'some string here', 84 | 'some string here', 85 | 'some string here', 86 | 'some string here', 87 | 'some string here', 88 | 'some string here', 89 | 'some string here', 90 | 'some string here', 91 | 'some string here', 92 | 'some string here', 93 | 'some string here', 94 | 'some string here', 95 | 'some string here', 96 | 'some string here', 97 | 'some string here', 98 | 'some string here', 99 | 'some string here', 100 | 'some string here', 101 | 'some string here', 102 | 'some string here', 103 | 'some string here', 104 | 'some string here', 105 | 'some string here', 106 | 'some string here', 107 | 'some string here', 108 | 'some string here', 109 | 'some string here', 110 | 'some string here', 111 | 'some string here', 112 | 'some string here', 113 | 'some string here', 114 | 'some string here', 115 | 'some string here', 116 | 'some string here', 117 | 'some string here', 118 | 'some string here', 119 | 'some string here', 120 | 'some string here', 121 | 'some string here', 122 | 'some string here', 123 | 'some string here', 124 | 'some string here', 125 | 'some string here', 126 | 'some string here', 127 | 'some string here', 128 | 'some string here', 129 | 'some string here', 130 | 'some string here', 131 | ${id}, 132 | ${id}, 133 | ${id}, 134 | ${id}, 135 | ${id}, 136 | ${id}, 137 | ${id}, 138 | ${id}, 139 | ${id}, 140 | ${id}, 141 | ${id}, 142 | ${id}, 143 | ${id}, 144 | ${id}, 145 | ${id}, 146 | ${id}, 147 | ${id}, 148 | ${id}, 149 | ${id}, 150 | ${id} 151 | )`); 152 | } 153 | 154 | const stmt = db.prepare("SELECT * FROM users WHERE field70 > ?"); 155 | 156 | group('Statement', () => { 157 | bench('iterate', () => { 158 | for (const row of stmt.iterate(10)) { 159 | if (row.field1 === 'Never appears') { 160 | break; 161 | } 162 | } 163 | }); 164 | }); 165 | 166 | await run({ 167 | units: false, // print small units cheatsheet 168 | silent: false, // enable/disable stdout output 169 | avg: true, // enable/disable avg column (default: true) 170 | json: false, // enable/disable json output (default: false) 171 | colors: true, // enable/disable colors (default: true) 172 | min_max: true, // enable/disable min/max column (default: true) 173 | percentiles: true, // enable/disable percentiles column (default: true) 174 | }); 175 | -------------------------------------------------------------------------------- /perf/perf-iterate-libsql.js: -------------------------------------------------------------------------------- 1 | import { run, bench, group, baseline } from 'mitata'; 2 | 3 | import Database from 'libsql'; 4 | 5 | const db = new Database(':memory:'); 6 | 7 | db.exec(`CREATE TABLE users ( 8 | field1 TEXT, 9 | field2 TEXT, 10 | field3 TEXT, 11 | field4 TEXT, 12 | field5 TEXT, 13 | field6 TEXT, 14 | field7 TEXT, 15 | field8 TEXT, 16 | field9 TEXT, 17 | field10 TEXT, 18 | field11 TEXT, 19 | field12 TEXT, 20 | field13 TEXT, 21 | field14 TEXT, 22 | field15 TEXT, 23 | field16 TEXT, 24 | field17 TEXT, 25 | field18 TEXT, 26 | field19 TEXT, 27 | field20 TEXT, 28 | field21 TEXT, 29 | field22 TEXT, 30 | field23 TEXT, 31 | field24 TEXT, 32 | field25 TEXT, 33 | field26 TEXT, 34 | field27 TEXT, 35 | field28 TEXT, 36 | field29 TEXT, 37 | field30 TEXT, 38 | field31 TEXT, 39 | field32 TEXT, 40 | field33 TEXT, 41 | field34 TEXT, 42 | field35 TEXT, 43 | field36 TEXT, 44 | field37 TEXT, 45 | field38 TEXT, 46 | field39 TEXT, 47 | field40 TEXT, 48 | field41 TEXT, 49 | field42 TEXT, 50 | field43 TEXT, 51 | field44 TEXT, 52 | field45 TEXT, 53 | field46 TEXT, 54 | field47 TEXT, 55 | field48 TEXT, 56 | field49 TEXT, 57 | field50 TEXT, 58 | field51 INTEGER, 59 | field52 INTEGER, 60 | field53 INTEGER, 61 | field54 INTEGER, 62 | field55 INTEGER, 63 | field56 INTEGER, 64 | field57 INTEGER, 65 | field58 INTEGER, 66 | field59 INTEGER, 67 | field60 INTEGER, 68 | field61 INTEGER, 69 | field62 INTEGER, 70 | field63 INTEGER, 71 | field64 INTEGER, 72 | field65 INTEGER, 73 | field66 INTEGER, 74 | field67 INTEGER, 75 | field68 INTEGER, 76 | field69 INTEGER, 77 | field70 INTEGER 78 | )`); 79 | for (let id = 0; id < 500; id++) { 80 | db.exec(`INSERT INTO users VALUES ( 81 | 'some string here', 82 | 'some string here', 83 | 'some string here', 84 | 'some string here', 85 | 'some string here', 86 | 'some string here', 87 | 'some string here', 88 | 'some string here', 89 | 'some string here', 90 | 'some string here', 91 | 'some string here', 92 | 'some string here', 93 | 'some string here', 94 | 'some string here', 95 | 'some string here', 96 | 'some string here', 97 | 'some string here', 98 | 'some string here', 99 | 'some string here', 100 | 'some string here', 101 | 'some string here', 102 | 'some string here', 103 | 'some string here', 104 | 'some string here', 105 | 'some string here', 106 | 'some string here', 107 | 'some string here', 108 | 'some string here', 109 | 'some string here', 110 | 'some string here', 111 | 'some string here', 112 | 'some string here', 113 | 'some string here', 114 | 'some string here', 115 | 'some string here', 116 | 'some string here', 117 | 'some string here', 118 | 'some string here', 119 | 'some string here', 120 | 'some string here', 121 | 'some string here', 122 | 'some string here', 123 | 'some string here', 124 | 'some string here', 125 | 'some string here', 126 | 'some string here', 127 | 'some string here', 128 | 'some string here', 129 | 'some string here', 130 | 'some string here', 131 | ${id}, 132 | ${id}, 133 | ${id}, 134 | ${id}, 135 | ${id}, 136 | ${id}, 137 | ${id}, 138 | ${id}, 139 | ${id}, 140 | ${id}, 141 | ${id}, 142 | ${id}, 143 | ${id}, 144 | ${id}, 145 | ${id}, 146 | ${id}, 147 | ${id}, 148 | ${id}, 149 | ${id}, 150 | ${id} 151 | )`); 152 | } 153 | 154 | const stmt = db.prepare("SELECT * FROM users WHERE field70 > ?"); 155 | 156 | group('Statement', () => { 157 | bench('iterate', () => { 158 | for (const row of stmt.iterate(10)) { 159 | if (row.field1 === 'Never appears') { 160 | break; 161 | } 162 | } 163 | }); 164 | }); 165 | 166 | await run({ 167 | units: false, // print small units cheatsheet 168 | silent: false, // enable/disable stdout output 169 | avg: true, // enable/disable avg column (default: true) 170 | json: false, // enable/disable json output (default: false) 171 | colors: true, // enable/disable colors (default: true) 172 | min_max: true, // enable/disable min/max column (default: true) 173 | percentiles: true, // enable/disable percentiles column (default: true) 174 | }); 175 | -------------------------------------------------------------------------------- /perf/perf-libsql.js: -------------------------------------------------------------------------------- 1 | import { run, bench, group, baseline } from 'mitata'; 2 | 3 | import Database from 'libsql'; 4 | 5 | const db = new Database(':memory:'); 6 | 7 | db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"); 8 | db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); 9 | 10 | const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); 11 | 12 | group('Statement', () => { 13 | bench('get(1)', () => { 14 | stmt.get(1); 15 | }); 16 | }); 17 | 18 | await run({ 19 | units: false, // print small units cheatsheet 20 | silent: false, // enable/disable stdout output 21 | avg: true, // enable/disable avg column (default: true) 22 | json: false, // enable/disable json output (default: false) 23 | colors: true, // enable/disable colors (default: true) 24 | min_max: true, // enable/disable min/max column (default: true) 25 | percentiles: true, // enable/disable percentiles column (default: true) 26 | }); 27 | -------------------------------------------------------------------------------- /promise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { load, currentTarget } = require("@neon-rs/load"); 4 | const { familySync, GLIBC, MUSL } = require("detect-libc"); 5 | 6 | // Static requires for bundlers. 7 | if (0) { 8 | require("./.targets"); 9 | } 10 | 11 | const Authorization = require("./auth"); 12 | const SqliteError = require("./sqlite-error"); 13 | 14 | function convertError(err) { 15 | if (err.libsqlError) { 16 | return new SqliteError(err.message, err.code, err.rawCode); 17 | } 18 | return err; 19 | } 20 | 21 | function requireNative() { 22 | if (process.env.LIBSQL_JS_DEV) { 23 | return load(__dirname) 24 | } 25 | let target = currentTarget(); 26 | // Workaround for Bun, which reports a musl target, but really wants glibc... 27 | if (familySync() == GLIBC) { 28 | switch (target) { 29 | case "linux-x64-musl": 30 | target = "linux-x64-gnu"; 31 | break; 32 | case "linux-arm64-musl": 33 | target = "linux-arm64-gnu"; 34 | break; 35 | } 36 | } 37 | // @neon-rs/load doesn't detect arm musl 38 | if (target === "linux-arm-gnueabihf" && familySync() == MUSL) { 39 | target = "linux-arm-musleabihf"; 40 | } 41 | return require(`@libsql/${target}`); 42 | } 43 | 44 | const { 45 | databaseOpen, 46 | databaseOpenWithSync, 47 | databaseInTransaction, 48 | databaseInterrupt, 49 | databaseClose, 50 | databaseSyncAsync, 51 | databaseSyncUntilAsync, 52 | databaseExecAsync, 53 | databasePrepareAsync, 54 | databaseMaxWriteReplicationIndex, 55 | databaseDefaultSafeIntegers, 56 | databaseAuthorizer, 57 | databaseLoadExtension, 58 | statementRaw, 59 | statementIsReader, 60 | statementGet, 61 | statementRun, 62 | statementInterrupt, 63 | statementRowsAsync, 64 | statementColumns, 65 | statementSafeIntegers, 66 | rowsNext, 67 | } = requireNative(); 68 | 69 | /** 70 | * Database represents a connection that can prepare and execute SQL statements. 71 | */ 72 | class Database { 73 | /** 74 | * Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created. 75 | * 76 | * @constructor 77 | * @param {string} path - Path to the database file. 78 | */ 79 | constructor(path, opts) { 80 | const encryptionCipher = opts?.encryptionCipher ?? "aes256cbc"; 81 | if (opts && opts.syncUrl) { 82 | var authToken = ""; 83 | if (opts.syncAuth) { 84 | console.warn("Warning: The `syncAuth` option is deprecated, please use `authToken` option instead."); 85 | authToken = opts.syncAuth; 86 | } else if (opts.authToken) { 87 | authToken = opts.authToken; 88 | } 89 | const encryptionKey = opts?.encryptionKey ?? ""; 90 | const syncPeriod = opts?.syncPeriod ?? 0.0; 91 | const offline = opts?.offline ?? false; 92 | this.db = databaseOpenWithSync(path, opts.syncUrl, authToken, encryptionCipher, encryptionKey, syncPeriod, offline); 93 | } else { 94 | const authToken = opts?.authToken ?? ""; 95 | const encryptionKey = opts?.encryptionKey ?? ""; 96 | const timeout = opts?.timeout ?? 0.0; 97 | this.db = databaseOpen(path, authToken, encryptionCipher, encryptionKey, timeout); 98 | } 99 | // TODO: Use a libSQL API for this? 100 | this.memory = path === ":memory:"; 101 | this.readonly = false; 102 | this.name = ""; 103 | this.open = true; 104 | 105 | const db = this.db; 106 | Object.defineProperties(this, { 107 | inTransaction: { 108 | get() { 109 | return databaseInTransaction(db); 110 | } 111 | }, 112 | }); 113 | } 114 | 115 | sync() { 116 | return databaseSyncAsync.call(this.db); 117 | } 118 | 119 | syncUntil(replicationIndex) { 120 | return databaseSyncUntilAsync.call(this.db, replicationIndex); 121 | } 122 | 123 | /** 124 | * Prepares a SQL statement for execution. 125 | * 126 | * @param {string} sql - The SQL statement string to prepare. 127 | */ 128 | prepare(sql) { 129 | return databasePrepareAsync.call(this.db, sql).then((stmt) => { 130 | return new Statement(stmt); 131 | }).catch((err) => { 132 | throw convertError(err); 133 | }); 134 | } 135 | 136 | /** 137 | * Returns a function that executes the given function in a transaction. 138 | * 139 | * @param {function} fn - The function to wrap in a transaction. 140 | */ 141 | transaction(fn) { 142 | if (typeof fn !== "function") 143 | throw new TypeError("Expected first argument to be a function"); 144 | 145 | const db = this; 146 | const wrapTxn = (mode) => { 147 | return async (...bindParameters) => { 148 | await db.exec("BEGIN " + mode); 149 | try { 150 | const result = fn(...bindParameters); 151 | await db.exec("COMMIT"); 152 | return result; 153 | } catch (err) { 154 | await db.exec("ROLLBACK"); 155 | throw err; 156 | } 157 | }; 158 | }; 159 | const properties = { 160 | default: { value: wrapTxn("") }, 161 | deferred: { value: wrapTxn("DEFERRED") }, 162 | immediate: { value: wrapTxn("IMMEDIATE") }, 163 | exclusive: { value: wrapTxn("EXCLUSIVE") }, 164 | database: { value: this, enumerable: true }, 165 | }; 166 | Object.defineProperties(properties.default.value, properties); 167 | Object.defineProperties(properties.deferred.value, properties); 168 | Object.defineProperties(properties.immediate.value, properties); 169 | Object.defineProperties(properties.exclusive.value, properties); 170 | return properties.default.value; 171 | } 172 | 173 | pragma(source, options) { 174 | if (options == null) options = {}; 175 | if (typeof source !== 'string') throw new TypeError('Expected first argument to be a string'); 176 | if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object'); 177 | const simple = options['simple']; 178 | return this.prepare(`PRAGMA ${source}`, this, true).then(async (stmt) => { 179 | return simple ? await stmt.pluck().get() : await stmt.all(); 180 | }); 181 | } 182 | 183 | backup(filename, options) { 184 | throw new Error("not implemented"); 185 | } 186 | 187 | serialize(options) { 188 | throw new Error("not implemented"); 189 | } 190 | 191 | function(name, options, fn) { 192 | // Apply defaults 193 | if (options == null) options = {}; 194 | if (typeof options === "function") { 195 | fn = options; 196 | options = {}; 197 | } 198 | 199 | // Validate arguments 200 | if (typeof name !== "string") 201 | throw new TypeError("Expected first argument to be a string"); 202 | if (typeof fn !== "function") 203 | throw new TypeError("Expected last argument to be a function"); 204 | if (typeof options !== "object") 205 | throw new TypeError("Expected second argument to be an options object"); 206 | if (!name) 207 | throw new TypeError( 208 | "User-defined function name cannot be an empty string" 209 | ); 210 | 211 | throw new Error("not implemented"); 212 | } 213 | 214 | aggregate(name, options) { 215 | // Validate arguments 216 | if (typeof name !== "string") 217 | throw new TypeError("Expected first argument to be a string"); 218 | if (typeof options !== "object" || options === null) 219 | throw new TypeError("Expected second argument to be an options object"); 220 | if (!name) 221 | throw new TypeError( 222 | "User-defined function name cannot be an empty string" 223 | ); 224 | 225 | throw new Error("not implemented"); 226 | } 227 | 228 | table(name, factory) { 229 | // Validate arguments 230 | if (typeof name !== "string") 231 | throw new TypeError("Expected first argument to be a string"); 232 | if (!name) 233 | throw new TypeError( 234 | "Virtual table module name cannot be an empty string" 235 | ); 236 | 237 | throw new Error("not implemented"); 238 | } 239 | 240 | authorizer(rules) { 241 | databaseAuthorizer.call(this.db, rules); 242 | } 243 | 244 | loadExtension(...args) { 245 | databaseLoadExtension.call(this.db, ...args); 246 | } 247 | 248 | maxWriteReplicationIndex() { 249 | return databaseMaxWriteReplicationIndex.call(this.db) 250 | } 251 | 252 | /** 253 | * Executes a SQL statement. 254 | * 255 | * @param {string} sql - The SQL statement string to execute. 256 | */ 257 | exec(sql) { 258 | return databaseExecAsync.call(this.db, sql).catch((err) => { 259 | throw convertError(err); 260 | }); 261 | } 262 | 263 | /** 264 | * Interrupts the database connection. 265 | */ 266 | interrupt() { 267 | databaseInterrupt.call(this.db); 268 | } 269 | 270 | /** 271 | * Closes the database connection. 272 | */ 273 | close() { 274 | databaseClose.call(this.db); 275 | } 276 | 277 | /** 278 | * Toggle 64-bit integer support. 279 | */ 280 | defaultSafeIntegers(toggle) { 281 | databaseDefaultSafeIntegers.call(this.db, toggle ?? true); 282 | return this; 283 | } 284 | 285 | unsafeMode(...args) { 286 | throw new Error("not implemented"); 287 | } 288 | } 289 | 290 | /** 291 | * Statement represents a prepared SQL statement that can be executed. 292 | */ 293 | class Statement { 294 | constructor(stmt) { 295 | this.stmt = stmt; 296 | this.pluckMode = false; 297 | } 298 | 299 | /** 300 | * Toggle raw mode. 301 | * 302 | * @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled. 303 | */ 304 | raw(raw) { 305 | statementRaw.call(this.stmt, raw ?? true); 306 | return this; 307 | } 308 | 309 | /** 310 | * Toggle pluck mode. 311 | * 312 | * @param pluckMode Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled. 313 | */ 314 | pluck(pluckMode) { 315 | this.pluckMode = pluckMode ?? true; 316 | return this; 317 | } 318 | 319 | get reader() { 320 | return statementIsReader.call(this.stmt); 321 | } 322 | 323 | /** 324 | * Executes the SQL statement and returns an info object. 325 | */ 326 | run(...bindParameters) { 327 | try { 328 | if (bindParameters.length == 1 && typeof bindParameters[0] === "object") { 329 | return statementRun.call(this.stmt, bindParameters[0]); 330 | } else { 331 | return statementRun.call(this.stmt, bindParameters.flat()); 332 | } 333 | } catch (err) { 334 | throw convertError(err); 335 | } 336 | } 337 | 338 | /** 339 | * Executes the SQL statement and returns the first row. 340 | * 341 | * @param bindParameters - The bind parameters for executing the statement. 342 | */ 343 | get(...bindParameters) { 344 | try { 345 | if (bindParameters.length == 1 && typeof bindParameters[0] === "object") { 346 | return statementGet.call(this.stmt, bindParameters[0]); 347 | } else { 348 | return statementGet.call(this.stmt, bindParameters.flat()); 349 | } 350 | } catch (e) { 351 | throw convertError(e); 352 | } 353 | } 354 | 355 | /** 356 | * Executes the SQL statement and returns an iterator to the resulting rows. 357 | * 358 | * @param bindParameters - The bind parameters for executing the statement. 359 | */ 360 | async iterate(...bindParameters) { 361 | var rows = undefined; 362 | if (bindParameters.length == 1 && typeof bindParameters[0] === "object") { 363 | rows = await statementRowsAsync.call(this.stmt, bindParameters[0]); 364 | } else { 365 | rows = await statementRowsAsync.call(this.stmt, bindParameters.flat()); 366 | } 367 | const iter = { 368 | nextRows: Array(100), 369 | nextRowIndex: 100, 370 | next() { 371 | try { 372 | if (this.nextRowIndex === 100) { 373 | this.nextRows.fill(null); 374 | rowsNext.call(rows, this.nextRows); 375 | this.nextRowIndex = 0; 376 | } 377 | const row = this.nextRows[this.nextRowIndex]; 378 | this.nextRows[this.nextRowIndex] = null; 379 | if (!row) { 380 | return { done: true }; 381 | } 382 | this.nextRowIndex++; 383 | return { value: row, done: false }; 384 | } catch (e) { 385 | throw convertError(e); 386 | } 387 | }, 388 | [Symbol.iterator]() { 389 | return this; 390 | }, 391 | }; 392 | return iter; 393 | } 394 | 395 | /** 396 | * Executes the SQL statement and returns an array of the resulting rows. 397 | * 398 | * @param bindParameters - The bind parameters for executing the statement. 399 | */ 400 | async all(...bindParameters) { 401 | try { 402 | const result = []; 403 | const it = await this.iterate(...bindParameters); 404 | for (const row of it) { 405 | if (this.pluckMode) { 406 | result.push(row[Object.keys(row)[0]]); 407 | } else { 408 | result.push(row); 409 | } 410 | } 411 | return result; 412 | } catch (e) { 413 | throw convertError(e); 414 | } 415 | } 416 | 417 | /** 418 | * Interrupts the statement. 419 | */ 420 | interrupt() { 421 | statementInterrupt.call(this.stmt); 422 | } 423 | 424 | /** 425 | * Returns the columns in the result set returned by this prepared statement. 426 | */ 427 | columns() { 428 | return statementColumns.call(this.stmt); 429 | } 430 | 431 | /** 432 | * Toggle 64-bit integer support. 433 | */ 434 | safeIntegers(toggle) { 435 | statementSafeIntegers.call(this.stmt, toggle ?? true); 436 | return this; 437 | } 438 | 439 | } 440 | 441 | module.exports = Database; 442 | module.exports.Authorization = Authorization; 443 | module.exports.SqliteError = SqliteError; 444 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.78.0" 3 | -------------------------------------------------------------------------------- /sqlite-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const descriptor = { value: 'SqliteError', writable: true, enumerable: false, configurable: true }; 3 | 4 | function SqliteError(message, code, rawCode) { 5 | if (new.target !== SqliteError) { 6 | return new SqliteError(message, code); 7 | } 8 | if (typeof code !== 'string') { 9 | throw new TypeError('Expected second argument to be a string'); 10 | } 11 | Error.call(this, message); 12 | descriptor.value = '' + message; 13 | Object.defineProperty(this, 'message', descriptor); 14 | Error.captureStackTrace(this, SqliteError); 15 | this.code = code; 16 | this.rawCode = rawCode 17 | } 18 | Object.setPrototypeOf(SqliteError, Error); 19 | Object.setPrototypeOf(SqliteError.prototype, Error.prototype); 20 | Object.defineProperty(SqliteError.prototype, 'name', descriptor); 21 | module.exports = SqliteError; 22 | -------------------------------------------------------------------------------- /src/auth.rs: -------------------------------------------------------------------------------- 1 | use tracing::trace; 2 | 3 | use std::collections::HashSet; 4 | 5 | pub struct AuthorizerBuilder { 6 | allow_list: HashSet, 7 | deny_list: HashSet, 8 | } 9 | 10 | impl AuthorizerBuilder { 11 | pub fn new() -> Self { 12 | Self { 13 | allow_list: HashSet::new(), 14 | deny_list: HashSet::new(), 15 | } 16 | } 17 | 18 | pub fn allow(&mut self, table: &str) -> &mut Self { 19 | self.allow_list.insert(table.to_string()); 20 | self 21 | } 22 | 23 | pub fn deny(&mut self, table: &str) -> &mut Self { 24 | self.deny_list.insert(table.to_string()); 25 | self 26 | } 27 | 28 | pub fn build(self) -> Authorizer { 29 | Authorizer::new(self.allow_list, self.deny_list) 30 | } 31 | } 32 | 33 | pub struct Authorizer { 34 | allow_list: HashSet, 35 | deny_list: HashSet, 36 | } 37 | 38 | impl Authorizer { 39 | pub fn new(allow_list: HashSet, deny_list: HashSet) -> Self { 40 | Self { 41 | allow_list, 42 | deny_list, 43 | } 44 | } 45 | 46 | pub fn authorize(&self, ctx: &libsql::AuthContext) -> libsql::Authorization { 47 | use libsql::AuthAction; 48 | let ret = match ctx.action { 49 | AuthAction::Unknown { .. } => libsql::Authorization::Deny, 50 | AuthAction::CreateIndex { table_name, .. } => self.authorize_table(table_name), 51 | AuthAction::CreateTable { table_name, .. } => self.authorize_table(table_name), 52 | AuthAction::CreateTempIndex { table_name, .. } => self.authorize_table(table_name), 53 | AuthAction::CreateTempTable { table_name, .. } => self.authorize_table(table_name), 54 | AuthAction::CreateTempTrigger { table_name, .. } => self.authorize_table(table_name), 55 | AuthAction::CreateTempView { .. } => libsql::Authorization::Deny, 56 | AuthAction::CreateTrigger { table_name, .. } => self.authorize_table(table_name), 57 | AuthAction::CreateView { .. } => libsql::Authorization::Deny, 58 | AuthAction::Delete { table_name, .. } => self.authorize_table(table_name), 59 | AuthAction::DropIndex { table_name, .. } => self.authorize_table(table_name), 60 | AuthAction::DropTable { table_name, .. } => self.authorize_table(table_name), 61 | AuthAction::DropTempIndex { table_name, .. } => self.authorize_table(table_name), 62 | AuthAction::DropTempTable { table_name, .. } => self.authorize_table(table_name), 63 | AuthAction::DropTempTrigger { table_name, .. } => self.authorize_table(table_name), 64 | AuthAction::DropTempView { .. } => libsql::Authorization::Deny, 65 | AuthAction::DropTrigger { .. } => libsql::Authorization::Deny, 66 | AuthAction::DropView { .. } => libsql::Authorization::Deny, 67 | AuthAction::Insert { table_name, .. } => self.authorize_table(table_name), 68 | AuthAction::Pragma { .. } => libsql::Authorization::Deny, 69 | AuthAction::Read { table_name, .. } => self.authorize_table(table_name), 70 | AuthAction::Select { .. } => libsql::Authorization::Allow, 71 | AuthAction::Transaction { .. } => libsql::Authorization::Deny, 72 | AuthAction::Update { table_name, .. } => self.authorize_table(table_name), 73 | AuthAction::Attach { .. } => libsql::Authorization::Deny, 74 | AuthAction::Detach { .. } => libsql::Authorization::Deny, 75 | AuthAction::AlterTable { table_name, .. } => self.authorize_table(table_name), 76 | AuthAction::Reindex { .. } => libsql::Authorization::Deny, 77 | AuthAction::Analyze { .. } => libsql::Authorization::Deny, 78 | AuthAction::CreateVtable { .. } => libsql::Authorization::Deny, 79 | AuthAction::DropVtable { .. } => libsql::Authorization::Deny, 80 | AuthAction::Function { .. } => libsql::Authorization::Deny, 81 | AuthAction::Savepoint { .. } => libsql::Authorization::Deny, 82 | AuthAction::Recursive { .. } => libsql::Authorization::Deny, 83 | }; 84 | trace!("authorize(ctx = {:?}) -> {:?}", ctx, ret); 85 | ret 86 | } 87 | 88 | fn authorize_table(&self, table: &str) -> libsql::Authorization { 89 | if self.deny_list.contains(table) { 90 | return libsql::Authorization::Deny; 91 | } 92 | if self.allow_list.contains(table) { 93 | return libsql::Authorization::Allow; 94 | } 95 | libsql::Authorization::Deny 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | use libsql::replication::Replicated; 2 | use neon::prelude::*; 3 | use std::cell::RefCell; 4 | use std::str::FromStr; 5 | use std::sync::Arc; 6 | use std::time::Duration; 7 | use tokio::sync::Mutex; 8 | use tracing::trace; 9 | 10 | use crate::auth::AuthorizerBuilder; 11 | use crate::errors::{throw_database_closed_error, throw_libsql_error}; 12 | use crate::runtime; 13 | use crate::Statement; 14 | 15 | pub(crate) struct Database { 16 | db: Arc>, 17 | conn: RefCell>>>, 18 | default_safe_integers: RefCell, 19 | } 20 | 21 | impl Finalize for Database {} 22 | 23 | impl Database { 24 | pub fn new(db: libsql::Database, conn: libsql::Connection) -> Self { 25 | Database { 26 | db: Arc::new(Mutex::new(db)), 27 | conn: RefCell::new(Some(Arc::new(Mutex::new(conn)))), 28 | default_safe_integers: RefCell::new(false), 29 | } 30 | } 31 | 32 | pub fn js_open(mut cx: FunctionContext) -> JsResult> { 33 | let rt = runtime(&mut cx)?; 34 | let db_path = cx.argument::(0)?.value(&mut cx); 35 | let auth_token = cx.argument::(1)?.value(&mut cx); 36 | let encryption_cipher = cx.argument::(2)?.value(&mut cx); 37 | let encryption_key = cx.argument::(3)?.value(&mut cx); 38 | let busy_timeout = cx.argument::(4)?.value(&mut cx); 39 | let db = if is_remote_path(&db_path) { 40 | let version = version("remote"); 41 | trace!("Opening remote database: {}", db_path); 42 | libsql::Database::open_remote_internal(db_path.clone(), auth_token, version) 43 | } else { 44 | let cipher = libsql::Cipher::from_str(&encryption_cipher).or_else(|err| { 45 | throw_libsql_error( 46 | &mut cx, 47 | libsql::Error::SqliteFailure(err.extended_code, "".into()), 48 | ) 49 | })?; 50 | let mut builder = libsql::Builder::new_local(&db_path); 51 | if !encryption_key.is_empty() { 52 | let encryption_config = 53 | libsql::EncryptionConfig::new(cipher, encryption_key.into()); 54 | builder = builder.encryption_config(encryption_config); 55 | } 56 | rt.block_on(builder.build()) 57 | } 58 | .or_else(|err| throw_libsql_error(&mut cx, err))?; 59 | let conn = db 60 | .connect() 61 | .or_else(|err| throw_libsql_error(&mut cx, err))?; 62 | if busy_timeout > 0.0 { 63 | conn.busy_timeout(Duration::from_millis(busy_timeout as u64)) 64 | .or_else(|err| throw_libsql_error(&mut cx, err))?; 65 | } 66 | let db = Database::new(db, conn); 67 | Ok(cx.boxed(db)) 68 | } 69 | 70 | pub fn js_open_with_sync(mut cx: FunctionContext) -> JsResult> { 71 | let db_path = cx.argument::(0)?.value(&mut cx); 72 | let sync_url = cx.argument::(1)?.value(&mut cx); 73 | let sync_auth = cx.argument::(2)?.value(&mut cx); 74 | let encryption_cipher = cx.argument::(3)?.value(&mut cx); 75 | let encryption_key = cx.argument::(4)?.value(&mut cx); 76 | let sync_period = cx.argument::(5)?.value(&mut cx); 77 | let read_your_writes = cx.argument::(6)?.value(&mut cx); 78 | let offline = cx.argument::(7)?.value(&mut cx); 79 | 80 | let cipher = libsql::Cipher::from_str(&encryption_cipher).or_else(|err| { 81 | throw_libsql_error( 82 | &mut cx, 83 | libsql::Error::SqliteFailure(err.extended_code, "".into()), 84 | ) 85 | })?; 86 | let encryption_config = if encryption_key.is_empty() { 87 | None 88 | } else { 89 | Some(libsql::EncryptionConfig::new(cipher, encryption_key.into())) 90 | }; 91 | 92 | let sync_period = if sync_period > 0.0 { 93 | Some(Duration::from_secs_f64(sync_period)) 94 | } else { 95 | None 96 | }; 97 | let version = version("rpc"); 98 | 99 | trace!( 100 | "Opening local database with sync: database = {}, URL = {}", 101 | db_path, 102 | sync_url 103 | ); 104 | let rt = runtime(&mut cx)?; 105 | let result = if offline { 106 | rt.block_on(libsql::Builder::new_synced_database(db_path, sync_url, sync_auth).build()) 107 | } else { 108 | rt.block_on(async { 109 | let mut builder = libsql::Builder::new_remote_replica(db_path, sync_url, sync_auth); 110 | if let Some(encryption_config) = encryption_config { 111 | builder = builder.encryption_config(encryption_config); 112 | } 113 | if let Some(sync_period) = sync_period { 114 | builder = builder.sync_interval(sync_period); 115 | } 116 | builder.build().await 117 | }) 118 | }; 119 | let db = result.or_else(|err| cx.throw_error(err.to_string()))?; 120 | let conn = db 121 | .connect() 122 | .or_else(|err| throw_libsql_error(&mut cx, err))?; 123 | let db = Database::new(db, conn); 124 | Ok(cx.boxed(db)) 125 | } 126 | 127 | pub fn js_in_transaction(mut cx: FunctionContext) -> JsResult { 128 | let db = cx.argument::>(0)?; 129 | let conn = db.conn.borrow(); 130 | let conn = conn.as_ref().unwrap().clone(); 131 | let result = !conn.blocking_lock().is_autocommit(); 132 | Ok(cx.boolean(result).upcast()) 133 | } 134 | 135 | pub fn js_interrupt(mut cx: FunctionContext) -> JsResult { 136 | let db: Handle<'_, JsBox> = cx.this()?; 137 | let conn = db.conn.borrow(); 138 | let conn = conn.as_ref().unwrap().clone(); 139 | conn.blocking_lock().interrupt().or_else(|err| { 140 | throw_libsql_error(&mut cx, err)?; 141 | Ok(()) 142 | })?; 143 | Ok(cx.undefined()) 144 | } 145 | 146 | pub fn js_close(mut cx: FunctionContext) -> JsResult { 147 | // the conn will be closed when the last statement in discarded. In most situation that 148 | // means immediately because you don't want to hold on a statement for longer that its 149 | // database is alive. 150 | trace!("Closing database"); 151 | let db: Handle<'_, JsBox> = cx.this()?; 152 | db.conn.replace(None); 153 | Ok(cx.undefined()) 154 | } 155 | 156 | pub fn js_max_write_replication_index(mut cx: FunctionContext) -> JsResult { 157 | let db: Handle<'_, JsBox> = cx.this()?; 158 | let replication_index = db.db.blocking_lock().max_write_replication_index(); 159 | Ok(if let Some(ri) = replication_index { 160 | cx.number(ri as f64).upcast() 161 | } else { 162 | cx.undefined().upcast() 163 | }) 164 | } 165 | 166 | pub fn js_sync_sync(mut cx: FunctionContext) -> JsResult { 167 | trace!("Synchronizing database (sync)"); 168 | let db: Handle<'_, JsBox> = cx.this()?; 169 | let db = db.db.clone(); 170 | let rt = runtime(&mut cx)?; 171 | let rep = rt 172 | .block_on(async move { 173 | let db = db.lock().await; 174 | db.sync().await 175 | }) 176 | .or_else(|err| throw_libsql_error(&mut cx, err))?; 177 | 178 | let obj = convert_replicated_to_object(&mut cx, &rep)?; 179 | 180 | Ok(obj) 181 | } 182 | 183 | pub fn js_sync_async(mut cx: FunctionContext) -> JsResult { 184 | trace!("Synchronizing database (async)"); 185 | let db: Handle<'_, JsBox> = cx.this()?; 186 | let (deferred, promise) = cx.promise(); 187 | let channel = cx.channel(); 188 | let db = db.db.clone(); 189 | let rt = runtime(&mut cx)?; 190 | rt.spawn(async move { 191 | let result = db.lock().await.sync().await; 192 | match result { 193 | Ok(rep) => { 194 | deferred.settle_with(&channel, move |mut cx| { 195 | convert_replicated_to_object(&mut cx, &rep) 196 | }); 197 | } 198 | Err(err) => { 199 | deferred.settle_with(&channel, |mut cx| { 200 | throw_libsql_error(&mut cx, err)?; 201 | Ok(cx.undefined()) 202 | }); 203 | } 204 | } 205 | }); 206 | Ok(promise) 207 | } 208 | 209 | pub fn js_sync_until_sync(mut cx: FunctionContext) -> JsResult { 210 | trace!("Synchronizing database until given replication index (sync)"); 211 | let db: Handle<'_, JsBox> = cx.this()?; 212 | let db = db.db.clone(); 213 | let replication_index = cx.argument::(0)?.value(&mut cx) as u64; 214 | let rt = runtime(&mut cx)?; 215 | let rep = rt 216 | .block_on(async move { 217 | let db = db.lock().await; 218 | db.sync_until(replication_index).await 219 | }) 220 | .or_else(|err| throw_libsql_error(&mut cx, err))?; 221 | 222 | let obj = convert_replicated_to_object(&mut cx, &rep)?; 223 | 224 | Ok(obj) 225 | } 226 | 227 | pub fn js_sync_until_async(mut cx: FunctionContext) -> JsResult { 228 | trace!("Synchronizing database until given replication index (async)"); 229 | let db: Handle<'_, JsBox> = cx.this()?; 230 | let replication_index = cx.argument::(0)?.value(&mut cx) as u64; 231 | let (deferred, promise) = cx.promise(); 232 | let channel = cx.channel(); 233 | let db = db.db.clone(); 234 | let rt = runtime(&mut cx)?; 235 | rt.spawn(async move { 236 | let result = db.lock().await.sync_until(replication_index).await; 237 | match result { 238 | Ok(rep) => { 239 | deferred.settle_with(&channel, move |mut cx| { 240 | convert_replicated_to_object(&mut cx, &rep) 241 | }); 242 | } 243 | Err(err) => { 244 | deferred.settle_with(&channel, |mut cx| { 245 | throw_libsql_error(&mut cx, err)?; 246 | Ok(cx.undefined()) 247 | }); 248 | } 249 | } 250 | }); 251 | Ok(promise) 252 | } 253 | 254 | pub fn js_exec_sync(mut cx: FunctionContext) -> JsResult { 255 | let db: Handle<'_, JsBox> = cx.this()?; 256 | let sql = cx.argument::(0)?.value(&mut cx); 257 | trace!("Executing SQL statement (sync): {}", sql); 258 | let conn = match db.get_conn(&mut cx) { 259 | Some(conn) => conn, 260 | None => throw_database_closed_error(&mut cx)?, 261 | }; 262 | let rt = runtime(&mut cx)?; 263 | let result = rt.block_on(async { conn.lock().await.execute_batch(&sql).await }); 264 | result.or_else(|err| throw_libsql_error(&mut cx, err))?; 265 | Ok(cx.undefined()) 266 | } 267 | 268 | pub fn js_exec_async(mut cx: FunctionContext) -> JsResult { 269 | let db: Handle<'_, JsBox> = cx.this()?; 270 | let sql = cx.argument::(0)?.value(&mut cx); 271 | trace!("Executing SQL statement (async): {}", sql); 272 | let (deferred, promise) = cx.promise(); 273 | let channel = cx.channel(); 274 | let conn = match db.get_conn(&mut cx) { 275 | Some(conn) => conn, 276 | None => { 277 | deferred.settle_with(&channel, |mut cx| { 278 | throw_database_closed_error(&mut cx)?; 279 | Ok(cx.undefined()) 280 | }); 281 | return Ok(promise); 282 | } 283 | }; 284 | let rt = runtime(&mut cx)?; 285 | rt.spawn(async move { 286 | match conn.lock().await.execute_batch(&sql).await { 287 | Ok(_) => { 288 | deferred.settle_with(&channel, |mut cx| Ok(cx.undefined())); 289 | } 290 | Err(err) => { 291 | deferred.settle_with(&channel, |mut cx| { 292 | throw_libsql_error(&mut cx, err)?; 293 | Ok(cx.undefined()) 294 | }); 295 | } 296 | } 297 | }); 298 | Ok(promise) 299 | } 300 | 301 | pub fn js_prepare_sync(mut cx: FunctionContext) -> JsResult> { 302 | let db: Handle<'_, JsBox> = cx.this()?; 303 | let sql = cx.argument::(0)?.value(&mut cx); 304 | trace!("Preparing SQL statement (sync): {}", sql); 305 | let conn = match db.get_conn(&mut cx) { 306 | Some(conn) => conn, 307 | None => throw_database_closed_error(&mut cx)?, 308 | }; 309 | let rt = runtime(&mut cx)?; 310 | let result = rt.block_on(async { conn.lock().await.prepare(&sql).await }); 311 | let stmt = result.or_else(|err| throw_libsql_error(&mut cx, err))?; 312 | let stmt = Arc::new(Mutex::new(stmt)); 313 | let stmt = Statement { 314 | conn: conn.clone(), 315 | stmt, 316 | raw: RefCell::new(false), 317 | safe_ints: RefCell::new(*db.default_safe_integers.borrow()), 318 | }; 319 | Ok(cx.boxed(stmt)) 320 | } 321 | 322 | pub fn js_prepare_async(mut cx: FunctionContext) -> JsResult { 323 | let db: Handle<'_, JsBox> = cx.this()?; 324 | let sql = cx.argument::(0)?.value(&mut cx); 325 | trace!("Preparing SQL statement (async): {}", sql); 326 | let (deferred, promise) = cx.promise(); 327 | let channel = cx.channel(); 328 | let safe_ints = *db.default_safe_integers.borrow(); 329 | let rt = runtime(&mut cx)?; 330 | let conn = match db.get_conn(&mut cx) { 331 | Some(conn) => conn, 332 | None => { 333 | deferred.settle_with(&channel, |mut cx| { 334 | throw_database_closed_error(&mut cx)?; 335 | Ok(cx.undefined()) 336 | }); 337 | return Ok(promise); 338 | } 339 | }; 340 | rt.spawn(async move { 341 | match conn.lock().await.prepare(&sql).await { 342 | Ok(stmt) => { 343 | let stmt = Arc::new(Mutex::new(stmt)); 344 | let stmt = Statement { 345 | conn: conn.clone(), 346 | stmt, 347 | raw: RefCell::new(false), 348 | safe_ints: RefCell::new(safe_ints), 349 | }; 350 | deferred.settle_with(&channel, |mut cx| Ok(cx.boxed(stmt))); 351 | } 352 | Err(err) => { 353 | deferred.settle_with(&channel, |mut cx| { 354 | throw_libsql_error(&mut cx, err)?; 355 | Ok(cx.undefined()) 356 | }); 357 | } 358 | } 359 | }); 360 | Ok(promise) 361 | } 362 | 363 | pub fn js_default_safe_integers(mut cx: FunctionContext) -> JsResult { 364 | let db: Handle<'_, JsBox> = cx.this()?; 365 | let toggle = cx.argument::(0)?; 366 | let toggle = toggle.value(&mut cx); 367 | db.set_default_safe_integers(toggle); 368 | Ok(cx.null()) 369 | } 370 | 371 | pub fn set_default_safe_integers(&self, toggle: bool) { 372 | self.default_safe_integers.replace(toggle); 373 | } 374 | 375 | pub fn js_authorizer(mut cx: FunctionContext) -> JsResult { 376 | let db: Handle<'_, JsBox> = cx.this()?; 377 | let rules_obj = cx.argument::(0)?; 378 | let conn = match db.get_conn(&mut cx) { 379 | Some(conn) => conn, 380 | None => throw_database_closed_error(&mut cx)?, 381 | }; 382 | let mut auth = AuthorizerBuilder::new(); 383 | let prop_names: Handle = rules_obj.get_own_property_names(&mut cx)?; 384 | let prop_len = prop_names.len(&mut cx); 385 | for i in 0..prop_len { 386 | let key_js = prop_names.get::(&mut cx, i)?; 387 | let key: String = key_js.to_string(&mut cx)?.value(&mut cx); 388 | let value = rules_obj.get::(&mut cx, key.as_str())?; 389 | let value = value.value(&mut cx) as i32; 390 | if value == 0 { 391 | // Authorization.ALLOW 392 | auth.allow(&key); 393 | } else if value == 1 { 394 | // Authorization.DENY 395 | auth.deny(&key); 396 | } else { 397 | return cx.throw_error(format!( 398 | "Invalid authorization rule value '{}' for table '{}'. Expected 0 (ALLOW) or 1 (DENY).", 399 | value, key 400 | )); 401 | } 402 | } 403 | let auth = auth.build(); 404 | if let Err(err) = conn 405 | .blocking_lock() 406 | .authorizer(Some(Arc::new(move |ctx| auth.authorize(ctx)))) 407 | { 408 | throw_libsql_error(&mut cx, err)?; 409 | } 410 | Ok(cx.undefined()) 411 | } 412 | 413 | pub fn js_load_extension(mut cx: FunctionContext) -> JsResult { 414 | let db: Handle<'_, JsBox> = cx.this()?; 415 | let extension = cx.argument::(0)?.value(&mut cx); 416 | let entry_point: Option<&str> = match cx.argument_opt(1) { 417 | Some(_arg) => todo!(), 418 | None => None, 419 | }; 420 | trace!("Loading extension: {}", extension); 421 | let conn = match db.get_conn(&mut cx) { 422 | Some(conn) => conn, 423 | None => throw_database_closed_error(&mut cx)?, 424 | }; 425 | let conn = conn.blocking_lock(); 426 | if let Err(err) = conn.load_extension_enable() { 427 | throw_libsql_error(&mut cx, err)?; 428 | } 429 | if let Err(err) = conn.load_extension(&extension, entry_point) { 430 | let _ = conn.load_extension_disable(); 431 | throw_libsql_error(&mut cx, err)?; 432 | } 433 | if let Err(err) = conn.load_extension_disable() { 434 | throw_libsql_error(&mut cx, err)?; 435 | } 436 | Ok(cx.undefined()) 437 | } 438 | 439 | fn get_conn(&self, _cx: &mut FunctionContext) -> Option>> { 440 | let conn = self.conn.borrow(); 441 | conn.as_ref().map(|conn| conn.clone()) 442 | } 443 | } 444 | 445 | fn is_remote_path(path: &str) -> bool { 446 | path.starts_with("libsql://") || path.starts_with("http://") || path.starts_with("https://") 447 | } 448 | 449 | fn version(protocol: &str) -> String { 450 | let ver = env!("CARGO_PKG_VERSION"); 451 | format!("libsql-js-{protocol}-{ver}") 452 | } 453 | 454 | fn convert_replicated_to_object<'a>( 455 | cx: &mut impl Context<'a>, 456 | rep: &Replicated, 457 | ) -> JsResult<'a, JsObject> { 458 | let obj = cx.empty_object(); 459 | 460 | let frames_synced = cx.number(rep.frames_synced() as f64); 461 | obj.set(cx, "frames_synced", frames_synced)?; 462 | 463 | if let Some(v) = rep.frame_no() { 464 | let frame_no = cx.number(v as f64); 465 | obj.set(cx, "frame_no", frame_no)?; 466 | } else { 467 | let undef = cx.undefined(); 468 | obj.set(cx, "frame_no", undef)?; 469 | } 470 | 471 | Ok(obj) 472 | } 473 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use neon::{context::Context, object::Object, result::NeonResult, types::JsError}; 2 | 3 | pub fn throw_database_closed_error<'a, C: Context<'a>, T>(cx: &mut C) -> NeonResult { 4 | let err = JsError::type_error(cx, "The database connection is not open")?; 5 | cx.throw(err) 6 | } 7 | 8 | pub fn throw_libsql_error<'a, C: Context<'a>, T>(cx: &mut C, err: libsql::Error) -> NeonResult { 9 | match err { 10 | libsql::Error::SqliteFailure(code, err) => { 11 | let err = err.to_string(); 12 | let err = JsError::error(cx, err).unwrap(); 13 | let code_num = cx.number(code); 14 | err.set(cx, "rawCode", code_num).unwrap(); 15 | let code = cx.string(convert_sqlite_code(code)); 16 | err.set(cx, "code", code).unwrap(); 17 | let val = cx.boolean(true); 18 | err.set(cx, "libsqlError", val).unwrap(); 19 | cx.throw(err)? 20 | } 21 | _ => { 22 | let err = format!("{:?}", err); 23 | let err = JsError::error(cx, err).unwrap(); 24 | let code = cx.string(""); 25 | err.set(cx, "code", code).unwrap(); 26 | cx.throw(err)? 27 | } 28 | } 29 | } 30 | 31 | fn convert_sqlite_code(code: i32) -> String { 32 | match code { 33 | libsql::ffi::SQLITE_OK => "SQLITE_OK".to_owned(), 34 | libsql::ffi::SQLITE_ERROR => "SQLITE_ERROR".to_owned(), 35 | libsql::ffi::SQLITE_INTERNAL => "SQLITE_INTERNAL".to_owned(), 36 | libsql::ffi::SQLITE_PERM => "SQLITE_PERM".to_owned(), 37 | libsql::ffi::SQLITE_ABORT => "SQLITE_ABORT".to_owned(), 38 | libsql::ffi::SQLITE_BUSY => "SQLITE_BUSY".to_owned(), 39 | libsql::ffi::SQLITE_LOCKED => "SQLITE_LOCKED".to_owned(), 40 | libsql::ffi::SQLITE_NOMEM => "SQLITE_NOMEM".to_owned(), 41 | libsql::ffi::SQLITE_READONLY => "SQLITE_READONLY".to_owned(), 42 | libsql::ffi::SQLITE_INTERRUPT => "SQLITE_INTERRUPT".to_owned(), 43 | libsql::ffi::SQLITE_IOERR => "SQLITE_IOERR".to_owned(), 44 | libsql::ffi::SQLITE_CORRUPT => "SQLITE_CORRUPT".to_owned(), 45 | libsql::ffi::SQLITE_NOTFOUND => "SQLITE_NOTFOUND".to_owned(), 46 | libsql::ffi::SQLITE_FULL => "SQLITE_FULL".to_owned(), 47 | libsql::ffi::SQLITE_CANTOPEN => "SQLITE_CANTOPEN".to_owned(), 48 | libsql::ffi::SQLITE_PROTOCOL => "SQLITE_PROTOCOL".to_owned(), 49 | libsql::ffi::SQLITE_EMPTY => "SQLITE_EMPTY".to_owned(), 50 | libsql::ffi::SQLITE_SCHEMA => "SQLITE_SCHEMA".to_owned(), 51 | libsql::ffi::SQLITE_TOOBIG => "SQLITE_TOOBIG".to_owned(), 52 | libsql::ffi::SQLITE_CONSTRAINT => "SQLITE_CONSTRAINT".to_owned(), 53 | libsql::ffi::SQLITE_MISMATCH => "SQLITE_MISMATCH".to_owned(), 54 | libsql::ffi::SQLITE_MISUSE => "SQLITE_MISUSE".to_owned(), 55 | libsql::ffi::SQLITE_NOLFS => "SQLITE_NOLFS".to_owned(), 56 | libsql::ffi::SQLITE_AUTH => "SQLITE_AUTH".to_owned(), 57 | libsql::ffi::SQLITE_FORMAT => "SQLITE_FORMAT".to_owned(), 58 | libsql::ffi::SQLITE_RANGE => "SQLITE_RANGE".to_owned(), 59 | libsql::ffi::SQLITE_NOTADB => "SQLITE_NOTADB".to_owned(), 60 | libsql::ffi::SQLITE_NOTICE => "SQLITE_NOTICE".to_owned(), 61 | libsql::ffi::SQLITE_WARNING => "SQLITE_WARNING".to_owned(), 62 | libsql::ffi::SQLITE_ROW => "SQLITE_ROW".to_owned(), 63 | libsql::ffi::SQLITE_DONE => "SQLITE_DONE".to_owned(), 64 | libsql::ffi::SQLITE_IOERR_READ => "SQLITE_IOERR_READ".to_owned(), 65 | libsql::ffi::SQLITE_IOERR_SHORT_READ => "SQLITE_IOERR_SHORT_READ".to_owned(), 66 | libsql::ffi::SQLITE_IOERR_WRITE => "SQLITE_IOERR_WRITE".to_owned(), 67 | libsql::ffi::SQLITE_IOERR_FSYNC => "SQLITE_IOERR_FSYNC".to_owned(), 68 | libsql::ffi::SQLITE_IOERR_DIR_FSYNC => "SQLITE_IOERR_DIR_FSYNC".to_owned(), 69 | libsql::ffi::SQLITE_IOERR_TRUNCATE => "SQLITE_IOERR_TRUNCATE".to_owned(), 70 | libsql::ffi::SQLITE_IOERR_FSTAT => "SQLITE_IOERR_FSTAT".to_owned(), 71 | libsql::ffi::SQLITE_IOERR_UNLOCK => "SQLITE_IOERR_UNLOCK".to_owned(), 72 | libsql::ffi::SQLITE_IOERR_RDLOCK => "SQLITE_IOERR_RDLOCK".to_owned(), 73 | libsql::ffi::SQLITE_IOERR_DELETE => "SQLITE_IOERR_DELETE".to_owned(), 74 | libsql::ffi::SQLITE_IOERR_BLOCKED => "SQLITE_IOERR_BLOCKED".to_owned(), 75 | libsql::ffi::SQLITE_IOERR_NOMEM => "SQLITE_IOERR_NOMEM".to_owned(), 76 | libsql::ffi::SQLITE_IOERR_ACCESS => "SQLITE_IOERR_ACCESS".to_owned(), 77 | libsql::ffi::SQLITE_IOERR_CHECKRESERVEDLOCK => "SQLITE_IOERR_CHECKRESERVEDLOCK".to_owned(), 78 | libsql::ffi::SQLITE_IOERR_LOCK => "SQLITE_IOERR_LOCK".to_owned(), 79 | libsql::ffi::SQLITE_IOERR_CLOSE => "SQLITE_IOERR_CLOSE".to_owned(), 80 | libsql::ffi::SQLITE_IOERR_DIR_CLOSE => "SQLITE_IOERR_DIR_CLOSE".to_owned(), 81 | libsql::ffi::SQLITE_IOERR_SHMOPEN => "SQLITE_IOERR_SHMOPEN".to_owned(), 82 | libsql::ffi::SQLITE_IOERR_SHMSIZE => "SQLITE_IOERR_SHMSIZE".to_owned(), 83 | libsql::ffi::SQLITE_IOERR_SHMLOCK => "SQLITE_IOERR_SHMLOCK".to_owned(), 84 | libsql::ffi::SQLITE_IOERR_SHMMAP => "SQLITE_IOERR_SHMMAP".to_owned(), 85 | libsql::ffi::SQLITE_IOERR_SEEK => "SQLITE_IOERR_SEEK".to_owned(), 86 | libsql::ffi::SQLITE_IOERR_DELETE_NOENT => "SQLITE_IOERR_DELETE_NOENT".to_owned(), 87 | libsql::ffi::SQLITE_IOERR_MMAP => "SQLITE_IOERR_MMAP".to_owned(), 88 | libsql::ffi::SQLITE_IOERR_GETTEMPPATH => "SQLITE_IOERR_GETTEMPPATH".to_owned(), 89 | libsql::ffi::SQLITE_IOERR_CONVPATH => "SQLITE_IOERR_CONVPATH".to_owned(), 90 | libsql::ffi::SQLITE_IOERR_VNODE => "SQLITE_IOERR_VNODE".to_owned(), 91 | libsql::ffi::SQLITE_IOERR_AUTH => "SQLITE_IOERR_AUTH".to_owned(), 92 | libsql::ffi::SQLITE_LOCKED_SHAREDCACHE => "SQLITE_LOCKED_SHAREDCACHE".to_owned(), 93 | libsql::ffi::SQLITE_BUSY_RECOVERY => "SQLITE_BUSY_RECOVERY".to_owned(), 94 | libsql::ffi::SQLITE_BUSY_SNAPSHOT => "SQLITE_BUSY_SNAPSHOT".to_owned(), 95 | libsql::ffi::SQLITE_CANTOPEN_NOTEMPDIR => "SQLITE_CANTOPEN_NOTEMPDIR".to_owned(), 96 | libsql::ffi::SQLITE_CANTOPEN_ISDIR => "SQLITE_CANTOPEN_ISDIR".to_owned(), 97 | libsql::ffi::SQLITE_CANTOPEN_FULLPATH => "SQLITE_CANTOPEN_FULLPATH".to_owned(), 98 | libsql::ffi::SQLITE_CANTOPEN_CONVPATH => "SQLITE_CANTOPEN_CONVPATH".to_owned(), 99 | libsql::ffi::SQLITE_CORRUPT_VTAB => "SQLITE_CORRUPT_VTAB".to_owned(), 100 | libsql::ffi::SQLITE_READONLY_RECOVERY => "SQLITE_READONLY_RECOVERY".to_owned(), 101 | libsql::ffi::SQLITE_READONLY_CANTLOCK => "SQLITE_READONLY_CANTLOCK".to_owned(), 102 | libsql::ffi::SQLITE_READONLY_ROLLBACK => "SQLITE_READONLY_ROLLBACK".to_owned(), 103 | libsql::ffi::SQLITE_READONLY_DBMOVED => "SQLITE_READONLY_DBMOVED".to_owned(), 104 | libsql::ffi::SQLITE_ABORT_ROLLBACK => "SQLITE_ABORT_ROLLBACK".to_owned(), 105 | libsql::ffi::SQLITE_CONSTRAINT_CHECK => "SQLITE_CONSTRAINT_CHECK".to_owned(), 106 | libsql::ffi::SQLITE_CONSTRAINT_COMMITHOOK => "SQLITE_CONSTRAINT_COMMITHOOK".to_owned(), 107 | libsql::ffi::SQLITE_CONSTRAINT_FOREIGNKEY => "SQLITE_CONSTRAINT_FOREIGNKEY".to_owned(), 108 | libsql::ffi::SQLITE_CONSTRAINT_FUNCTION => "SQLITE_CONSTRAINT_FUNCTION".to_owned(), 109 | libsql::ffi::SQLITE_CONSTRAINT_NOTNULL => "SQLITE_CONSTRAINT_NOTNULL".to_owned(), 110 | libsql::ffi::SQLITE_CONSTRAINT_PRIMARYKEY => "SQLITE_CONSTRAINT_PRIMARYKEY".to_owned(), 111 | libsql::ffi::SQLITE_CONSTRAINT_TRIGGER => "SQLITE_CONSTRAINT_TRIGGER".to_owned(), 112 | libsql::ffi::SQLITE_CONSTRAINT_UNIQUE => "SQLITE_CONSTRAINT_UNIQUE".to_owned(), 113 | libsql::ffi::SQLITE_CONSTRAINT_VTAB => "SQLITE_CONSTRAINT_VTAB".to_owned(), 114 | libsql::ffi::SQLITE_CONSTRAINT_ROWID => "SQLITE_CONSTRAINT_ROWID".to_owned(), 115 | libsql::ffi::SQLITE_NOTICE_RECOVER_WAL => "SQLITE_NOTICE_RECOVER_WAL".to_owned(), 116 | libsql::ffi::SQLITE_NOTICE_RECOVER_ROLLBACK => "SQLITE_NOTICE_RECOVER_ROLLBACK".to_owned(), 117 | libsql::ffi::SQLITE_WARNING_AUTOINDEX => "SQLITE_WARNING_AUTOINDEX".to_owned(), 118 | libsql::ffi::SQLITE_AUTH_USER => "SQLITE_AUTH_USER".to_owned(), 119 | libsql::ffi::SQLITE_OK_LOAD_PERMANENTLY => "SQLITE_OK_LOAD_PERMANENTLY".to_owned(), 120 | _ => format!("UNKNOWN_SQLITE_ERROR_{}", code), 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod auth; 2 | mod database; 3 | mod errors; 4 | mod statement; 5 | 6 | use crate::database::Database; 7 | use crate::statement::{Rows, Statement}; 8 | use neon::prelude::*; 9 | use once_cell::sync::OnceCell; 10 | use tokio::runtime::Runtime; 11 | use tracing::level_filters::LevelFilter; 12 | use tracing_subscriber::EnvFilter; 13 | 14 | fn runtime<'a, C: Context<'a>>(cx: &mut C) -> NeonResult<&'static Runtime> { 15 | static RUNTIME: OnceCell = OnceCell::new(); 16 | 17 | RUNTIME 18 | .get_or_try_init(Runtime::new) 19 | .or_else(|err| cx.throw_error(&err.to_string())) 20 | } 21 | 22 | #[neon::main] 23 | fn main(mut cx: ModuleContext) -> NeonResult<()> { 24 | let _ = tracing_subscriber::fmt::fmt() 25 | .with_env_filter( 26 | EnvFilter::builder() 27 | .with_default_directive(LevelFilter::ERROR.into()) 28 | .from_env_lossy(), 29 | ) 30 | .try_init(); 31 | cx.export_function("databaseOpen", Database::js_open)?; 32 | cx.export_function("databaseOpenWithSync", Database::js_open_with_sync)?; 33 | cx.export_function("databaseInTransaction", Database::js_in_transaction)?; 34 | cx.export_function("databaseInterrupt", Database::js_interrupt)?; 35 | cx.export_function("databaseClose", Database::js_close)?; 36 | cx.export_function("databaseSyncSync", Database::js_sync_sync)?; 37 | cx.export_function("databaseSyncAsync", Database::js_sync_async)?; 38 | cx.export_function("databaseSyncUntilSync", Database::js_sync_until_sync)?; 39 | cx.export_function("databaseSyncUntilAsync", Database::js_sync_until_async)?; 40 | cx.export_function("databaseExecSync", Database::js_exec_sync)?; 41 | cx.export_function("databaseExecAsync", Database::js_exec_async)?; 42 | cx.export_function("databasePrepareSync", Database::js_prepare_sync)?; 43 | cx.export_function("databasePrepareAsync", Database::js_prepare_async)?; 44 | cx.export_function( 45 | "databaseDefaultSafeIntegers", 46 | Database::js_default_safe_integers, 47 | )?; 48 | cx.export_function("databaseAuthorizer", Database::js_authorizer)?; 49 | cx.export_function("databaseLoadExtension", Database::js_load_extension)?; 50 | cx.export_function( 51 | "databaseMaxWriteReplicationIndex", 52 | Database::js_max_write_replication_index, 53 | )?; 54 | cx.export_function("statementRaw", Statement::js_raw)?; 55 | cx.export_function("statementIsReader", Statement::js_is_reader)?; 56 | cx.export_function("statementRun", Statement::js_run)?; 57 | cx.export_function("statementInterrupt", Statement::js_interrupt)?; 58 | cx.export_function("statementGet", Statement::js_get)?; 59 | cx.export_function("statementRowsSync", Statement::js_rows_sync)?; 60 | cx.export_function("statementRowsAsync", Statement::js_rows_async)?; 61 | cx.export_function("statementColumns", Statement::js_columns)?; 62 | cx.export_function("statementSafeIntegers", Statement::js_safe_integers)?; 63 | cx.export_function("rowsNext", Rows::js_next)?; 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /src/statement.rs: -------------------------------------------------------------------------------- 1 | use neon::types::buffer::TypedArray; 2 | use neon::types::JsPromise; 3 | use neon::{prelude::*, types::JsBigInt}; 4 | use std::cell::RefCell; 5 | use std::sync::Arc; 6 | use tokio::sync::Mutex; 7 | use tokio::time::Instant; 8 | 9 | use crate::errors::throw_libsql_error; 10 | use crate::runtime; 11 | 12 | pub(crate) struct Statement { 13 | pub conn: Arc>, 14 | pub stmt: Arc>, 15 | pub raw: RefCell, 16 | pub safe_ints: RefCell, 17 | } 18 | 19 | impl Finalize for Statement {} 20 | 21 | fn js_value_to_value( 22 | cx: &mut FunctionContext, 23 | v: Handle<'_, JsValue>, 24 | ) -> NeonResult { 25 | if v.is_a::(cx) || v.is_a::(cx) { 26 | Ok(libsql::Value::Null) 27 | } else if v.is_a::(cx) { 28 | todo!("bool"); 29 | } else if v.is_a::(cx) { 30 | let v = v.downcast_or_throw::(cx)?; 31 | let v = v.value(cx); 32 | Ok(libsql::Value::Real(v)) 33 | } else if v.is_a::(cx) { 34 | let v = v.downcast_or_throw::(cx)?; 35 | let v = v.value(cx); 36 | Ok(libsql::Value::Text(v)) 37 | } else if v.is_a::(cx) { 38 | let v = v.downcast_or_throw::(cx)?; 39 | let v = v.to_i64(cx).or_throw(cx)?; 40 | Ok(libsql::Value::Integer(v)) 41 | } else if v.is_a::(cx) { 42 | let v = v.downcast_or_throw::(cx)?; 43 | let v = v.as_slice(cx); 44 | Ok(libsql::Value::Blob(v.to_vec())) 45 | } else if v.is_a::(cx) { 46 | let v = v.downcast_or_throw::(cx)?; 47 | let v = v.buffer(cx); 48 | let v = v.as_slice(cx); 49 | Ok(libsql::Value::Blob(v.to_vec())) 50 | } else { 51 | cx.throw_error("SQLite3 can only bind numbers, strings, bigints, buffers, and null") 52 | } 53 | } 54 | 55 | impl Statement { 56 | pub fn js_raw(mut cx: FunctionContext) -> JsResult { 57 | let stmt: Handle<'_, JsBox> = cx.this()?; 58 | let raw_stmt = stmt.stmt.blocking_lock(); 59 | if raw_stmt.columns().is_empty() { 60 | return cx.throw_error("The raw() method is only for statements that return data"); 61 | } 62 | let raw = cx.argument::(0)?; 63 | let raw = raw.value(&mut cx); 64 | stmt.set_raw(raw); 65 | Ok(cx.null()) 66 | } 67 | 68 | fn set_raw(&self, raw: bool) { 69 | self.raw.replace(raw); 70 | } 71 | 72 | pub fn js_is_reader(mut cx: FunctionContext) -> JsResult { 73 | let stmt: Handle<'_, JsBox> = cx.this()?; 74 | let raw_stmt = stmt.stmt.blocking_lock(); 75 | Ok(cx.boolean(!raw_stmt.columns().is_empty())) 76 | } 77 | 78 | pub fn js_run(mut cx: FunctionContext) -> JsResult { 79 | let stmt: Handle<'_, JsBox> = cx.this()?; 80 | let raw_conn = stmt.conn.clone(); 81 | let total_changes_before = raw_conn.blocking_lock().total_changes(); 82 | let params = cx.argument::(0)?; 83 | let params = convert_params(&mut cx, &stmt, params)?; 84 | let mut raw_stmt = stmt.stmt.blocking_lock(); 85 | raw_stmt.reset(); 86 | let fut = raw_stmt.run(params); 87 | let rt = runtime(&mut cx)?; 88 | 89 | let initial = Instant::now(); 90 | 91 | rt.block_on(fut) 92 | .or_else(|err| throw_libsql_error(&mut cx, err))?; 93 | 94 | let duration = Instant::now() - initial; 95 | 96 | let (changes, last_insert_rowid) = { 97 | let raw_conn = stmt.conn.clone(); 98 | let raw_conn = raw_conn.blocking_lock(); 99 | let changes = if raw_conn.total_changes() == total_changes_before { 100 | 0 101 | } else { 102 | raw_conn.changes() 103 | }; 104 | let last_insert_rowid = raw_conn.last_insert_rowid(); 105 | (changes, last_insert_rowid) 106 | }; 107 | 108 | let info = cx.empty_object(); 109 | 110 | let changes = cx.number(changes as f64); 111 | info.set(&mut cx, "changes", changes)?; 112 | 113 | let duration = cx.number(duration.as_secs_f64() as f64); 114 | info.set(&mut cx, "duration", duration)?; 115 | 116 | let last_insert_row_id = cx.number(last_insert_rowid as f64); 117 | info.set(&mut cx, "lastInsertRowid", last_insert_row_id)?; 118 | 119 | Ok(info.upcast()) 120 | } 121 | 122 | pub fn js_interrupt(mut cx: FunctionContext) -> JsResult { 123 | let stmt: Handle<'_, JsBox> = cx.this()?; 124 | let mut raw_stmt = stmt.stmt.blocking_lock(); 125 | raw_stmt 126 | .interrupt() 127 | .or_else(|err| throw_libsql_error(&mut cx, err))?; 128 | Ok(cx.null()) 129 | } 130 | 131 | pub fn js_get(mut cx: FunctionContext) -> JsResult { 132 | let stmt: Handle<'_, JsBox> = cx.this()?; 133 | let params = cx.argument::(0)?; 134 | let params = convert_params(&mut cx, &stmt, params)?; 135 | let safe_ints = *stmt.safe_ints.borrow(); 136 | let mut raw_stmt = stmt.stmt.blocking_lock(); 137 | let fut = raw_stmt.query(params); 138 | let rt = runtime(&mut cx)?; 139 | let result = rt.block_on(fut); 140 | let mut rows = result.or_else(|err| throw_libsql_error(&mut cx, err))?; 141 | 142 | let initial = Instant::now(); 143 | 144 | let result = rt 145 | .block_on(rows.next()) 146 | .or_else(|err| throw_libsql_error(&mut cx, err))?; 147 | 148 | let duration = Instant::now() - initial; 149 | 150 | let result = match result { 151 | Some(row) => { 152 | if *stmt.raw.borrow() { 153 | let mut result = cx.empty_array(); 154 | convert_row_raw(&mut cx, safe_ints, &mut result, &rows, &row)?; 155 | Ok(result.upcast()) 156 | } else { 157 | let mut result = cx.empty_object(); 158 | convert_row(&mut cx, safe_ints, &mut result, &rows, &row)?; 159 | 160 | let metadata = cx.empty_object(); 161 | result.set(&mut cx, "_metadata", metadata)?; 162 | 163 | let duration = cx.number(duration.as_secs_f64()); 164 | metadata.set(&mut cx, "duration", duration)?; 165 | 166 | Ok(result.upcast()) 167 | } 168 | } 169 | None => Ok(cx.undefined().upcast()), 170 | }; 171 | raw_stmt.reset(); 172 | result 173 | } 174 | 175 | pub fn js_rows_sync(mut cx: FunctionContext) -> JsResult { 176 | let stmt: Handle<'_, JsBox> = cx.this()?; 177 | let params = cx.argument::(0)?; 178 | let params = convert_params(&mut cx, &stmt, params)?; 179 | let rt = runtime(&mut cx)?; 180 | let result = rt.block_on(async move { 181 | let mut raw_stmt = stmt.stmt.lock().await; 182 | raw_stmt.reset(); 183 | raw_stmt.query(params).await 184 | }); 185 | let rows = result.or_else(|err| throw_libsql_error(&mut cx, err))?; 186 | let rows = Rows { 187 | rows: RefCell::new(rows), 188 | raw: *stmt.raw.borrow(), 189 | safe_ints: *stmt.safe_ints.borrow(), 190 | }; 191 | Ok(cx.boxed(rows).upcast()) 192 | } 193 | 194 | pub fn js_rows_async(mut cx: FunctionContext) -> JsResult { 195 | let stmt: Handle<'_, JsBox> = cx.this()?; 196 | let params = cx.argument::(0)?; 197 | let params = convert_params(&mut cx, &stmt, params)?; 198 | { 199 | let mut raw_stmt = stmt.stmt.blocking_lock(); 200 | raw_stmt.reset(); 201 | } 202 | let (deferred, promise) = cx.promise(); 203 | let channel = cx.channel(); 204 | let rt = runtime(&mut cx)?; 205 | let raw = *stmt.raw.borrow(); 206 | let safe_ints = *stmt.safe_ints.borrow(); 207 | let raw_stmt = stmt.stmt.clone(); 208 | rt.spawn(async move { 209 | let result = { 210 | let mut raw_stmt = raw_stmt.lock().await; 211 | raw_stmt.query(params).await 212 | }; 213 | match result { 214 | Ok(rows) => { 215 | deferred.settle_with(&channel, move |mut cx| { 216 | let rows = Rows { 217 | rows: RefCell::new(rows), 218 | raw, 219 | safe_ints, 220 | }; 221 | Ok(cx.boxed(rows)) 222 | }); 223 | } 224 | Err(err) => { 225 | deferred.settle_with(&channel, |mut cx| { 226 | throw_libsql_error(&mut cx, err)?; 227 | Ok(cx.undefined()) 228 | }); 229 | } 230 | } 231 | }); 232 | Ok(promise) 233 | } 234 | 235 | pub fn js_columns(mut cx: FunctionContext) -> JsResult { 236 | let stmt: Handle<'_, JsBox> = cx.this()?; 237 | let result = cx.empty_array(); 238 | let raw_stmt = stmt.stmt.blocking_lock(); 239 | for (i, col) in raw_stmt.columns().iter().enumerate() { 240 | let column = cx.empty_object(); 241 | let column_name = cx.string(col.name()); 242 | column.set(&mut cx, "name", column_name)?; 243 | let column_origin_name: Handle<'_, JsValue> = 244 | if let Some(origin_name) = col.origin_name() { 245 | cx.string(origin_name).upcast() 246 | } else { 247 | cx.null().upcast() 248 | }; 249 | column.set(&mut cx, "column", column_origin_name)?; 250 | let column_table_name: Handle<'_, JsValue> = if let Some(table_name) = col.table_name() 251 | { 252 | cx.string(table_name).upcast() 253 | } else { 254 | cx.null().upcast() 255 | }; 256 | column.set(&mut cx, "table", column_table_name)?; 257 | let column_database_name: Handle<'_, JsValue> = 258 | if let Some(database_name) = col.database_name() { 259 | cx.string(database_name).upcast() 260 | } else { 261 | cx.null().upcast() 262 | }; 263 | column.set(&mut cx, "database", column_database_name)?; 264 | let column_decl_type: Handle<'_, JsValue> = if let Some(decl_type) = col.decl_type() { 265 | cx.string(decl_type).upcast() 266 | } else { 267 | cx.null().upcast() 268 | }; 269 | column.set(&mut cx, "type", column_decl_type)?; 270 | result.set(&mut cx, i as u32, column)?; 271 | } 272 | Ok(result.upcast()) 273 | } 274 | 275 | pub fn js_safe_integers(mut cx: FunctionContext) -> JsResult { 276 | let stmt: Handle<'_, JsBox> = cx.this()?; 277 | let toggle = cx.argument::(0)?; 278 | let toggle = toggle.value(&mut cx); 279 | stmt.set_safe_integers(toggle); 280 | Ok(cx.null()) 281 | } 282 | 283 | fn set_safe_integers(&self, toggle: bool) { 284 | self.safe_ints.replace(toggle); 285 | } 286 | } 287 | 288 | pub(crate) struct Rows { 289 | rows: RefCell, 290 | raw: bool, 291 | safe_ints: bool, 292 | } 293 | 294 | impl Finalize for Rows {} 295 | 296 | impl Rows { 297 | pub fn js_next(mut cx: FunctionContext) -> JsResult { 298 | let result_arr = cx.argument::(0)?; 299 | let rows: Handle<'_, JsBox> = cx.this()?; 300 | let raw = rows.raw; 301 | let safe_ints = rows.safe_ints; 302 | let mut rows = rows.rows.borrow_mut(); 303 | let rt = runtime(&mut cx)?; 304 | let count = result_arr.len(&mut cx); 305 | let res = cx.null(); 306 | rt.block_on(async move { 307 | let mut keys = Vec::>::with_capacity(rows.column_count() as usize); 308 | for idx in 0..rows.column_count() { 309 | let column_name = rows.column_name(idx).unwrap(); 310 | keys.push(cx.string(column_name)); 311 | } 312 | for idx in 0..count { 313 | match rows 314 | .next() 315 | .await 316 | .or_else(|err| throw_libsql_error(&mut cx, err))? 317 | { 318 | Some(row) => { 319 | if raw { 320 | let mut result = cx.empty_array(); 321 | convert_row_raw(&mut cx, safe_ints, &mut result, &rows, &row)?; 322 | result_arr.set(&mut cx, idx, result)?; 323 | } else { 324 | let result = cx.empty_object(); 325 | for idx in 0..rows.column_count() { 326 | let v = row 327 | .get_value(idx) 328 | .or_else(|err| throw_libsql_error(&mut cx, err))?; 329 | let v: Handle<'_, JsValue> = match v { 330 | libsql::Value::Null => cx.null().upcast(), 331 | libsql::Value::Integer(v) => { 332 | if safe_ints { 333 | neon::types::JsBigInt::from_i64(&mut cx, v).upcast() 334 | } else { 335 | cx.number(v as f64).upcast() 336 | } 337 | } 338 | libsql::Value::Real(v) => cx.number(v).upcast(), 339 | libsql::Value::Text(v) => cx.string(v).upcast(), 340 | libsql::Value::Blob(v) => { 341 | JsArrayBuffer::from_slice(&mut cx, &v)?.upcast() 342 | } 343 | }; 344 | result.set(&mut cx, keys[idx as usize], v)?; 345 | } 346 | result_arr.set(&mut cx, idx, result)?; 347 | } 348 | } 349 | None => { 350 | break; 351 | } 352 | }; 353 | } 354 | Ok(()) 355 | })?; 356 | Ok(res) 357 | } 358 | } 359 | 360 | fn convert_params( 361 | cx: &mut FunctionContext, 362 | stmt: &Statement, 363 | v: Handle<'_, JsValue>, 364 | ) -> NeonResult { 365 | if v.is_a::(cx) { 366 | let v = v.downcast_or_throw::(cx)?; 367 | convert_params_array(cx, v) 368 | } else { 369 | let v = v.downcast_or_throw::(cx)?; 370 | convert_params_object(cx, stmt, v) 371 | } 372 | } 373 | 374 | fn convert_params_array( 375 | cx: &mut FunctionContext, 376 | v: Handle<'_, JsArray>, 377 | ) -> NeonResult { 378 | let mut params = vec![]; 379 | for i in 0..v.len(cx) { 380 | let v = v.get(cx, i)?; 381 | let v = js_value_to_value(cx, v)?; 382 | params.push(v); 383 | } 384 | Ok(libsql::params::Params::Positional(params)) 385 | } 386 | 387 | fn convert_params_object( 388 | cx: &mut FunctionContext, 389 | stmt: &Statement, 390 | v: Handle<'_, JsObject>, 391 | ) -> NeonResult { 392 | let mut params = vec![]; 393 | let stmt = &stmt.stmt; 394 | let raw_stmt = stmt.blocking_lock(); 395 | for idx in 0..raw_stmt.parameter_count() { 396 | let name = raw_stmt.parameter_name((idx + 1) as i32).unwrap(); 397 | let name = name.to_string(); 398 | let v = v.get(cx, &name[1..])?; 399 | let v = js_value_to_value(cx, v)?; 400 | params.push((name, v)); 401 | } 402 | Ok(libsql::params::Params::Named(params)) 403 | } 404 | 405 | fn convert_row( 406 | cx: &mut FunctionContext, 407 | safe_ints: bool, 408 | result: &mut JsObject, 409 | rows: &libsql::Rows, 410 | row: &libsql::Row, 411 | ) -> NeonResult<()> { 412 | for idx in 0..rows.column_count() { 413 | let v = row 414 | .get_value(idx) 415 | .or_else(|err| throw_libsql_error(cx, err))?; 416 | let column_name = rows.column_name(idx).unwrap(); 417 | let v: Handle<'_, JsValue> = match v { 418 | libsql::Value::Null => cx.null().upcast(), 419 | libsql::Value::Integer(v) => { 420 | if safe_ints { 421 | neon::types::JsBigInt::from_i64(cx, v).upcast() 422 | } else { 423 | cx.number(v as f64).upcast() 424 | } 425 | } 426 | libsql::Value::Real(v) => cx.number(v).upcast(), 427 | libsql::Value::Text(v) => cx.string(v).upcast(), 428 | libsql::Value::Blob(v) => JsBuffer::from_slice(cx, &v)?.upcast(), 429 | }; 430 | result.set(cx, column_name, v)?; 431 | } 432 | Ok(()) 433 | } 434 | 435 | fn convert_row_raw( 436 | cx: &mut FunctionContext, 437 | safe_ints: bool, 438 | result: &mut JsArray, 439 | rows: &libsql::Rows, 440 | row: &libsql::Row, 441 | ) -> NeonResult<()> { 442 | for idx in 0..rows.column_count() { 443 | let v = row 444 | .get_value(idx) 445 | .or_else(|err| throw_libsql_error(cx, err))?; 446 | let v: Handle<'_, JsValue> = match v { 447 | libsql::Value::Null => cx.null().upcast(), 448 | libsql::Value::Integer(v) => { 449 | if safe_ints { 450 | neon::types::JsBigInt::from_i64(cx, v).upcast() 451 | } else { 452 | cx.number(v as f64).upcast() 453 | } 454 | } 455 | libsql::Value::Real(v) => cx.number(v).upcast(), 456 | libsql::Value::Text(v) => cx.string(v).upcast(), 457 | libsql::Value::Blob(v) => JsBuffer::from_slice(cx, &v)?.upcast(), 458 | }; 459 | result.set(cx, idx as u32, v)?; 460 | } 461 | Ok(()) 462 | } 463 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./promise.js"], 3 | "compilerOptions": { 4 | // Tells TypeScript to read JS files, as 5 | // normally they are ignored as source files 6 | "allowJs": true, 7 | // Generate d.ts files 8 | "declaration": true, 9 | // This compiler run should 10 | // only output d.ts files 11 | "emitDeclarationOnly": true, 12 | // Types should go into this directory. 13 | // Removing this would place the .d.ts files 14 | // next to the .js files 15 | "outDir": "types", 16 | // go to js file when using IDE functions like 17 | // "Go to Definition" in VSCode 18 | "declarationMap": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /types/auth.d.ts: -------------------------------------------------------------------------------- 1 | export = Authorization; 2 | /** 3 | * * 4 | */ 5 | type Authorization = number; 6 | declare namespace Authorization { 7 | let ALLOW: number; 8 | let DENY: number; 9 | } 10 | //# sourceMappingURL=auth.d.ts.map -------------------------------------------------------------------------------- /types/auth.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../auth.js"],"names":[],"mappings":";;;;qBAIU,MAAM;;eAOJ,MAAM;cAMN,MAAM"} -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for better-sqlite3 7.6 2 | // Project: https://github.com/JoshuaWise/better-sqlite3 3 | // Definitions by: Ben Davies 4 | // Mathew Rumsey 5 | // Santiago Aguilar 6 | // Alessandro Vergani 7 | // Andrew Kaiser 8 | // Mark Stewart 9 | // Florian Stamer 10 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 11 | // TypeScript Version: 3.8 12 | 13 | /// 14 | 15 | // FIXME: Is this `any` really necessary? 16 | type VariableArgFunction = (...params: any[]) => unknown; 17 | type ArgumentTypes = F extends (...args: infer A) => unknown ? A : never; 18 | type ElementOf = T extends Array ? E : T; 19 | 20 | declare namespace Libsql { 21 | interface Statement { 22 | database: Database; 23 | source: string; 24 | reader: boolean; 25 | readonly: boolean; 26 | busy: boolean; 27 | 28 | run(...params: BindParameters): Database.RunResult; 29 | get(...params: BindParameters): unknown; 30 | all(...params: BindParameters): unknown[]; 31 | iterate(...params: BindParameters): IterableIterator; 32 | pluck(toggleState?: boolean): this; 33 | expand(toggleState?: boolean): this; 34 | raw(toggleState?: boolean): this; 35 | bind(...params: BindParameters): this; 36 | columns(): ColumnDefinition[]; 37 | safeIntegers(toggleState?: boolean): this; 38 | } 39 | 40 | interface ColumnDefinition { 41 | name: string; 42 | column: string | null; 43 | table: string | null; 44 | database: string | null; 45 | type: string | null; 46 | } 47 | 48 | interface Transaction { 49 | (...params: ArgumentTypes): ReturnType; 50 | default(...params: ArgumentTypes): ReturnType; 51 | deferred(...params: ArgumentTypes): ReturnType; 52 | immediate(...params: ArgumentTypes): ReturnType; 53 | exclusive(...params: ArgumentTypes): ReturnType; 54 | } 55 | 56 | interface VirtualTableOptions { 57 | rows: () => Generator; 58 | columns: string[]; 59 | parameters?: string[] | undefined; 60 | safeIntegers?: boolean | undefined; 61 | directOnly?: boolean | undefined; 62 | } 63 | 64 | interface Database { 65 | memory: boolean; 66 | readonly: boolean; 67 | name: string; 68 | open: boolean; 69 | inTransaction: boolean; 70 | 71 | prepare( 72 | source: string, 73 | ): BindParameters extends unknown[] ? Statement : Statement<[BindParameters]>; 74 | transaction(fn: F): Transaction; 75 | sync(): any; 76 | exec(source: string): this; 77 | pragma(source: string, options?: Database.PragmaOptions): unknown; 78 | function(name: string, cb: (...params: unknown[]) => unknown): this; 79 | function(name: string, options: Database.RegistrationOptions, cb: (...params: unknown[]) => unknown): this; 80 | aggregate(name: string, options: Database.RegistrationOptions & { 81 | start?: T | (() => T); 82 | step: (total: T, next: ElementOf) => T | void; 83 | inverse?: ((total: T, dropped: T) => T) | undefined; 84 | result?: ((total: T) => unknown) | undefined; 85 | }): this; 86 | loadExtension(path: string): this; 87 | close(): this; 88 | defaultSafeIntegers(toggleState?: boolean): this; 89 | backup(destinationFile: string, options?: Database.BackupOptions): Promise; 90 | table(name: string, options: VirtualTableOptions): this; 91 | unsafeMode(unsafe?: boolean): this; 92 | serialize(options?: Database.SerializeOptions): Buffer; 93 | } 94 | 95 | interface DatabaseConstructor { 96 | new (filename: string | Buffer, options?: Database.Options): Database; 97 | (filename: string, options?: Database.Options): Database; 98 | prototype: Database; 99 | 100 | SqliteError: typeof SqliteError; 101 | } 102 | } 103 | 104 | declare class SqliteError extends Error { 105 | name: string; 106 | message: string; 107 | code: string; 108 | rawCode?: number; 109 | constructor(message: string, code: string, rawCode?: number); 110 | } 111 | 112 | declare namespace Database { 113 | interface RunResult { 114 | changes: number; 115 | lastInsertRowid: number | bigint; 116 | } 117 | 118 | interface Options { 119 | readonly?: boolean | undefined; 120 | fileMustExist?: boolean | undefined; 121 | timeout?: number | undefined; 122 | verbose?: ((message?: unknown, ...additionalArgs: unknown[]) => void) | undefined; 123 | nativeBinding?: string | undefined; 124 | syncUrl?: string | undefined; 125 | } 126 | 127 | interface SerializeOptions { 128 | attached?: string; 129 | } 130 | 131 | interface PragmaOptions { 132 | simple?: boolean | undefined; 133 | } 134 | 135 | interface RegistrationOptions { 136 | varargs?: boolean | undefined; 137 | deterministic?: boolean | undefined; 138 | safeIntegers?: boolean | undefined; 139 | directOnly?: boolean | undefined; 140 | } 141 | 142 | type AggregateOptions = Parameters[1]; 143 | 144 | interface BackupMetadata { 145 | totalPages: number; 146 | remainingPages: number; 147 | } 148 | interface BackupOptions { 149 | progress: (info: BackupMetadata) => number; 150 | } 151 | 152 | type SqliteError = typeof SqliteError; 153 | type Statement = BindParameters extends unknown[] 154 | ? Libsql.Statement 155 | : Libsql.Statement<[BindParameters]>; 156 | type ColumnDefinition = Libsql.ColumnDefinition; 157 | type Transaction = Libsql.Transaction; 158 | type Database = Libsql.Database; 159 | } 160 | 161 | declare const Database: Libsql.DatabaseConstructor; 162 | export = Database; 163 | -------------------------------------------------------------------------------- /types/promise.d.ts: -------------------------------------------------------------------------------- 1 | export = Database; 2 | /** 3 | * Database represents a connection that can prepare and execute SQL statements. 4 | */ 5 | declare class Database { 6 | /** 7 | * Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created. 8 | * 9 | * @constructor 10 | * @param {string} path - Path to the database file. 11 | */ 12 | constructor(path: string, opts: any); 13 | db: any; 14 | memory: boolean; 15 | readonly: boolean; 16 | name: string; 17 | open: boolean; 18 | sync(): any; 19 | syncUntil(replicationIndex: any): any; 20 | /** 21 | * Prepares a SQL statement for execution. 22 | * 23 | * @param {string} sql - The SQL statement string to prepare. 24 | */ 25 | prepare(sql: string): any; 26 | /** 27 | * Returns a function that executes the given function in a transaction. 28 | * 29 | * @param {function} fn - The function to wrap in a transaction. 30 | */ 31 | transaction(fn: Function): (...bindParameters: any[]) => Promise; 32 | pragma(source: any, options: any): any; 33 | backup(filename: any, options: any): void; 34 | serialize(options: any): void; 35 | function(name: any, options: any, fn: any): void; 36 | aggregate(name: any, options: any): void; 37 | table(name: any, factory: any): void; 38 | authorizer(rules: any): void; 39 | loadExtension(...args: any[]): void; 40 | maxWriteReplicationIndex(): any; 41 | /** 42 | * Executes a SQL statement. 43 | * 44 | * @param {string} sql - The SQL statement string to execute. 45 | */ 46 | exec(sql: string): any; 47 | /** 48 | * Interrupts the database connection. 49 | */ 50 | interrupt(): void; 51 | /** 52 | * Closes the database connection. 53 | */ 54 | close(): void; 55 | /** 56 | * Toggle 64-bit integer support. 57 | */ 58 | defaultSafeIntegers(toggle: any): this; 59 | unsafeMode(...args: any[]): void; 60 | } 61 | declare namespace Database { 62 | export { Authorization, SqliteError }; 63 | } 64 | import Authorization = require("./auth"); 65 | import SqliteError = require("./sqlite-error"); 66 | //# sourceMappingURL=promise.d.ts.map -------------------------------------------------------------------------------- /types/promise.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"promise.d.ts","sourceRoot":"","sources":["../promise.js"],"names":[],"mappings":";AAoEA;;GAEG;AACH;IACE;;;;;OAKG;IACH,kBAFW,MAAM,aAoChB;IArBG,QAAmH;IAQrH,gBAAiC;IACjC,kBAAqB;IACrB,aAAc;IACd,cAAgB;IAYlB,YAEC;IAED,sCAEC;IAED;;;;OAIG;IACH,aAFW,MAAM,OAQhB;IAED;;;;OAIG;IACH,sEA8BC;IAED,uCAQC;IAED,0CAEC;IAED,8BAEC;IAED,iDAqBC;IAED,yCAYC;IAED,qCAUC;IAED,6BAEC;IAED,oCAEC;IAED,gCAEC;IAED;;;;OAIG;IACH,UAFW,MAAM,OAMhB;IAED;;OAEG;IACH,kBAEC;IAED;;OAEG;IACH,cAEC;IAED;;OAEG;IACH,uCAGC;IAED,iCAEC;CACF"} -------------------------------------------------------------------------------- /types/sqlite-error.d.ts: -------------------------------------------------------------------------------- 1 | export = SqliteError; 2 | declare function SqliteError(message: any, code: any, rawCode: any): SqliteError; 3 | declare class SqliteError { 4 | constructor(message: any, code: any, rawCode: any); 5 | code: string; 6 | rawCode: any; 7 | name: string; 8 | } 9 | //# sourceMappingURL=sqlite-error.d.ts.map -------------------------------------------------------------------------------- /types/sqlite-error.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"sqlite-error.d.ts","sourceRoot":"","sources":["../sqlite-error.js"],"names":[],"mappings":";AAGA,iFAaC;;IAbD,mDAaC;IAFO,aAAgB;IAChB,aAAsB"} --------------------------------------------------------------------------------