├── .changelog.toml ├── .gitignore ├── .node-version ├── .release-plz.toml ├── .vscode └── settings.json ├── .yarn └── releases │ └── yarn-4.5.1.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── web-sqlite │ ├── .vscode │ └── settings.json │ ├── Cargo.toml │ ├── README.md │ ├── build.sh │ ├── css │ ├── attribution.txt │ ├── brutalist.css │ └── style.css │ ├── diesel.toml │ ├── index.html │ ├── index.js │ ├── migrations │ └── 2024-12-09-190250_posts │ │ ├── down.sql │ │ └── up.sql │ ├── netlify.toml │ ├── pkg │ ├── example_sqlite_web.d.ts │ ├── example_sqlite_web.js │ ├── example_sqlite_web_bg.wasm │ ├── example_sqlite_web_bg.wasm.d.ts │ └── snippets │ │ └── sqlite-web-90f8afa7b0cd2303 │ │ └── src │ │ └── js │ │ ├── sqlite3-diesel.js │ │ └── sqlite3-opfs-async-proxy.js │ ├── run.sh │ └── src │ ├── lib.rs │ ├── posts.rs │ ├── schema.rs │ └── worker.js ├── flake.lock ├── flake.nix ├── package.json ├── rollup.config.js ├── rust-toolchain ├── sqlite3-diesel.js ├── src ├── backend.rs ├── connection │ ├── bind_collector.rs │ ├── diesel_manage_updated_at.sql │ ├── err.rs │ ├── functions.rs │ ├── mod.rs │ ├── owned_row.rs │ ├── raw.rs │ ├── row.rs │ ├── serialized_database.rs │ ├── sqlite_value.rs │ ├── statement_iterator.rs │ └── stmt.rs ├── ffi.rs ├── ffi │ ├── constants.rs │ └── wasm.rs ├── js │ ├── sqlite3-diesel.js │ ├── sqlite3-opfs-async-proxy.js │ └── sqlite3.wasm ├── lib.rs ├── query_builder │ ├── insert_with_default_sqlite.rs │ ├── limit_offset.rs │ ├── mod.rs │ ├── query_fragment_impls.rs │ └── returning.rs ├── sqlite_fixes.rs ├── sqlite_types.rs ├── sqlite_types │ └── to_sql.rs └── utils.rs ├── tests ├── common │ └── mod.rs ├── dedicated_web_worker.rs ├── migrations │ ├── 2024-08-20-203551_create_books │ │ ├── down.sql │ │ └── up.sql │ └── 2024-09-03-193701_test_table │ │ ├── down.sql │ │ └── up.sql └── test │ ├── row.rs │ └── web.rs └── yarn.lock /.changelog.toml: -------------------------------------------------------------------------------- 1 | [changelog] 2 | body = """ 3 | 4 | ## [{{ version | trim_start_matches(pat="v") }}]\ 5 | {%- if release_link -%}\ 6 | ({{ release_link }})\ 7 | {% endif %} \ 8 | - {{ timestamp | date(format="%Y-%m-%d") }} 9 | {% for group, commits in commits | group_by(attribute="group") %} 10 | ### {{ group | upper_first }} 11 | {% for commit in commits %} 12 | {%- if commit.scope -%} 13 | - *({{commit.scope}})* {% if commit.breaking %}[**breaking**] {% endif %}\ 14 | {{ commit.message }}{{ self::username(commit=commit) }}\ 15 | {%- if commit.links %} \ 16 | ({% for link in commit.links %}[{{link.text}}]({{link.href}}) {% endfor -%})\ 17 | {% endif %} 18 | {% else -%} 19 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}{{ self::username(commit=commit) }} 20 | {% endif -%} 21 | {% endfor -%} 22 | {% endfor %} 23 | {%- if remote.contributors %} 24 | ### Contributors 25 | {% for contributor in remote.contributors %} 26 | * @{{ contributor.username }} 27 | {%- endfor %} 28 | {% endif -%} 29 | {%- macro username(commit) -%} 30 | {% if commit.remote.username %} (by @{{ commit.remote.username }}){% endif -%} 31 | {% endmacro -%} 32 | """ 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # RustRover 17 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 18 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 19 | # and can be added to the global gitignore or merged into this file. For a more nuclear 20 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 21 | #.idea/# yarn 22 | .pnp.* 23 | .yarn/* 24 | !.yarn/patches 25 | !.yarn/plugins 26 | !.yarn/releases 27 | !.yarn/sdks 28 | !.yarn/versions 29 | node_modules/** 30 | .direnv/** 31 | .envrc 32 | .DS_Store 33 | examples/web-sqlite/pkg/** 34 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.17.0 2 | -------------------------------------------------------------------------------- /.release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | changelog_config = "./.changelog.toml" 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.sysroot": "discover", 3 | "rust-analyzer.cargo.allTargets": false, 4 | "rust-analyzer.cargo.target": "wasm32-unknown-unknown", 5 | "rust-analyzer.procMacro.enable": true, 6 | "rust-analyzer.diagnostics.enable": true, 7 | "rust-analyzer.diagnostics.disabled": [ 8 | "unlinked-file", 9 | "unresolved-macro-call", 10 | "unresolved-proc-macro" 11 | ], 12 | "rust-analyzer.procMacro.attributes.enable": true, 13 | "rust-analyzer.procMacro.ignored": { 14 | "async-trait": ["async_trait"], 15 | "napi-derive": ["napi"], 16 | "async-recursion": ["async_recursion"], 17 | "ctor": ["ctor"], 18 | "tokio": ["test"], 19 | "diesel": ["table"], 20 | "wasm-bindgen": ["wasm-bindgen"] 21 | }, 22 | "[toml]": { 23 | "editor.defaultFormatter": "tamasfe.even-better-toml" 24 | }, 25 | "[typescript]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[javascript]": { 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[json]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | enableTelemetry: false 6 | 7 | nodeLinker: node-modules 8 | 9 | yarnPath: .yarn/releases/yarn-4.5.1.cjs 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlite-web" 3 | version = "0.0.1" 4 | edition = "2021" 5 | resolver = "2" 6 | authors = ["XMTP Labs "] 7 | description = "SQLite WebAssembly backend for Diesel" 8 | homepage = "https://xmtp.org/" 9 | repository = "https://github.com/xmtp/libxmtp" 10 | license = "MIT" 11 | keywords = ["orm", "database", "sql", "wasm"] 12 | categories = ["database", "wasm"] 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | targets = ["wasm32-unknown-unknown"] 18 | 19 | [dependencies] 20 | talc = { version = "4.4", default-features = false, features = ["lock_api"] } 21 | diesel = { version = "2.2", features = [ 22 | "i-implement-a-third-party-backend-and-opt-into-breaking-changes", 23 | ] } 24 | diesel_derives = "2.2" 25 | wasm-bindgen = "=0.2.100" 26 | wasm-bindgen-futures = "0.4.50" 27 | js-sys = { version = "0.3" } 28 | tracing = { version = "0.1", default-features = false } 29 | tokio = { version = "1.38", default-features = false, features = ["sync"] } 30 | serde = { version = "1.0", default-features = false, features = ["derive"] } 31 | serde-wasm-bindgen = "0.6" 32 | thiserror = "2" 33 | 34 | [dev-dependencies] 35 | wasm-bindgen-test = "=0.3.50" 36 | console_error_panic_hook = { version = "0.1" } 37 | rand = "0.8" 38 | getrandom = { version = "0.2", features = ["js"] } 39 | web-sys = { version = "0.3", features = ["console"] } 40 | chrono = { version = "0.4", features = ["wasmbind", "serde"] } 41 | diesel_migrations = "2.2" 42 | diesel = { version = "2.2", features = ["chrono"] } 43 | tracing-wasm = { version = "0.2" } 44 | tracing-subscriber = { version = "0.3", features = [ 45 | "env-filter", 46 | "tracing-log", 47 | ] } 48 | 49 | [patch.crates-io] 50 | diesel = { git = "https://github.com/diesel-rs/diesel", branch = "master" } 51 | diesel_derives = { git = "https://github.com/diesel-rs/diesel", branch = "master" } 52 | diesel_migrations = { git = "https://github.com/diesel-rs/diesel", branch = "master" } 53 | 54 | [profile.release] 55 | opt-level = "s" 56 | lto = true 57 | 58 | [lib] 59 | crate-type = ["cdylib", "rlib"] 60 | 61 | [features] 62 | default = [] 63 | r2d2 = ["diesel/r2d2"] 64 | # enables a `DebugQueryWrapper` for diesel 65 | # but is unsafe because of mem::transmute 66 | unsafe-debug-query = [] 67 | 68 | [build] 69 | target = "wasm32-unknown-unknown" 70 | 71 | [package.metadata.wasm-pack.profile.dev.wasm-bindgen] 72 | split-linked-modules = true 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Status](https://img.shields.io/badge/Deprecated-brown) 2 | 3 | > [!CAUTION] 4 | > This repo is no longer maintained. Prefer to use 5 | > [Spxg/sqlite-wasm-rs](https://github.com/Spxg/sqlite-wasm-rs) directly with 6 | > [diesel](https://github.com/diesel-rs/diesel/tree/master/examples/sqlite/wasm) 7 | 8 | The documentation below is provided for historical reference only. 9 | 10 | # Diesel Backend for SQLite and WASM 11 | 12 | Use SQLite with Diesel ORM in your web apps! 13 | 14 | ## Quickstart 15 | 16 | Add `sqlite-web` to your project. SQLite is automatically bundled with the 17 | library. 18 | 19 | ```toml 20 | [dependencies] 21 | diesel = { version = "2.2" } 22 | sqlite-web = { git = "https://github.com/xmtp/sqlite-web-rs", branch = "main" } 23 | wasm-bindgen = "0.2" 24 | ``` 25 | 26 | ## Try It Out! 27 | 28 | Try out SQLite on the web with rust at this 29 | [example app](https://sqlite-web-example.netlify.app/) 30 | 31 | ## Running the Example 32 | 33 | Look in `examples/web-sqlite` for a working example! 34 | 35 | - run `yarn` in the root of the repo 36 | - navigate to `examples/web-sqlite` 37 | - make sure the [`miniserve`](https://github.com/svenstaro/miniserve) crate is 38 | installed locally 39 | - run `./build.sh && ./run.sh` 40 | - navigate to `localhost:8080` 41 | 42 | ## Contributing 43 | 44 | ### Building 45 | 46 | #### Install yarn dependencies 47 | 48 | `yarn` 49 | 50 | #### Build the SQLite/OPFS JavaScript Bundle 51 | 52 | `yarn build` 53 | 54 | #### Build the rust code 55 | 56 | `cargo build --target wasm32-unknown-unknown` 57 | 58 | #### Run tests (browser) 59 | 60 | - `yarn test:chrome` 61 | - `yarn test:firefox` 62 | - `yarn test:safari` 63 | 64 | Navigate to `http://localhost:8000` to observe test output. 65 | 66 | #### Run tests (headless) 67 | 68 | - `yarn test:chrome:headless` 69 | - `yarn test:firefox:headless` 70 | - `yarn test:safari:headless` 71 | 72 | ### Opening a Pull Request 73 | 74 | PR Title should follow 75 | [conventional commits format](https://www.conventionalcommits.org/en/v1.0.0/) 76 | 77 | In short, if should be one of: 78 | 79 | - `fix:` represents bug fixes, and results in a SemVer patch bump. 80 | - `feat:` represents a new feature, and results in a SemVer minor bump. 81 | - `!:` (e.g. feat!:): represents a **breaking change** (indicated by the 82 | !) and results in a SemVer major bump. 83 | - `doc:` documentation changes 84 | - `perf:` changes related to performance 85 | - `refactor:` a refactor 86 | - `style:` 87 | - `test:` 88 | 89 | You can add extra context to conventional commits by using parantheses, for 90 | instance if a PR touched only database-related code, a PR title may be: 91 | 92 | - `feat(db): Add SQLCipher plaintext_header support to database connection` 93 | -------------------------------------------------------------------------------- /examples/web-sqlite/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.sysroot": "discover", 3 | "rust-analyzer.cargo.allTargets": false, 4 | "rust-analyzer.cargo.target": "wasm32-unknown-unknown", 5 | "rust-analyzer.procMacro.enable": true, 6 | "rust-analyzer.diagnostics.enable": true, 7 | "rust-analyzer.diagnostics.disabled": [ 8 | "unlinked-file", 9 | "unresolved-macro-call", 10 | "unresolved-proc-macro" 11 | ], 12 | "rust-analyzer.procMacro.attributes.enable": true, 13 | "rust-analyzer.procMacro.ignored": { 14 | "async-trait": ["async_trait"], 15 | "napi-derive": ["napi"], 16 | "async-recursion": ["async_recursion"], 17 | "ctor": ["ctor"], 18 | "tokio": ["test"], 19 | "diesel": ["table"], 20 | "wasm-bindgen": ["wasm-bindgen"] 21 | }, 22 | "[toml]": { 23 | "editor.defaultFormatter": "tamasfe.even-better-toml" 24 | }, 25 | "[typescript]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[javascript]": { 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[json]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/web-sqlite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-sqlite-web" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | sqlite-web = { path = "./../../" } 12 | wasm-bindgen = "=0.2.99" 13 | wasm-bindgen-futures = "0.4" 14 | diesel = "2.2" 15 | console_error_panic_hook = { version = "0.1.6" } 16 | diesel_migrations = "2.2" 17 | serde = "1" 18 | serde-wasm-bindgen = "0.6" 19 | anyhow = "1" 20 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 21 | tracing = "0.1" 22 | tracing-wasm = "0.2" 23 | 24 | [dependencies.web-sys] 25 | version = "0.3" 26 | features = [ 27 | 'console', 28 | 'Document', 29 | 'HtmlElement', 30 | 'HtmlInputElement', 31 | 'HtmlTextAreaElement', 32 | 'MessageEvent', 33 | 'Window', 34 | 'Worker', 35 | 'WorkerType', 36 | 'WorkerOptions' 37 | ] 38 | 39 | # patch needed until diesel makes a release that includes 40 | # the latest changes 41 | [patch.crates-io] 42 | diesel = { git = "https://github.com/diesel-rs/diesel", branch = "master" } 43 | diesel_derives = { git = "https://github.com/diesel-rs/diesel", branch = "master" } 44 | diesel_migrations = { git = "https://github.com/diesel-rs/diesel", branch = "master" } 45 | 46 | 47 | [package.metadata.wasm-pack.profile.dev.wasm-bindgen] 48 | # Required for sqlite-web 49 | split-linked-modules = true 50 | -------------------------------------------------------------------------------- /examples/web-sqlite/README.md: -------------------------------------------------------------------------------- 1 | # web-sqlite 2 | -------------------------------------------------------------------------------- /examples/web-sqlite/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | # This example requires to *not* create ES modules, therefore we pass the flag 6 | # `--target no-modules` 7 | cd ../../ 8 | yarn 9 | cd examples/web-sqlite 10 | 11 | curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash 12 | cargo binstall wasm-bindgen-cli 13 | 14 | RUSTFLAGS="-Ctarget-feature=+bulk-memory,+mutable-globals,+reference-types" cargo build \ 15 | --release --target wasm32-unknown-unknown 16 | 17 | wasm-bindgen ./target/wasm32-unknown-unknown/release/example_sqlite_web.wasm \ 18 | --out-dir ./pkg --split-linked-modules --target web 19 | rm pkg/package.json 20 | yarn wasm-opt ./pkg/example_sqlite_web_bg.wasm -o ./pkg/example_sqlite_web_bg.wasm-opt.wasm -Oz 21 | 22 | if [[ $? -eq 0 ]]; then 23 | mv ./pkg/example_sqlite_web_bg.wasm-opt.wasm ./pkg/example_sqlite_web_bg.wasm 24 | else 25 | echo "wasm-opt failed" 26 | fi 27 | -------------------------------------------------------------------------------- /examples/web-sqlite/css/attribution.txt: -------------------------------------------------------------------------------- 1 | CSS provided by https://github.com/matifandy8/NeoBrutalismCSS/tree/main 2 | -------------------------------------------------------------------------------- /examples/web-sqlite/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | font-family: "Century Gothic", CenturyGothic, Geneva, AppleGothic, sans-serif; 9 | } 10 | 11 | #wrapper { 12 | width: 50%; 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | justify-content: center; 17 | } 18 | 19 | #div-wrapper { 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | justify-content: center; 24 | padding: 2em; 25 | } 26 | 27 | #inputNumber { 28 | text-align: center; 29 | } 30 | 31 | #resultField { 32 | text-align: center; 33 | height: 1em; 34 | padding-top: 0.2em; 35 | } 36 | -------------------------------------------------------------------------------- /examples/web-sqlite/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see https://diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/storage/encrypted_store/schema.rs" 6 | 7 | [migrations_directory] 8 | dir = "migrations" 9 | -------------------------------------------------------------------------------- /examples/web-sqlite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Post Manager 7 | 8 | 9 | 10 | 11 |
12 |

Database

13 |

web-sqlite-rs-test-db.db3

14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 30 |
31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 | 49 |
50 |

Posts

51 |
52 |
53 |
54 |
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/web-sqlite/index.js: -------------------------------------------------------------------------------- 1 | // We only need `startup` here which is the main entry point 2 | // In theory, we could also use all other functions/struct types from Rust which we have bound with 3 | // `#[wasm_bindgen]` 4 | import init, { startup } from "./pkg/example_sqlite_web.js"; 5 | 6 | async function run_wasm() { 7 | // Load the Wasm file by awaiting the Promise returned by `wasm_bindgen` 8 | // `wasm_bindgen` was imported in `index.html` 9 | await init(); 10 | 11 | console.log("index.js loaded"); 12 | 13 | // Run main Wasm entry point 14 | // This will create a worker from within our Rust code compiled to Wasm 15 | startup(); 16 | } 17 | 18 | run_wasm(); 19 | -------------------------------------------------------------------------------- /examples/web-sqlite/migrations/2024-12-09-190250_posts/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE posts 2 | -------------------------------------------------------------------------------- /examples/web-sqlite/migrations/2024-12-09-190250_posts/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE posts ( 2 | id INTEGER NOT NULL PRIMARY KEY, 3 | title VARCHAR NOT NULL, 4 | body TEXT NOT NULL, 5 | published BOOLEAN NOT NULL DEFAULT 0 6 | ) 7 | -------------------------------------------------------------------------------- /examples/web-sqlite/netlify.toml: -------------------------------------------------------------------------------- 1 | [[headers]] 2 | for = "/*" 3 | [headers.values] 4 | Cross-Origin-Embedder-Policy = "require-corp" 5 | Cross-Origin-Opener-Policy = "same-origin" 6 | -------------------------------------------------------------------------------- /examples/web-sqlite/pkg/example_sqlite_web.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Run entry point for the main thread. 5 | */ 6 | export function startup(): void; 7 | export class Posts { 8 | private constructor(); 9 | free(): void; 10 | static new(): Posts; 11 | static init_sqlite(): Promise; 12 | create_post(title: string, body: string, published: boolean): number; 13 | delete_post(id: number): number; 14 | clear(): number; 15 | list_posts(): any[]; 16 | } 17 | export class Version { 18 | private constructor(); 19 | free(): void; 20 | } 21 | 22 | export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; 23 | 24 | export interface InitOutput { 25 | readonly memory: WebAssembly.Memory; 26 | readonly __wbg_posts_free: (a: number, b: number) => void; 27 | readonly posts_new: () => number; 28 | readonly posts_init_sqlite: () => any; 29 | readonly posts_create_post: (a: number, b: number, c: number, d: number, e: number, f: number) => number; 30 | readonly posts_delete_post: (a: number, b: number) => number; 31 | readonly posts_clear: (a: number) => number; 32 | readonly posts_list_posts: (a: number) => [number, number]; 33 | readonly startup: () => void; 34 | readonly __wbg_version_free: (a: number, b: number) => void; 35 | readonly __wbindgen_free: (a: number, b: number, c: number) => void; 36 | readonly __wbindgen_exn_store: (a: number) => void; 37 | readonly __externref_table_alloc: () => number; 38 | readonly __wbindgen_export_3: WebAssembly.Table; 39 | readonly __wbindgen_malloc: (a: number, b: number) => number; 40 | readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; 41 | readonly __wbindgen_export_6: WebAssembly.Table; 42 | readonly __externref_drop_slice: (a: number, b: number) => void; 43 | readonly closure132_externref_shim: (a: number, b: number, c: any) => void; 44 | readonly _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6becb480f13c0dbc: (a: number, b: number) => void; 45 | readonly closure817_externref_shim: (a: number, b: number, c: any) => void; 46 | readonly closure883_externref_shim: (a: number, b: number, c: any, d: any) => void; 47 | readonly __wbindgen_start: () => void; 48 | } 49 | 50 | export type SyncInitInput = BufferSource | WebAssembly.Module; 51 | /** 52 | * Instantiates the given `module`, which can either be bytes or 53 | * a precompiled `WebAssembly.Module`. 54 | * 55 | * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. 56 | * 57 | * @returns {InitOutput} 58 | */ 59 | export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; 60 | 61 | /** 62 | * If `module_or_path` is {RequestInfo} or {URL}, makes a request and 63 | * for everything else, calls `WebAssembly.instantiate` directly. 64 | * 65 | * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. 66 | * 67 | * @returns {Promise} 68 | */ 69 | export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; 70 | -------------------------------------------------------------------------------- /examples/web-sqlite/pkg/example_sqlite_web_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmtp/sqlite-web-rs/HEAD/examples/web-sqlite/pkg/example_sqlite_web_bg.wasm -------------------------------------------------------------------------------- /examples/web-sqlite/pkg/example_sqlite_web_bg.wasm.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export const memory: WebAssembly.Memory; 4 | export const __wbg_posts_free: (a: number, b: number) => void; 5 | export const posts_new: () => number; 6 | export const posts_init_sqlite: () => any; 7 | export const posts_create_post: (a: number, b: number, c: number, d: number, e: number, f: number) => number; 8 | export const posts_delete_post: (a: number, b: number) => number; 9 | export const posts_clear: (a: number) => number; 10 | export const posts_list_posts: (a: number) => [number, number]; 11 | export const startup: () => void; 12 | export const __wbg_version_free: (a: number, b: number) => void; 13 | export const __wbindgen_free: (a: number, b: number, c: number) => void; 14 | export const __wbindgen_exn_store: (a: number) => void; 15 | export const __externref_table_alloc: () => number; 16 | export const __wbindgen_export_3: WebAssembly.Table; 17 | export const __wbindgen_malloc: (a: number, b: number) => number; 18 | export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; 19 | export const __wbindgen_export_6: WebAssembly.Table; 20 | export const __externref_drop_slice: (a: number, b: number) => void; 21 | export const closure132_externref_shim: (a: number, b: number, c: any) => void; 22 | export const _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6becb480f13c0dbc: (a: number, b: number) => void; 23 | export const closure817_externref_shim: (a: number, b: number, c: any) => void; 24 | export const closure883_externref_shim: (a: number, b: number, c: any, d: any) => void; 25 | export const __wbindgen_start: () => void; 26 | -------------------------------------------------------------------------------- /examples/web-sqlite/pkg/snippets/sqlite-web-90f8afa7b0cd2303/src/js/sqlite3-opfs-async-proxy.js: -------------------------------------------------------------------------------- 1 | const e=(e,...t)=>postMessage({type:e,payload:t}),t=function(){const t=function(...e){throw new Error(e.join(" "))};globalThis.window===globalThis?t("This code cannot run from the main thread.","Load it as a Worker from a separate Worker."):navigator?.storage?.getDirectory||t("This API requires navigator.storage.getDirectory.");const n=Object.create(null);n.verbose=1;const s={0:console.error.bind(console),1:console.warn.bind(console),2:console.log.bind(console)},a=(e,...t)=>{n.verbose>e&&s[e]("OPFS asyncer:",...t)},i=(...e)=>a(2,...e),o=(...e)=>a(1,...e),r=(...e)=>a(0,...e),c=Object.create(null),l=new Set,d=function(e,t){const n=new URL(e,"file://irrelevant").pathname;return t?n.split("/").filter((e=>!!e)):n},f=async function(e,t=!1){const s=d(e,!0),a=s.pop();let i=n.rootDir;for(const e of s)e&&(i=await i.getDirectoryHandle(e,{create:!!t}));return[i,a]},y=async e=>{if(e.syncHandle){i("Closing sync handle for",e.filenameAbs);const t=e.syncHandle;return delete e.syncHandle,delete e.xLock,l.delete(e.fid),t.close()}},u=async e=>{try{await y(e)}catch(t){o("closeSyncHandleNoThrow() ignoring:",t,e)}},E=async()=>{if(l.size)for(const e of l){const t=c[e];await u(t),i("Auto-unlocked",e,t.filenameAbs)}},b=async e=>{if(e.releaseImplicitLocks&&l.has(e.fid))return u(e)};class O extends Error{constructor(e,...t){super([...t,": "+e.name+":",e.message].join(" "),{cause:e}),this.name="GetSyncHandleError"}}O.convertRc=(e,t)=>{if(e instanceof O){if("NoModificationAllowedError"===e.cause.name||"DOMException"===e.cause.name&&0===e.cause.message.indexOf("Access Handles cannot"))return n.sq3Codes.SQLITE_BUSY;if("NotFoundError"===e.cause.name)return n.sq3Codes.SQLITE_CANTOPEN}else if("NotFoundError"===e?.name)return n.sq3Codes.SQLITE_CANTOPEN;return t};const g=async(e,t)=>{if(!e.syncHandle){const s=performance.now();i("Acquiring sync handle for",e.filenameAbs);const a=6,r=2*n.asyncIdleWaitTime;let c=1,d=r;for(;;d=r*++c)try{e.syncHandle=await e.fileHandle.createSyncAccessHandle();break}catch(s){if(c===a)throw new O(s,"Error getting sync handle for",t+"().",a,"attempts failed.",e.filenameAbs);o("Error getting sync handle for",t+"(). Waiting",d,"ms and trying again.",e.filenameAbs,s),Atomics.wait(n.sabOPView,n.opIds.retry,0,d)}i("Got",t+"() sync handle for",e.filenameAbs,"in",performance.now()-s,"ms"),e.xLock||(l.add(e.fid),i("Acquired implicit lock for",t+"()",e.fid,e.filenameAbs))}return e.syncHandle},h=(e,t)=>{i(e+"() => notify(",t,")"),Atomics.store(n.sabOPView,n.opIds.rc,t),Atomics.notify(n.sabOPView,n.opIds.rc)},w=function(e,n){n.readOnly&&t(e+"(): File is read-only: "+n.filenameAbs)};let p=!1;const I={"opfs-async-shutdown":async()=>{p=!0,h("opfs-async-shutdown",0)},mkdir:async e=>{let t=0;try{await f(e+"/filepart",!0)}catch(e){n.s11n.storeException(2,e),t=n.sq3Codes.SQLITE_IOERR}h("mkdir",t)},xAccess:async e=>{let t=0;try{const[t,n]=await f(e);await t.getFileHandle(n)}catch(e){n.s11n.storeException(2,e),t=n.sq3Codes.SQLITE_IOERR}h("xAccess",t)},xClose:async function(e){l.delete(e);const t=c[e];let s=0;if(t){if(delete c[e],await y(t),t.deleteOnClose)try{await t.dirHandle.removeEntry(t.filenamePart)}catch(e){o("Ignoring dirHandle.removeEntry() failure of",t,e)}}else n.s11n.serialize(),s=n.sq3Codes.SQLITE_NOTFOUND;h("xClose",s)},xDelete:async function(...e){const t=await I.xDeleteNoWait(...e);h("xDelete",t)},xDeleteNoWait:async function(e,t=0,s=!1){let a=0;try{for(;e;){const[n,a]=await f(e,!1);if(!a)break;if(await n.removeEntry(a,{recursive:s}),4660!==t)break;s=!1,(e=d(e,!0)).pop(),e=e.join("/")}}catch(e){n.s11n.storeException(2,e),a=n.sq3Codes.SQLITE_IOERR_DELETE}return a},xFileSize:async function(e){const t=c[e];let s=0;try{const e=await(await g(t,"xFileSize")).getSize();n.s11n.serialize(Number(e))}catch(e){n.s11n.storeException(1,e),s=O.convertRc(e,n.sq3Codes.SQLITE_IOERR)}await b(t),h("xFileSize",s)},xLock:async function(e,t){const s=c[e];let a=0;const i=s.xLock;if(s.xLock=t,!s.syncHandle)try{await g(s,"xLock"),l.delete(e)}catch(e){n.s11n.storeException(1,e),a=O.convertRc(e,n.sq3Codes.SQLITE_IOERR_LOCK),s.xLock=i}h("xLock",a)},xOpen:async function(e,t,s,a){const i="xOpen",o=n.sq3Codes.SQLITE_OPEN_CREATE&s;try{let r,l;try{[r,l]=await f(t,!!o)}catch(e){return n.s11n.storeException(1,e),void h(i,n.sq3Codes.SQLITE_NOTFOUND)}if(n.opfsFlags.OPFS_UNLINK_BEFORE_OPEN&a)try{await r.removeEntry(l)}catch(e){}const d=await r.getFileHandle(l,{create:o}),y=Object.assign(Object.create(null),{fid:e,filenameAbs:t,filenamePart:l,dirHandle:r,fileHandle:d,sabView:n.sabFileBufView,readOnly:!o&&n.sq3Codes.SQLITE_OPEN_READONLY&s,deleteOnClose:!!(n.sq3Codes.SQLITE_OPEN_DELETEONCLOSE&s)});y.releaseImplicitLocks=a&n.opfsFlags.OPFS_UNLOCK_ASAP||n.opfsFlags.defaultUnlockAsap,c[e]=y,h(i,0)}catch(e){r(i,e),n.s11n.storeException(1,e),h(i,n.sq3Codes.SQLITE_IOERR)}},xRead:async function(e,t,s){let a,i=0;const o=c[e];try{a=(await g(o,"xRead")).read(o.sabView.subarray(0,t),{at:Number(s)}),a{Number.isFinite(n.opIds[e])||t("Maintenance required: missing state.opIds[",e,"]")})),(()=>{if(n.s11n)return n.s11n;const e=new TextDecoder,s=new TextEncoder("utf-8"),a=new Uint8Array(n.sabIO,n.sabS11nOffset,n.sabS11nSize),i=new DataView(n.sabIO,n.sabS11nOffset,n.sabS11nSize);n.s11n=Object.create(null);const o=Object.create(null);o.number={id:1,size:8,getter:"getFloat64",setter:"setFloat64"},o.bigint={id:2,size:8,getter:"getBigInt64",setter:"setBigInt64"},o.boolean={id:3,size:4,getter:"getInt32",setter:"setInt32"},o.string={id:4};const r=e=>{switch(e){case o.number.id:return o.number;case o.bigint.id:return o.bigint;case o.boolean.id:return o.boolean;case o.string.id:return o.string;default:t("Invalid type ID:",e)}};n.s11n.deserialize=function(t=!1){const s=a[0],o=s?[]:null;if(s){const t=[];let c,l,d,f=1;for(c=0;c{e<=n.asyncS11nExceptions&&n.s11n.serialize([t.name,": ",t.message].join(""))}:()=>{},n.s11n})(),i("init state",n),e("opfs-async-inited"),m();break}case"opfs-async-restart":p&&(o("Restarting after opfs-async-shutdown. Might or might not work."),p=!1,m())}},e("opfs-async-loaded")})).catch((e=>r("error initializing OPFS asyncer:",e)))};globalThis.SharedArrayBuffer?globalThis.Atomics?globalThis.FileSystemHandle&&globalThis.FileSystemDirectoryHandle&&globalThis.FileSystemFileHandle&&globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle&&navigator?.storage?.getDirectory?t():e("opfs-unavailable","Missing required OPFS APIs."):e("opfs-unavailable","Missing Atomics API.","The server must emit the COOP/COEP response headers to enable that."):e("opfs-unavailable","Missing SharedArrayBuffer API.","The server must emit the COOP/COEP response headers to enable that."); 2 | -------------------------------------------------------------------------------- /examples/web-sqlite/run.sh: -------------------------------------------------------------------------------- 1 | miniserve . --index "index.html" -p 8080 --header "Cross-Origin-Embedder-Policy: require-corp" --header "Cross-Origin-Opener-Policy: same-origin" 2 | -------------------------------------------------------------------------------- /examples/web-sqlite/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// An example using sqlite-web on the web 2 | /// Some worker code taken from https://github.com/rustwasm/wasm-bindgen/blob/main/examples/wasm-in-web-worker/src/lib.rs 3 | /// newer init method used, https://rustwasm.github.io/docs/wasm-bindgen/examples/without-a-bundler.html#using-the-older---target-no-modules 4 | use posts::StoredPost; 5 | use std::cell::RefCell; 6 | use std::rc::Rc; 7 | use wasm_bindgen::prelude::*; 8 | use web_sys::{console, HtmlElement, HtmlInputElement, HtmlTextAreaElement, MessageEvent, Worker, Document, WorkerType, WorkerOptions}; 9 | use serde::{Serialize, Deserialize}; 10 | use anyhow::Error; 11 | 12 | mod posts; 13 | mod schema; 14 | 15 | pub fn logger() { 16 | use tracing_subscriber::layer::SubscriberExt; 17 | use tracing_subscriber::util::SubscriberInitExt; 18 | use tracing_subscriber::EnvFilter; 19 | let filter = EnvFilter::builder() 20 | .with_default_directive(tracing::metadata::LevelFilter::DEBUG.into()) 21 | .from_env_lossy(); 22 | 23 | let _ = tracing_subscriber::registry() 24 | .with(tracing_wasm::WASMLayer::default()) 25 | .with(filter) 26 | .try_init(); 27 | } 28 | 29 | /// Run entry point for the main thread. 30 | #[wasm_bindgen] 31 | pub fn startup() { 32 | console_error_panic_hook::set_once(); 33 | logger(); 34 | // Here, we create our worker. In a larger app, multiple callbacks should be 35 | // able to interact with the code in the worker. Therefore, we wrap it in 36 | // `Rc` following the interior mutability pattern. Here, it would 37 | // not be needed but we include the wrapping anyway as example. 38 | let options = WorkerOptions::new(); 39 | options.set_type(WorkerType::Module); 40 | let worker_handle = Rc::new(RefCell::new(Worker::new_with_options( 41 | &wasm_bindgen::link_to!(module = "/src/worker.js"), 42 | &options 43 | ).unwrap())); 44 | console::log_1(&"Created a new worker from within Wasm".into()); 45 | // Pass the worker to the function which sets up the `oninput` callback. 46 | setup_input_oninput_callback(worker_handle); 47 | } 48 | 49 | #[derive(Serialize, Deserialize)] 50 | enum WorkerMessage { 51 | Create { 52 | title: String, 53 | body: String, 54 | published: bool 55 | }, 56 | Delete(i32), 57 | List, 58 | Clear 59 | } 60 | 61 | fn setup_input_oninput_callback(worker: Rc>) { 62 | let document = web_sys::window().unwrap().document().unwrap(); 63 | 64 | // If our `onmessage` callback should stay valid after exiting from the 65 | // `oninput` closure scope, we need to either forget it (so it is not 66 | // destroyed) or store it somewhere. To avoid leaking memory every time we 67 | // want to receive a response from the worker, we move a handle into the 68 | // `oninput` closure to which we will always attach the last `onmessage` 69 | // callback. The initial value will not be used and we silence the warning. 70 | #[allow(unused_assignments)] 71 | let mut persistent_callback_handle = get_on_msg_callback(); 72 | 73 | let create_post: Closure = { 74 | let worker = worker.clone(); 75 | Closure::new(move || { 76 | let document = web_sys::window().unwrap().document().unwrap(); 77 | 78 | // If the value in the field can be parsed to a `i32`, send it to the 79 | // worker. Otherwise clear the result field. 80 | let worker_handle = &*worker.borrow(); 81 | if parse_create(&document, worker_handle).is_ok() { 82 | reset_input(&document, "title"); 83 | reset_input(&document, "body"); 84 | reset_input(&document, "published"); 85 | } 86 | }) 87 | }; 88 | 89 | let delete_post: Closure = { 90 | let worker = worker.clone(); 91 | Closure::new(move || { 92 | let document = web_sys::window().unwrap().document().unwrap(); 93 | let id = get_input(&document, "post-id"); 94 | let worker_handle = &*worker.borrow(); 95 | if let Ok(id) = id.value().parse::() { 96 | let msg = WorkerMessage::Delete(id); 97 | worker_handle.post_message(&serde_wasm_bindgen::to_value(&msg).unwrap()).unwrap(); 98 | } else { 99 | reset_input(&document, "post-id") 100 | } 101 | 102 | }) 103 | }; 104 | 105 | let clear: Closure = { 106 | let worker = worker.clone(); 107 | Closure::new(move || { 108 | let worker_handle = &*worker.borrow(); 109 | let msg = WorkerMessage::Clear; 110 | worker_handle.post_message(&serde_wasm_bindgen::to_value(&msg).unwrap()).unwrap(); 111 | }) 112 | }; 113 | 114 | 115 | let list_post: Closure = { 116 | let worker = worker.clone(); 117 | Closure::new(move || { 118 | let worker_handle = &*worker.borrow(); 119 | let msg = WorkerMessage::List; 120 | worker_handle.post_message(&serde_wasm_bindgen::to_value(&msg).unwrap()).unwrap(); 121 | persistent_callback_handle = get_on_msg_callback(); 122 | worker_handle 123 | .set_onmessage(Some(persistent_callback_handle.as_ref().unchecked_ref())); 124 | }) 125 | }; 126 | 127 | document 128 | .get_element_by_id("create-post-btn") 129 | .expect("#create-post-btn should exist") 130 | .dyn_ref::() 131 | .expect("#create-post-btn should be a HtmlElement") 132 | .set_onclick(Some(create_post.as_ref().unchecked_ref())); 133 | document 134 | .get_element_by_id("delete-post-btn") 135 | .expect("#delete-post-form should exist") 136 | .dyn_ref::() 137 | .expect("#delete-post-form should be a HtmlElement") 138 | .set_onclick(Some(delete_post.as_ref().unchecked_ref())); 139 | document 140 | .get_element_by_id("list-posts-btn") 141 | .expect("#list-posts-btn should exist") 142 | .dyn_ref::() 143 | .expect("#list-posts-button should be an HtmlElement") 144 | .set_onclick(Some(list_post.as_ref().unchecked_ref())); 145 | document 146 | .get_element_by_id("clear-posts-btn") 147 | .expect("#clear-posts-btn should exist") 148 | .dyn_ref::() 149 | .expect("#clear-posts-button should be an HtmlElement") 150 | .set_onclick(Some(clear.as_ref().unchecked_ref())); 151 | 152 | // Leaks memory. 153 | create_post.forget(); 154 | delete_post.forget(); 155 | list_post.forget(); 156 | clear.forget(); 157 | } 158 | 159 | fn get_input(document: &Document, id: &str) -> HtmlInputElement { 160 | let title_input = document.get_element_by_id(id).expect("#{id} should exist"); 161 | title_input 162 | .dyn_into::() 163 | .expect(&format!("#{id} should be a HtmlInputElement")) 164 | } 165 | 166 | fn reset_input(document: &Document, id: &str) { 167 | document 168 | .get_element_by_id(id) 169 | .expect("#{id} should exist") 170 | .dyn_ref::() 171 | .expect("#{id} should be a HtmlInputElement") 172 | .set_inner_text(""); 173 | } 174 | 175 | fn parse_create(document: &Document, worker: &web_sys::Worker) -> Result<(), Error> { 176 | let title = get_input(&document, "title").value().parse::()?; 177 | let body = document.get_element_by_id("body").expect("body should exist"); 178 | let body = body.dyn_ref::().expect("Body should be a textarea"); 179 | let published = get_input(&document, "published"); 180 | let msg = WorkerMessage::Create { title, body: body.value(), published: published.checked() }; 181 | worker.post_message(&serde_wasm_bindgen::to_value(&msg).unwrap()).unwrap(); 182 | Ok(()) 183 | } 184 | 185 | /// Create a closure to act on the message returned by the worker 186 | fn get_on_msg_callback() -> Closure { 187 | Closure::new(move |event: MessageEvent| { 188 | console::log_2(&"Received response: ".into(), &event.data()); 189 | 190 | let posts: Vec = serde_wasm_bindgen::from_value(event.data()).unwrap(); 191 | let mut html = String::new(); 192 | for post in posts { 193 | let StoredPost { id, title, body, published } = post; 194 | let part = format!(" 195 |
196 |

Title: {title}

197 |
ID: {id}
198 |
Published: {published}
199 |

{body}

200 |
201 |
202 | "); 203 | html.push_str(&part); 204 | } 205 | let document = web_sys::window().unwrap().document().unwrap(); 206 | document 207 | .get_element_by_id("posts-container") 208 | .expect("#posts-container should exist") 209 | .dyn_ref::() 210 | .expect("#resultField should be a HtmlInputElement") 211 | .set_inner_html(&html); 212 | }) 213 | } 214 | -------------------------------------------------------------------------------- /examples/web-sqlite/src/posts.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use sqlite_web::{connection::WasmSqliteConnection, dsl::RunQueryDsl}; 3 | use diesel_migrations::MigrationHarness; 4 | use wasm_bindgen::prelude::*; 5 | use serde::{Serialize, Deserialize}; 6 | use std::cell::RefCell; 7 | use std::rc::Rc; 8 | 9 | use diesel_migrations::{embed_migrations, EmbeddedMigrations}; 10 | 11 | use crate::schema::*; 12 | 13 | pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations"); 14 | 15 | #[derive(Debug, Queryable, Selectable, Serialize, Deserialize)] 16 | #[diesel(table_name = crate::schema::posts)] 17 | #[diesel(check_for_backend(sqlite_web::WasmSqlite))] 18 | pub struct StoredPost { 19 | pub id: i32, 20 | pub title: String, 21 | pub body: String, 22 | pub published: bool, 23 | } 24 | 25 | #[derive(Deserialize, Insertable, Debug, PartialEq, Clone)] 26 | #[diesel(table_name = posts)] 27 | pub struct NewPost { 28 | title: String, 29 | body: String, 30 | published: bool 31 | } 32 | 33 | 34 | #[wasm_bindgen] 35 | pub struct Posts { 36 | connection: Rc>, 37 | } 38 | 39 | #[wasm_bindgen] 40 | impl Posts { 41 | pub fn new() -> Self { 42 | let mut connection = WasmSqliteConnection::establish(&format!("web-sqlite-rs-test-db.db3")).expect("CONN"); 43 | connection.run_pending_migrations(MIGRATIONS).expect("MIGRATIONS"); 44 | Self { connection: Rc::new(RefCell::new(connection)) } 45 | } 46 | 47 | pub async fn init_sqlite() { 48 | crate::logger(); 49 | tracing::debug!("init sqlite"); 50 | sqlite_web::init_sqlite().await; 51 | } 52 | 53 | pub fn create_post(&self, title: String, body: String, published: bool) -> usize { 54 | use crate::schema::posts; 55 | let post = NewPost { 56 | title, body, published 57 | }; 58 | let conn = &mut *self.connection.borrow_mut(); 59 | let rows = diesel::insert_into(posts::table).values(&post).execute(conn).unwrap(); 60 | rows 61 | } 62 | 63 | pub fn delete_post(&self, id: i32) -> usize { 64 | use crate::schema::posts::dsl; 65 | let conn = &mut *self.connection.borrow_mut(); 66 | let rows = diesel::delete(dsl::posts.filter(dsl::id.eq(&id))).execute(conn).unwrap(); 67 | rows 68 | } 69 | 70 | pub fn clear(&self) -> usize { 71 | use crate::schema::posts::dsl; 72 | let conn = &mut *self.connection.borrow_mut(); 73 | let rows = diesel::delete(dsl::posts).execute(conn).unwrap(); 74 | rows 75 | } 76 | 77 | pub fn list_posts(&self) -> Vec { 78 | use crate::schema::posts::dsl; 79 | let conn = &mut *self.connection.borrow_mut(); 80 | let posts = dsl::posts.select(StoredPost::as_select()) 81 | .load(conn); 82 | posts.unwrap().into_iter().map(|v| serde_wasm_bindgen::to_value(&v).unwrap()).collect() 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /examples/web-sqlite/src/schema.rs: -------------------------------------------------------------------------------- 1 | diesel::table! { 2 | posts (id) { 3 | id -> Integer, 4 | title -> Varchar, 5 | body -> Text, 6 | published -> Bool, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/web-sqlite/src/worker.js: -------------------------------------------------------------------------------- 1 | // The worker has its own scope and no direct access to functions/objects of the 2 | // global scope. We import the generated JS file to make `wasm_bindgen` 3 | // available which we need to initialize our Wasm code. 4 | console.log("Initializing worker"); 5 | import { default as init, Posts } from "/pkg/example_sqlite_web.js"; 6 | 7 | // import init from ("/pkg/example_sqlite_web.js"); 8 | // importScripts("/pkgs/example_sqlite_web.js"); 9 | 10 | // In the worker, we have a different struct that we want to use as in 11 | // `index.js`. 12 | 13 | const port = self; 14 | 15 | async function init_wasm_in_worker() { 16 | // Create a new object of the `NumberEval` struct. 17 | // await posts.init(); 18 | const wasm = await init("/pkg/example_sqlite_web_bg.wasm"); 19 | await Posts.init_sqlite(); 20 | 21 | // Set callback to handle messages passed to the worker. 22 | port.onmessage = async (event) => { 23 | // Load the Wasm file by awaiting the Promise returned by `wasm_bindgen`. 24 | // await posts.initSqlite(); 25 | var posts = Posts.new(); 26 | 27 | let msg = event.data; 28 | 29 | let worker_result = { result: "test" }; 30 | 31 | if (msg.hasOwnProperty("Create")) { 32 | var args = msg["Create"]; 33 | posts.create_post(args.title, args.body, args.published); 34 | } else if (msg.hasOwnProperty("Delete")) { 35 | var id = msg["Delete"]; 36 | posts.delete_post(id); 37 | } else if (msg == "Clear") { 38 | posts.clear(); 39 | self.postMessage(new Array()); 40 | } else if (msg == "List") { 41 | var posts = posts.list_posts(); 42 | self.postMessage(posts); 43 | } else { 44 | console.log("unknown msg"); 45 | } 46 | }; 47 | } 48 | 49 | await init_wasm_in_worker(); 50 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "environments": { 4 | "inputs": { 5 | "fenix": "fenix", 6 | "flake-utils": "flake-utils", 7 | "foundry": "foundry", 8 | "nixpkgs": "nixpkgs_2", 9 | "solc": "solc" 10 | }, 11 | "locked": { 12 | "lastModified": 1732768501, 13 | "narHash": "sha256-0/Op6q6Jw5TN1a88VIwHQz6Pvmrd2BMk6fTRSjA7uV8=", 14 | "owner": "insipx", 15 | "repo": "environments", 16 | "rev": "d4364cab2f852206e568994661cf3f17c5694b0e", 17 | "type": "github" 18 | }, 19 | "original": { 20 | "owner": "insipx", 21 | "repo": "environments", 22 | "type": "github" 23 | } 24 | }, 25 | "fenix": { 26 | "inputs": { 27 | "nixpkgs": [ 28 | "environments", 29 | "nixpkgs" 30 | ], 31 | "rust-analyzer-src": "rust-analyzer-src" 32 | }, 33 | "locked": { 34 | "lastModified": 1732429952, 35 | "narHash": "sha256-kTXRYkTpvYn4AtYY8OUVpO7h9xNjPAaHxtTuz1N4l9g=", 36 | "owner": "nix-community", 37 | "repo": "fenix", 38 | "rev": "39603febfbb0b494a368905a5c5c1b13bd6e1e53", 39 | "type": "github" 40 | }, 41 | "original": { 42 | "owner": "nix-community", 43 | "repo": "fenix", 44 | "type": "github" 45 | } 46 | }, 47 | "fenix_2": { 48 | "inputs": { 49 | "nixpkgs": [ 50 | "nixpkgs" 51 | ], 52 | "rust-analyzer-src": "rust-analyzer-src_2" 53 | }, 54 | "locked": { 55 | "lastModified": 1733726208, 56 | "narHash": "sha256-Z6zL4AtpZWxkvTd3l0KkPZamu2wtTKl4nNiqNSlgsb4=", 57 | "owner": "nix-community", 58 | "repo": "fenix", 59 | "rev": "d51a64e1d23e509f28a6955a6652cc62409dd4a8", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "owner": "nix-community", 64 | "repo": "fenix", 65 | "type": "github" 66 | } 67 | }, 68 | "flake-utils": { 69 | "inputs": { 70 | "systems": "systems" 71 | }, 72 | "locked": { 73 | "lastModified": 1731533236, 74 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 75 | "owner": "numtide", 76 | "repo": "flake-utils", 77 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 78 | "type": "github" 79 | }, 80 | "original": { 81 | "owner": "numtide", 82 | "repo": "flake-utils", 83 | "type": "github" 84 | } 85 | }, 86 | "flake-utils_2": { 87 | "locked": { 88 | "lastModified": 1644229661, 89 | "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", 90 | "owner": "numtide", 91 | "repo": "flake-utils", 92 | "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", 93 | "type": "github" 94 | }, 95 | "original": { 96 | "owner": "numtide", 97 | "repo": "flake-utils", 98 | "type": "github" 99 | } 100 | }, 101 | "flake-utils_3": { 102 | "inputs": { 103 | "systems": "systems_2" 104 | }, 105 | "locked": { 106 | "lastModified": 1731533236, 107 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 108 | "owner": "numtide", 109 | "repo": "flake-utils", 110 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 111 | "type": "github" 112 | }, 113 | "original": { 114 | "owner": "numtide", 115 | "repo": "flake-utils", 116 | "type": "github" 117 | } 118 | }, 119 | "foundry": { 120 | "inputs": { 121 | "flake-utils": "flake-utils_2", 122 | "nixpkgs": "nixpkgs" 123 | }, 124 | "locked": { 125 | "lastModified": 1730625090, 126 | "narHash": "sha256-lWfkkj+GEUM0UqYLD2Rx3zzILTL3xdmGJKGR4fwONpA=", 127 | "owner": "shazow", 128 | "repo": "foundry.nix", 129 | "rev": "1c6a742bcbfd55a80de0e1f967a60174716a1560", 130 | "type": "github" 131 | }, 132 | "original": { 133 | "owner": "shazow", 134 | "ref": "monthly", 135 | "repo": "foundry.nix", 136 | "type": "github" 137 | } 138 | }, 139 | "nixpkgs": { 140 | "locked": { 141 | "lastModified": 1666753130, 142 | "narHash": "sha256-Wff1dGPFSneXJLI2c0kkdWTgxnQ416KE6X4KnFkgPYQ=", 143 | "owner": "NixOS", 144 | "repo": "nixpkgs", 145 | "rev": "f540aeda6f677354f1e7144ab04352f61aaa0118", 146 | "type": "github" 147 | }, 148 | "original": { 149 | "id": "nixpkgs", 150 | "type": "indirect" 151 | } 152 | }, 153 | "nixpkgs_2": { 154 | "locked": { 155 | "lastModified": 0, 156 | "narHash": "sha256-sQxuJm8rHY20xq6Ah+GwIUkF95tWjGRd1X8xF+Pkk38=", 157 | "path": "/nix/store/csfywra6032psdbgna9qcbdads87gmzw-source", 158 | "type": "path" 159 | }, 160 | "original": { 161 | "id": "nixpkgs", 162 | "type": "indirect" 163 | } 164 | }, 165 | "nixpkgs_3": { 166 | "locked": { 167 | "lastModified": 1733581040, 168 | "narHash": "sha256-Qn3nPMSopRQJgmvHzVqPcE3I03zJyl8cSbgnnltfFDY=", 169 | "owner": "NixOS", 170 | "repo": "nixpkgs", 171 | "rev": "22c3f2cf41a0e70184334a958e6b124fb0ce3e01", 172 | "type": "github" 173 | }, 174 | "original": { 175 | "owner": "NixOS", 176 | "ref": "nixos-unstable", 177 | "repo": "nixpkgs", 178 | "type": "github" 179 | } 180 | }, 181 | "root": { 182 | "inputs": { 183 | "environments": "environments", 184 | "fenix": "fenix_2", 185 | "flake-utils": "flake-utils_3", 186 | "nixpkgs": "nixpkgs_3" 187 | } 188 | }, 189 | "rust-analyzer-src": { 190 | "flake": false, 191 | "locked": { 192 | "lastModified": 1732388649, 193 | "narHash": "sha256-N6zhzSLdGWUZnyRgCRyOy4Nyz8TkekXgXD2ZhCKAjzE=", 194 | "owner": "rust-lang", 195 | "repo": "rust-analyzer", 196 | "rev": "a2bb0149144e0e6330e60abe4683b4e1a7ab3582", 197 | "type": "github" 198 | }, 199 | "original": { 200 | "owner": "rust-lang", 201 | "ref": "nightly", 202 | "repo": "rust-analyzer", 203 | "type": "github" 204 | } 205 | }, 206 | "rust-analyzer-src_2": { 207 | "flake": false, 208 | "locked": { 209 | "lastModified": 1733642337, 210 | "narHash": "sha256-I1uc97f/cNhOpCemIbBAUS+CV0R7jts0NW9lc8jrpxc=", 211 | "owner": "rust-lang", 212 | "repo": "rust-analyzer", 213 | "rev": "4c755e62a617eeeef3066994731ce1cdd16504ac", 214 | "type": "github" 215 | }, 216 | "original": { 217 | "owner": "rust-lang", 218 | "ref": "nightly", 219 | "repo": "rust-analyzer", 220 | "type": "github" 221 | } 222 | }, 223 | "solc": { 224 | "inputs": { 225 | "flake-utils": [ 226 | "environments", 227 | "flake-utils" 228 | ], 229 | "nixpkgs": [ 230 | "environments", 231 | "nixpkgs" 232 | ], 233 | "solc-macos-amd64-list-json": "solc-macos-amd64-list-json" 234 | }, 235 | "locked": { 236 | "lastModified": 1731758759, 237 | "narHash": "sha256-NX4+V6Q8bwopah0oza/Dpf6UsYNGbokW2kE9qT3wdHY=", 238 | "owner": "hellwolf", 239 | "repo": "solc.nix", 240 | "rev": "0714c24cd521b9eb3ee435818c5d743ac6179176", 241 | "type": "github" 242 | }, 243 | "original": { 244 | "owner": "hellwolf", 245 | "repo": "solc.nix", 246 | "type": "github" 247 | } 248 | }, 249 | "solc-macos-amd64-list-json": { 250 | "flake": false, 251 | "locked": { 252 | "narHash": "sha256-KBEEpcDeKtVvCeguRP0D499yg9O5Jef9Nxn3yfrmw9g=", 253 | "type": "file", 254 | "url": "https://github.com/ethereum/solc-bin/raw/67f45d8/macosx-amd64/list.json" 255 | }, 256 | "original": { 257 | "type": "file", 258 | "url": "https://github.com/ethereum/solc-bin/raw/67f45d8/macosx-amd64/list.json" 259 | } 260 | }, 261 | "systems": { 262 | "locked": { 263 | "lastModified": 1681028828, 264 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 265 | "owner": "nix-systems", 266 | "repo": "default", 267 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 268 | "type": "github" 269 | }, 270 | "original": { 271 | "owner": "nix-systems", 272 | "repo": "default", 273 | "type": "github" 274 | } 275 | }, 276 | "systems_2": { 277 | "locked": { 278 | "lastModified": 1681028828, 279 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 280 | "owner": "nix-systems", 281 | "repo": "default", 282 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 283 | "type": "github" 284 | }, 285 | "original": { 286 | "owner": "nix-systems", 287 | "repo": "default", 288 | "type": "github" 289 | } 290 | } 291 | }, 292 | "root": "root", 293 | "version": 7 294 | } 295 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | fenix = { 5 | url = "github:nix-community/fenix"; 6 | inputs = { nixpkgs.follows = "nixpkgs"; }; 7 | }; 8 | environments.url = "github:insipx/environments"; 9 | flake-utils.url = "github:numtide/flake-utils"; 10 | }; 11 | 12 | outputs = { nixpkgs, flake-utils, fenix, environments, ... }: 13 | flake-utils.lib.eachDefaultSystem (system: 14 | let 15 | pkgs = import nixpkgs { inherit system; }; 16 | inherit (pkgs.stdenv) isDarwin; 17 | inherit (pkgs.darwin.apple_sdk) frameworks; 18 | fenixPkgs = fenix.packages.${system}; 19 | # probably don't need all these linters 20 | linters = import "${environments}/linters.nix" { inherit pkgs; }; 21 | rust-toolchain = fenixPkgs.fromToolchainFile { 22 | file = ./rust-toolchain; 23 | sha256 = "sha256-s1RPtyvDGJaX/BisLT+ifVfuhDT1nZkZ1NcK8sbwELM="; 24 | }; 25 | nativeBuildInputs = with pkgs; [ pkg-config ]; 26 | buildInputs = with pkgs; 27 | [ 28 | rust-toolchain 29 | rust-analyzer 30 | llvmPackages_16.libcxxClang 31 | mktemp 32 | markdownlint-cli 33 | shellcheck 34 | buf 35 | curl 36 | twiggy 37 | # binaryen 38 | linters 39 | cargo-nextest 40 | cargo-udeps 41 | cargo-sweep 42 | cargo-cache 43 | cargo-machete 44 | cargo-features-manager 45 | cargo-bloat 46 | cargo-mutants 47 | cargo-deny 48 | cargo-audit 49 | chromedriver 50 | geckodriver 51 | diesel-cli 52 | 53 | nodejs 54 | yarn-berry 55 | ] ++ lib.optionals isDarwin [ 56 | libiconv 57 | frameworks.CoreServices 58 | frameworks.Carbon 59 | frameworks.ApplicationServices 60 | frameworks.AppKit 61 | darwin.cctools 62 | ]; 63 | in 64 | with pkgs; { 65 | devShells.default = mkShell { inherit buildInputs nativeBuildInputs; }; 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmtp/sqlite-web-rs", 3 | "type": "module", 4 | "private": true, 5 | "version": "0.0.0", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "test:chrome": "wasm-pack test --chrome --features unsafe-debug-query", 9 | "test:chrome:headless": "wasm-pack test --chrome --headless --features unsafe-debug-query", 10 | "test:firefox": "wasm-pack test --firefox --features unsafe-debug-query", 11 | "test:firefox:headless": "wasm-pack test --firefox --headless --features unsafe-debug-query", 12 | "test:safari": "wasm-pack test --safari --features unsafe-debug-query", 13 | "test:safari:headless": "wasm-pack test --safari --headless --features unsafe-debug-query" 14 | }, 15 | "packageManager": "yarn@4.5.1", 16 | "engines": { 17 | "node": ">=20" 18 | }, 19 | "dependencies": { 20 | "@sqlite.org/sqlite-wasm": "latest" 21 | }, 22 | "devDependencies": { 23 | "@rollup/plugin-node-resolve": "^15.3.0", 24 | "@rollup/plugin-terser": "^0.4.4", 25 | "binaryen": "^120.0.0", 26 | "rollup": "^4.24.4", 27 | "rollup-plugin-copy": "^3.5.0", 28 | "wasm-pack": "^0.13.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "rollup"; 2 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 3 | import copy from "rollup-plugin-copy"; 4 | import terser from "@rollup/plugin-terser"; 5 | 6 | export default defineConfig([ 7 | { 8 | input: "sqlite3-diesel.js", 9 | output: { 10 | file: "src/js/sqlite3-diesel.js", 11 | format: "es", 12 | }, 13 | treeshake: "smallest", 14 | plugins: [ 15 | nodeResolve(), 16 | terser(), 17 | copy({ 18 | targets: [ 19 | { 20 | src: "./node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm", 21 | dest: "src/js", 22 | }, 23 | ], 24 | }), 25 | ], 26 | }, 27 | { 28 | input: 29 | "./node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js", 30 | output: { 31 | file: "src/js/sqlite3-opfs-async-proxy.js", 32 | format: "es", 33 | }, 34 | plugins: [terser()], 35 | }, 36 | ]); 37 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = [ "rustc", "cargo", "clippy", "rustfmt", "rust-analyzer" ] 4 | targets = [ "wasm32-unknown-unknown", "x86_64-unknown-linux-gnu"] 5 | profile = "default" 6 | -------------------------------------------------------------------------------- /sqlite3-diesel.js: -------------------------------------------------------------------------------- 1 | import sqlite3InitModule from "@sqlite.org/sqlite-wasm"; 2 | 3 | const log = console.log; 4 | const err_log = console.error; 5 | 6 | export class SQLiteError extends Error { 7 | constructor(message, code) { 8 | super(message); 9 | this.code = code; 10 | } 11 | } 12 | 13 | export class SQLite { 14 | #module; 15 | #sqlite3; 16 | constructor(sqlite3) { 17 | if (typeof sqlite3 === "undefined") { 18 | throw new Error( 19 | "`sqliteObject` must be defined before calling constructor", 20 | ); 21 | } 22 | this.sqlite3 = sqlite3; 23 | } 24 | 25 | static async init_module(opts) { 26 | return await sqlite3InitModule({ 27 | print: log, 28 | printErr: err_log, 29 | ...opts, 30 | }); 31 | } 32 | 33 | version() { 34 | return this.sqlite3.version; 35 | } 36 | 37 | filename(db, name) { 38 | return this.sqlite3.capi.sqlite3_db_filename(db, name); 39 | } 40 | 41 | extended_errcode(connection) { 42 | return this.sqlite3.capi.sqlite3_extended_errcode(connection); 43 | } 44 | 45 | errstr(code) { 46 | return this.sqlite3.capi.sqlite3_errstr(code); 47 | } 48 | 49 | errmsg(connection) { 50 | return this.sqlite3.capi.sqlite3_errmsg(connection); 51 | } 52 | 53 | result_js(context, value) { 54 | return this.sqlite3.capi.sqlite3_result_js(context, value); 55 | } 56 | 57 | result_text(context, value) { 58 | return this.sqlite3.capi.sqlite3_result_text(context, value); 59 | } 60 | 61 | result_int(context, value) { 62 | return this.sqlite3.capi.sqlite3_result_int(context, value); 63 | } 64 | 65 | result_int64(context, value) { 66 | return this.sqlite3.capi.sqlite3_result_int64(context, value); 67 | } 68 | 69 | result_double(context, value) { 70 | return this.sqlite3.capi.sqlite3_result_double(context, value); 71 | } 72 | 73 | result_blob(context, value) { 74 | return this.sqlite3.capi.sqlite3_result_blob(context, value); 75 | } 76 | 77 | result_null(context) { 78 | return this.sqlite3.capi.sqlite3_result_null(context); 79 | } 80 | 81 | bind_blob(stmt, i, value, len, flags) { 82 | return this.sqlite3.capi.sqlite3_bind_blob(stmt, i, value, len, flags); 83 | } 84 | 85 | bind_text(stmt, i, value, len, flags) { 86 | return this.sqlite3.capi.sqlite3_bind_text(stmt, i, value, len, flags); 87 | } 88 | 89 | bind_double(stmt, i, value) { 90 | return this.sqlite3.capi.sqlite3_bind_double(stmt, i, value); 91 | } 92 | 93 | bind_int(stmt, i, value) { 94 | return this.sqlite3.capi.sqlite3_bind_int(stmt, i, value); 95 | } 96 | 97 | bind_int64(stmt, i, value) { 98 | return this.sqlite3.capi.sqlite3_bind_int64(stmt, i, value); 99 | } 100 | 101 | bind_null(stmt, i) { 102 | this.sqlite3.capi.sqlite3_bind_null(stmt, i); 103 | /// There's no way bind_null can fail. 104 | return this.sqlite3.capi.SQLITE_OK; 105 | } 106 | 107 | bind_parameter_count(stmt) { 108 | return this.sqlite3.capi.sqlite3_bind_parameter_count(stmt); 109 | } 110 | 111 | bind_parameter_name(stmt, i) { 112 | return this.sqlite3.capi.sqlite3_bind_paramater_name(stmt, it); 113 | } 114 | 115 | value_dup(pValue) { 116 | return this.sqlite3.capi.sqlite3_value_dup(pValue); 117 | } 118 | 119 | value_blob(pValue) { 120 | return this.sqlite3.capi.sqlite3_value_blob(pValue); 121 | } 122 | 123 | value_bytes(pValue) { 124 | return this.sqlite3.capi.sqlite3_value_bytes(pValue); 125 | } 126 | 127 | value_double(pValue) { 128 | return this.sqlite3.capi.sqlite3_value_double(pValue); 129 | } 130 | 131 | value_int(pValue) { 132 | return this.sqlite3.capi.sqlite3_value_int(pValue); 133 | } 134 | 135 | value_int64(pValue) { 136 | return this.sqlite3.capi.sqlite3_value_int64(pValue); 137 | } 138 | 139 | value_text(pValue) { 140 | return this.sqlite3.capi.sqlite3_value_text(pValue); 141 | } 142 | 143 | value_type(pValue) { 144 | return this.sqlite3.capi.sqlite3_value_type(pValue); 145 | } 146 | 147 | open(database_url, iflags) { 148 | try { 149 | return new this.sqlite3.oo1.OpfsDb(database_url); 150 | } catch (error) { 151 | console.log("OPFS open error", error); 152 | throw error; 153 | } 154 | } 155 | 156 | exec(db, query) { 157 | try { 158 | return db.exec(query, { 159 | callback: (row) => {}, 160 | }); 161 | } catch (error) { 162 | throw error; 163 | } 164 | } 165 | 166 | finalize(stmt) { 167 | return this.sqlite3.capi.sqlite3_finalize(stmt); 168 | } 169 | 170 | changes(db) { 171 | return this.sqlite3.capi.sqlite3_changes(db); 172 | } 173 | 174 | clear_bindings(stmt) { 175 | return this.sqlite3.capi.sqlite3_clear_bindings(stmt); 176 | } 177 | 178 | reset(stmt) { 179 | return this.sqlite3.capi.sqlite3_reset(stmt); 180 | } 181 | 182 | close(db) { 183 | return this.sqlite3.capi.sqlite3_close_v2(db.pointer); 184 | } 185 | 186 | db_handle(stmt) { 187 | return this.sqlite3.capi.sqlite3_db_handle(stmt); 188 | } 189 | 190 | prepare_v3(db, sql, nByte, prepFlags, ppStmt, pzTail) { 191 | return this.sqlite3.capi.sqlite3_prepare_v3( 192 | db.pointer, 193 | sql, 194 | nByte, 195 | prepFlags, 196 | ppStmt, 197 | pzTail, 198 | ); 199 | } 200 | 201 | step(stmt) { 202 | return this.sqlite3.capi.sqlite3_step(stmt); 203 | } 204 | 205 | column_value(stmt, i) { 206 | return this.sqlite3.capi.sqlite3_column_value(stmt, i); 207 | } 208 | 209 | column_name(stmt, idx) { 210 | return this.sqlite3.capi.sqlite3_column_name(stmt, idx); 211 | } 212 | 213 | column_count(stmt) { 214 | return this.sqlite3.capi.sqlite3_column_count(stmt); 215 | } 216 | 217 | create_function( 218 | database, 219 | functionName, 220 | nArg, 221 | textRep, 222 | pApp, 223 | xFunc, 224 | xStep, 225 | xFinal, 226 | ) { 227 | try { 228 | this.sqlite3.capi.sqlite3_create_function( 229 | database, 230 | functionName, 231 | nArg, 232 | textRep, 233 | pApp, // pApp is ignored 234 | xFunc, 235 | xStep, 236 | xFinal, 237 | ); 238 | console.log("create function"); 239 | } catch (error) { 240 | console.log("create function err"); 241 | throw error; 242 | } 243 | } 244 | 245 | //TODO: At some point need a way to register functions from rust 246 | //but for now this is fine. 247 | register_diesel_sql_functions(database) { 248 | try { 249 | this.sqlite3.capi.sqlite3_create_function( 250 | database, 251 | "diesel_manage_updated_at", 252 | 1, 253 | this.sqlite3.capi.SQLITE_UTF8, 254 | 0, 255 | async (context, values) => { 256 | const table_name = this.sqlite3.value_text(values[0]); 257 | 258 | database.exec( 259 | context, 260 | `CREATE TRIGGER __diesel_manage_updated_at_${table_name} 261 | AFTER UPDATE ON ${table_name} 262 | FOR EACH ROW WHEN 263 | old.updated_at IS NULL AND 264 | new.updated_at IS NULL OR 265 | old.updated_at == new.updated_at 266 | BEGIN 267 | UPDATE ${table_name} 268 | SET updated_at = CURRENT_TIMESTAMP 269 | WHERE ROWID = new.ROWID; 270 | END`, 271 | (row) => { 272 | log(`------------------------------------`); 273 | log(`Created trigger for ${table_name}`); 274 | log(row); 275 | log(`------------------------------------`); 276 | }, 277 | ); 278 | }, 279 | ); 280 | } catch (error) { 281 | console.log("error creating diesel trigger"); 282 | throw error; 283 | } 284 | } 285 | 286 | value_free(value) { 287 | return this.sqlite3.capi.sqlite3_value_free(value); 288 | } 289 | 290 | sqlite3_serialize(database, z_schema, p_size, m_flags) { 291 | try { 292 | return this.sqlite3.capi.sqlite3_serialize( 293 | database, 294 | z_schema, 295 | p_size, 296 | m_flags, 297 | ); 298 | } catch (error) { 299 | console.log("error serializing"); 300 | throw error; 301 | } 302 | } 303 | 304 | sqlite3_deserialize( 305 | database, 306 | z_schema, 307 | p_data, 308 | sz_database, 309 | sz_buffer, 310 | m_flags, 311 | ) { 312 | try { 313 | return this.sqlite3.capi.sqlite3_deserialize( 314 | database, 315 | z_schema, 316 | p_data, 317 | sz_database, 318 | sz_buffer, 319 | m_flags, 320 | ); 321 | } catch (error) { 322 | console.log("error deserializing"); 323 | throw error; 324 | } 325 | } 326 | 327 | sqlite3_free(_database, arg1) { 328 | return this.sqlite3.capi.sqlite3_free(arg1); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/backend.rs: -------------------------------------------------------------------------------- 1 | //! The WasmSQLite backend 2 | 3 | use super::connection::SqliteBindCollector; 4 | use super::connection::SqliteValue; 5 | use super::query_builder::SqliteQueryBuilder; 6 | use diesel::backend::*; 7 | use diesel::sql_types::TypeMetadata; 8 | 9 | /// The SQLite backend 10 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Default)] 11 | pub struct WasmSqlite; 12 | 13 | /// Determines how a bind parameter is given to SQLite 14 | /// 15 | /// Diesel deals with bind parameters after serialization as opaque blobs of 16 | /// bytes. However, SQLite instead has several functions where it expects the 17 | /// relevant C types. 18 | /// 19 | /// The variants of this struct determine what bytes are expected from 20 | /// `ToSql` impls. 21 | #[allow(missing_debug_implementations)] 22 | #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] 23 | pub enum SqliteType { 24 | /// Bind using `sqlite3_bind_blob` 25 | Binary, 26 | /// Bind using `sqlite3_bind_text` 27 | Text, 28 | /// `bytes` should contain an `f32` 29 | Float, 30 | /// `bytes` should contain an `f64` 31 | Double, 32 | /// `bytes` should contain an `i16` 33 | SmallInt, 34 | /// `bytes` should contain an `i32` 35 | Integer, 36 | /// `bytes` should contain an `i64` 37 | Long, 38 | } 39 | 40 | impl Backend for WasmSqlite { 41 | type QueryBuilder = SqliteQueryBuilder; 42 | type RawValue<'a> = SqliteValue<'a, 'a, 'a>; 43 | type BindCollector<'a> = SqliteBindCollector<'a>; 44 | } 45 | 46 | impl TypeMetadata for WasmSqlite { 47 | type TypeMetadata = SqliteType; 48 | type MetadataLookup = (); 49 | } 50 | 51 | impl SqlDialect for WasmSqlite { 52 | type ReturningClause = SqliteReturningClause; 53 | 54 | type OnConflictClause = SqliteOnConflictClause; 55 | 56 | type InsertWithDefaultKeyword = 57 | sql_dialect::default_keyword_for_insert::DoesNotSupportDefaultKeyword; 58 | type BatchInsertSupport = SqliteBatchInsert; 59 | type ConcatClause = sql_dialect::concat_clause::ConcatWithPipesClause; 60 | type DefaultValueClauseForInsert = sql_dialect::default_value_clause::AnsiDefaultValueClause; 61 | 62 | type EmptyFromClauseSyntax = sql_dialect::from_clause_syntax::AnsiSqlFromClauseSyntax; 63 | type SelectStatementSyntax = sql_dialect::select_statement_syntax::AnsiSqlSelectStatement; 64 | 65 | type ExistsSyntax = sql_dialect::exists_syntax::AnsiSqlExistsSyntax; 66 | type ArrayComparison = sql_dialect::array_comparison::AnsiSqlArrayComparison; 67 | type AliasSyntax = sql_dialect::alias_syntax::AsAliasSyntax; 68 | } 69 | 70 | impl DieselReserveSpecialization for WasmSqlite {} 71 | impl TrustedBackend for WasmSqlite {} 72 | 73 | #[derive(Debug, Copy, Clone)] 74 | pub struct SqliteOnConflictClause; 75 | 76 | impl sql_dialect::on_conflict_clause::SupportsOnConflictClause for SqliteOnConflictClause {} 77 | impl sql_dialect::on_conflict_clause::PgLikeOnConflictClause for SqliteOnConflictClause {} 78 | 79 | #[derive(Debug, Copy, Clone)] 80 | pub struct SqliteBatchInsert; 81 | 82 | #[derive(Debug, Copy, Clone)] 83 | pub struct SqliteReturningClause; 84 | 85 | impl sql_dialect::returning_clause::SupportsReturningClause for SqliteReturningClause {} 86 | -------------------------------------------------------------------------------- /src/connection/bind_collector.rs: -------------------------------------------------------------------------------- 1 | use crate::{SqliteType, WasmSqlite}; 2 | use diesel::{ 3 | query_builder::{BindCollector, MoveableBindCollector}, 4 | result::QueryResult, 5 | serialize::{IsNull, Output}, 6 | sql_types::HasSqlType, 7 | }; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Debug, Default)] 11 | pub struct SqliteBindCollector<'a> { 12 | pub(crate) binds: Vec<(InternalSqliteBindValue<'a>, SqliteType)>, 13 | } 14 | 15 | impl SqliteBindCollector<'_> { 16 | pub(crate) fn new() -> Self { 17 | Self { binds: Vec::new() } 18 | } 19 | } 20 | 21 | /// This type represents a value bound to 22 | /// a sqlite prepared statement 23 | /// 24 | /// It can be constructed via the various `From` implementations 25 | #[derive(Debug)] 26 | pub struct SqliteBindValue<'a> { 27 | pub(crate) inner: InternalSqliteBindValue<'a>, 28 | } 29 | 30 | impl<'a> From for SqliteBindValue<'a> { 31 | fn from(i: i32) -> Self { 32 | Self { 33 | inner: InternalSqliteBindValue::I32(i), 34 | } 35 | } 36 | } 37 | 38 | impl<'a> From for SqliteBindValue<'a> { 39 | fn from(i: i64) -> Self { 40 | Self { 41 | inner: InternalSqliteBindValue::I64(i), 42 | } 43 | } 44 | } 45 | 46 | impl<'a> From for SqliteBindValue<'a> { 47 | fn from(f: f64) -> Self { 48 | Self { 49 | inner: InternalSqliteBindValue::F64(f), 50 | } 51 | } 52 | } 53 | 54 | impl<'a, T> From> for SqliteBindValue<'a> 55 | where 56 | T: Into>, 57 | { 58 | fn from(o: Option) -> Self { 59 | match o { 60 | Some(v) => v.into(), 61 | None => Self { 62 | inner: InternalSqliteBindValue::Null, 63 | }, 64 | } 65 | } 66 | } 67 | 68 | impl<'a> From<&'a str> for SqliteBindValue<'a> { 69 | fn from(s: &'a str) -> Self { 70 | Self { 71 | inner: InternalSqliteBindValue::BorrowedString(s), 72 | } 73 | } 74 | } 75 | 76 | impl<'a> From for SqliteBindValue<'a> { 77 | fn from(s: String) -> Self { 78 | Self { 79 | inner: InternalSqliteBindValue::String(s), 80 | } 81 | } 82 | } 83 | 84 | impl<'a> From> for SqliteBindValue<'a> { 85 | fn from(b: Vec) -> Self { 86 | Self { 87 | inner: InternalSqliteBindValue::Binary(b), 88 | } 89 | } 90 | } 91 | 92 | impl<'a> From<&'a [u8]> for SqliteBindValue<'a> { 93 | fn from(b: &'a [u8]) -> Self { 94 | Self { 95 | inner: InternalSqliteBindValue::BorrowedBinary(b), 96 | } 97 | } 98 | } 99 | 100 | #[derive(Debug, Serialize, Deserialize)] 101 | #[serde(untagged)] 102 | pub(crate) enum InternalSqliteBindValue<'a> { 103 | BorrowedString(&'a str), 104 | String(String), 105 | BorrowedBinary(&'a [u8]), 106 | Binary(Vec), 107 | I32(i32), 108 | I64(i64), 109 | F64(f64), 110 | Null, 111 | } 112 | 113 | impl std::fmt::Display for InternalSqliteBindValue<'_> { 114 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 115 | let n = match self { 116 | InternalSqliteBindValue::BorrowedString(_) | InternalSqliteBindValue::String(_) => { 117 | "Text" 118 | } 119 | InternalSqliteBindValue::BorrowedBinary(_) | InternalSqliteBindValue::Binary(_) => { 120 | "Binary" 121 | } 122 | InternalSqliteBindValue::I32(_) | InternalSqliteBindValue::I64(_) => "Integer", 123 | InternalSqliteBindValue::F64(_) => "Float", 124 | InternalSqliteBindValue::Null => "Null", 125 | }; 126 | f.write_str(n) 127 | } 128 | } 129 | /* 130 | impl InternalSqliteBindValue<'_> { 131 | #[allow(unsafe_code)] // ffi function calls 132 | pub(crate) fn result_of(self, ctx: &mut i32) { 133 | let sqlite3 = crate::get_sqlite_unchecked(); 134 | match self { 135 | InternalSqliteBindValue::BorrowedString(s) => sqlite3.result_text(*ctx, s.to_string()), 136 | InternalSqliteBindValue::String(s) => sqlite3.result_text(*ctx, s.to_string()), 137 | InternalSqliteBindValue::Binary(b) => sqlite3.result_blob(*ctx, b.to_vec()), 138 | InternalSqliteBindValue::BorrowedBinary(b) => sqlite3.result_blob(*ctx, b.to_vec()), 139 | InternalSqliteBindValue::I32(i) => sqlite3.result_int(*ctx, i), 140 | InternalSqliteBindValue::I64(l) => sqlite3.result_int64(*ctx, l), 141 | InternalSqliteBindValue::F64(d) => sqlite3.result_double(*ctx, d), 142 | InternalSqliteBindValue::Null => sqlite3.result_null(*ctx), 143 | } 144 | } 145 | } 146 | */ 147 | 148 | impl<'a> BindCollector<'a, WasmSqlite> for SqliteBindCollector<'a> { 149 | type Buffer = SqliteBindValue<'a>; 150 | 151 | fn push_bound_value(&mut self, bind: &'a U, metadata_lookup: &mut ()) -> QueryResult<()> 152 | where 153 | WasmSqlite: diesel::sql_types::HasSqlType, 154 | U: diesel::serialize::ToSql + ?Sized, 155 | { 156 | let value = SqliteBindValue { 157 | inner: InternalSqliteBindValue::Null, 158 | }; 159 | let mut to_sql_output = Output::new(value, metadata_lookup); 160 | let is_null = bind 161 | .to_sql(&mut to_sql_output) 162 | .map_err(diesel::result::Error::SerializationError)?; 163 | let bind = to_sql_output.into_inner(); 164 | let metadata = WasmSqlite::metadata(metadata_lookup); 165 | 166 | self.binds.push(( 167 | match is_null { 168 | IsNull::No => bind.inner, 169 | IsNull::Yes => InternalSqliteBindValue::Null, 170 | }, 171 | metadata, 172 | )); 173 | Ok(()) 174 | } 175 | 176 | fn push_null_value(&mut self, metadata: SqliteType) -> QueryResult<()> { 177 | self.binds.push((InternalSqliteBindValue::Null, metadata)); 178 | Ok(()) 179 | } 180 | } 181 | 182 | #[derive(Debug, Serialize, Deserialize)] 183 | #[serde(untagged)] 184 | pub enum OwnedSqliteBindValue { 185 | String(String), 186 | Binary(Vec), 187 | I32(i32), 188 | I64(i64), 189 | F64(f64), 190 | Null, 191 | } 192 | 193 | impl<'a> std::convert::From<&InternalSqliteBindValue<'a>> for OwnedSqliteBindValue { 194 | fn from(value: &InternalSqliteBindValue<'a>) -> Self { 195 | match value { 196 | InternalSqliteBindValue::String(s) => Self::String(s.clone()), 197 | InternalSqliteBindValue::BorrowedString(s) => Self::String(String::from(*s)), 198 | InternalSqliteBindValue::Binary(b) => Self::Binary(b.clone()), 199 | InternalSqliteBindValue::BorrowedBinary(s) => Self::Binary(Vec::from(*s)), 200 | InternalSqliteBindValue::I32(val) => Self::I32(*val), 201 | InternalSqliteBindValue::I64(val) => Self::I64(*val), 202 | InternalSqliteBindValue::F64(val) => Self::F64(*val), 203 | InternalSqliteBindValue::Null => Self::Null, 204 | } 205 | } 206 | } 207 | 208 | impl<'a> std::convert::From<&OwnedSqliteBindValue> for InternalSqliteBindValue<'a> { 209 | fn from(value: &OwnedSqliteBindValue) -> Self { 210 | match value { 211 | OwnedSqliteBindValue::String(s) => Self::String(s.clone()), 212 | OwnedSqliteBindValue::Binary(b) => Self::Binary(b.clone()), 213 | OwnedSqliteBindValue::I32(val) => Self::I32(*val), 214 | OwnedSqliteBindValue::I64(val) => Self::I64(*val), 215 | OwnedSqliteBindValue::F64(val) => Self::F64(*val), 216 | OwnedSqliteBindValue::Null => Self::Null, 217 | } 218 | } 219 | } 220 | 221 | #[derive(Debug)] 222 | /// Sqlite bind collector data that is movable across threads 223 | pub struct SqliteBindCollectorData { 224 | pub binds: Vec<(OwnedSqliteBindValue, SqliteType)>, 225 | } 226 | 227 | impl MoveableBindCollector for SqliteBindCollector<'_> { 228 | type BindData = SqliteBindCollectorData; 229 | 230 | fn moveable(&self) -> Self::BindData { 231 | let mut binds = Vec::with_capacity(self.binds.len()); 232 | for b in self 233 | .binds 234 | .iter() 235 | .map(|(bind, tpe)| (OwnedSqliteBindValue::from(bind), *tpe)) 236 | { 237 | binds.push(b); 238 | } 239 | SqliteBindCollectorData { binds } 240 | } 241 | 242 | fn append_bind_data(&mut self, from: &Self::BindData) { 243 | self.binds.reserve_exact(from.binds.len()); 244 | self.binds.extend( 245 | from.binds 246 | .iter() 247 | .map(|(bind, tpe)| (InternalSqliteBindValue::from(bind), *tpe)), 248 | ); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/connection/diesel_manage_updated_at.sql: -------------------------------------------------------------------------------- 1 | CREATE TRIGGER __diesel_manage_updated_at_{table_name} 2 | AFTER UPDATE ON {table_name} 3 | FOR EACH ROW WHEN 4 | old.updated_at IS NULL AND 5 | new.updated_at IS NULL OR 6 | old.updated_at == new.updated_at 7 | BEGIN 8 | UPDATE {table_name} 9 | SET updated_at = CURRENT_TIMESTAMP 10 | WHERE ROWID = new.ROWID; 11 | END 12 | -------------------------------------------------------------------------------- /src/connection/err.rs: -------------------------------------------------------------------------------- 1 | use crate::ffi; 2 | use diesel::result::Error::DatabaseError; 3 | use diesel::result::*; 4 | use wasm_bindgen::JsValue; 5 | 6 | pub(super) fn error_message(code: i32) -> String { 7 | let sqlite3 = crate::get_sqlite_unchecked(); 8 | sqlite3.errstr(code) 9 | } 10 | 11 | pub(super) fn ensure_sqlite_ok(code: i32, raw_connection: &JsValue) -> QueryResult<()> { 12 | if code == *ffi::SQLITE_OK { 13 | Ok(()) 14 | } else { 15 | Err(last_error(raw_connection)) 16 | } 17 | } 18 | 19 | pub(super) fn last_error(raw_connection: &JsValue) -> diesel::result::Error { 20 | let error_message = last_error_message(raw_connection); 21 | let error_information = Box::new(error_message); 22 | let error_kind = match last_error_code(raw_connection) { 23 | e if *ffi::SQLITE_CONSTRAINT_UNIQUE | *ffi::SQLITE_CONSTRAINT_PRIMARYKEY == e => { 24 | DatabaseErrorKind::UniqueViolation 25 | } 26 | e if *ffi::SQLITE_CONSTRAINT_FOREIGNKEY == e => DatabaseErrorKind::ForeignKeyViolation, 27 | e if *ffi::SQLITE_CONSTRAINT_NOTNULL == e => DatabaseErrorKind::NotNullViolation, 28 | e if *ffi::SQLITE_CONSTRAINT_CHECK == e => DatabaseErrorKind::CheckViolation, 29 | _ => DatabaseErrorKind::Unknown, 30 | }; 31 | DatabaseError(error_kind, error_information) 32 | } 33 | 34 | fn last_error_message(conn: &JsValue) -> String { 35 | let sqlite3 = crate::get_sqlite_unchecked(); 36 | sqlite3.errmsg(conn) 37 | } 38 | 39 | fn last_error_code(conn: &JsValue) -> i32 { 40 | let sqlite3 = crate::get_sqlite_unchecked(); 41 | sqlite3.extended_errcode(conn) 42 | } 43 | -------------------------------------------------------------------------------- /src/connection/functions.rs: -------------------------------------------------------------------------------- 1 | use super::raw::RawConnection; 2 | use super::row::PrivateSqliteRow; 3 | use super::{/*SqliteAggregateFunction,*/ SqliteBindValue, WasmSqlite}; 4 | use crate::connection::bind_collector::InternalSqliteBindValue; 5 | use crate::connection::sqlite_value::OwnedSqliteValue; 6 | use crate::connection::SqliteValue; 7 | use diesel::backend::Backend; 8 | use diesel::deserialize::{FromSqlRow, StaticallySizedRow}; 9 | use diesel::result::{DatabaseErrorKind, Error, QueryResult}; 10 | use diesel::row::{Field, PartialRow, Row, RowIndex, RowSealed}; 11 | use diesel::serialize::{IsNull, Output, ToSql}; 12 | use diesel::sql_types::HasSqlType; 13 | use std::cell::{Ref, RefCell}; 14 | use std::marker::PhantomData; 15 | use std::mem::ManuallyDrop; 16 | use std::ops::DerefMut; 17 | use std::rc::Rc; 18 | use wasm_bindgen::JsValue; 19 | 20 | pub(super) fn register( 21 | conn: &RawConnection, 22 | fn_name: &str, 23 | deterministic: bool, 24 | mut f: F, 25 | ) -> QueryResult<()> 26 | where 27 | F: FnMut(&RawConnection, Args) -> QueryResult, 28 | Args: FromSqlRow + StaticallySizedRow, 29 | Ret: ToSql, 30 | WasmSqlite: HasSqlType, 31 | { 32 | let fields_needed = Args::FIELD_COUNT; 33 | if fields_needed > 127 { 34 | return Err(Error::DatabaseError( 35 | DatabaseErrorKind::UnableToSendCommand, 36 | Box::new("SQLite functions cannot take more than 127 parameters".to_string()), 37 | )); 38 | } 39 | 40 | conn.register_sql_function(fn_name, fields_needed, deterministic, move |conn, args| { 41 | async { 42 | let args = build_sql_function_args::(args)?; 43 | let conn = RawConnection { 44 | internal_connection: conn, 45 | }; 46 | Ok(f(&conn, args)) 47 | } 48 | .boxed() 49 | })?; 50 | Ok(()) 51 | } 52 | 53 | /* 54 | pub(super) fn register_noargs( 55 | conn: &RawConnection, 56 | fn_name: &str, 57 | deterministic: bool, 58 | mut f: F, 59 | ) -> QueryResult<()> 60 | where 61 | F: FnMut() -> Ret + std::panic::UnwindSafe + Send + 'static, 62 | Ret: ToSql, 63 | WasmSqlite: HasSqlType, 64 | { 65 | conn.register_sql_function(fn_name, 0, deterministic, move |_, _| Ok(f()))?; 66 | Ok(()) 67 | } 68 | 69 | pub(super) fn register_aggregate( 70 | conn: &RawConnection, 71 | fn_name: &str, 72 | ) -> QueryResult<()> 73 | where 74 | A: SqliteAggregateFunction + 'static + Send + std::panic::UnwindSafe, 75 | Args: FromSqlRow + StaticallySizedRow, 76 | Ret: ToSql, 77 | WasmSqlite: HasSqlType, 78 | { 79 | let fields_needed = Args::FIELD_COUNT; 80 | if fields_needed > 127 { 81 | return Err(Error::DatabaseError( 82 | DatabaseErrorKind::UnableToSendCommand, 83 | Box::new("SQLite functions cannot take more than 127 parameters".to_string()), 84 | )); 85 | } 86 | 87 | conn.register_aggregate_function::( 88 | fn_name, 89 | fields_needed, 90 | )?; 91 | 92 | Ok(()) 93 | } 94 | */ 95 | 96 | pub(super) fn build_sql_function_args(args: Vec) -> Result 97 | where 98 | Args: FromSqlRow, 99 | { 100 | let row = FunctionRow::new(args); 101 | Args::build_from_row(&row).map_err(Error::DeserializationError) 102 | } 103 | 104 | // clippy is wrong here, the let binding is required 105 | // for lifetime reasons 106 | #[allow(clippy::let_unit_value)] 107 | pub(super) fn process_sql_function_result( 108 | result: &'_ Ret, 109 | ) -> QueryResult> 110 | where 111 | Ret: ToSql, 112 | WasmSqlite: HasSqlType, 113 | { 114 | let mut metadata_lookup = (); 115 | let value = SqliteBindValue { 116 | inner: InternalSqliteBindValue::Null, 117 | }; 118 | let mut buf = Output::new(value, &mut metadata_lookup); 119 | let is_null = result.to_sql(&mut buf).map_err(Error::SerializationError)?; 120 | 121 | if let IsNull::Yes = is_null { 122 | Ok(InternalSqliteBindValue::Null) 123 | } else { 124 | Ok(buf.into_inner().inner) 125 | } 126 | } 127 | 128 | struct FunctionRow<'a> { 129 | // we use `ManuallyDrop` to prevent dropping the content of the internal vector 130 | // as this buffer is owned by sqlite not by diesel 131 | args: Rc>>>, 132 | field_count: usize, 133 | marker: PhantomData<&'a JsValue>, 134 | } 135 | 136 | impl<'a> Drop for FunctionRow<'a> { 137 | #[allow(unsafe_code)] // manual drop calls 138 | fn drop(&mut self) { 139 | if let Some(args) = Rc::get_mut(&mut self.args) { 140 | if let PrivateSqliteRow::Duplicated { column_names, .. } = 141 | DerefMut::deref_mut(RefCell::get_mut(args)) 142 | { 143 | if Rc::strong_count(column_names) == 1 { 144 | // According the https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html#method.drop 145 | // it's fine to just drop the values here 146 | unsafe { std::ptr::drop_in_place(column_names as *mut _) } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | impl<'a> FunctionRow<'a> { 154 | #[allow(unsafe_code)] // complicated ptr cast 155 | fn new(args: Vec) -> Self { 156 | let lengths = args.len(); 157 | 158 | Self { 159 | field_count: lengths, 160 | args: Rc::new(RefCell::new(ManuallyDrop::new( 161 | PrivateSqliteRow::Duplicated { 162 | values: args 163 | .into_iter() 164 | .map(|a| Some(OwnedSqliteValue { value: a.into() })) 165 | .collect(), 166 | column_names: Rc::from(vec![None; lengths]), 167 | }, 168 | ))), 169 | marker: PhantomData, 170 | } 171 | } 172 | } 173 | 174 | impl RowSealed for FunctionRow<'_> {} 175 | 176 | impl<'a> Row<'a, WasmSqlite> for FunctionRow<'a> { 177 | type Field<'f> = FunctionArgument<'f> where 'a: 'f, Self: 'f; 178 | type InnerPartialRow = Self; 179 | 180 | fn field_count(&self) -> usize { 181 | self.field_count 182 | } 183 | 184 | fn get<'b, I>(&'b self, idx: I) -> Option> 185 | where 186 | 'a: 'b, 187 | Self: RowIndex, 188 | { 189 | let idx = self.idx(idx)?; 190 | Some(FunctionArgument { 191 | args: self.args.borrow(), 192 | col_idx: idx as i32, 193 | }) 194 | } 195 | 196 | fn partial_row(&self, range: std::ops::Range) -> PartialRow<'_, Self::InnerPartialRow> { 197 | PartialRow::new(self, range) 198 | } 199 | } 200 | 201 | impl<'a> RowIndex for FunctionRow<'a> { 202 | fn idx(&self, idx: usize) -> Option { 203 | if idx < self.field_count() { 204 | Some(idx) 205 | } else { 206 | None 207 | } 208 | } 209 | } 210 | 211 | impl<'a, 'b> RowIndex<&'a str> for FunctionRow<'b> { 212 | fn idx(&self, _idx: &'a str) -> Option { 213 | None 214 | } 215 | } 216 | 217 | struct FunctionArgument<'a> { 218 | args: Ref<'a, ManuallyDrop>>, 219 | col_idx: i32, 220 | } 221 | 222 | impl<'a> Field<'a, WasmSqlite> for FunctionArgument<'a> { 223 | fn field_name(&self) -> Option<&str> { 224 | None 225 | } 226 | 227 | fn is_null(&self) -> bool { 228 | self.value().is_none() 229 | } 230 | 231 | fn value(&self) -> Option<::RawValue<'_>> { 232 | SqliteValue::new( 233 | Ref::map(Ref::clone(&self.args), |drop| std::ops::Deref::deref(drop)), 234 | self.col_idx, 235 | ) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/connection/mod.rs: -------------------------------------------------------------------------------- 1 | mod bind_collector; 2 | mod err; 3 | mod owned_row; 4 | mod raw; 5 | mod row; 6 | mod serialized_database; 7 | mod sqlite_value; 8 | mod statement_iterator; 9 | mod stmt; 10 | 11 | pub(crate) use self::bind_collector::SqliteBindCollector; 12 | pub use self::bind_collector::SqliteBindValue; 13 | pub use self::sqlite_value::SqliteValue; 14 | // pub use self::serialized_database::SerializedDatabase; 15 | 16 | use self::raw::RawConnection; 17 | pub use self::statement_iterator::*; 18 | use self::stmt::{Statement, StatementUse}; 19 | use err::*; 20 | // use diesel::connection::DynInstrumentation; 21 | use diesel::{ 22 | connection::WithMetadataLookup, 23 | connection::{statement_cache::StatementCache, DefaultLoadingMode, LoadConnection}, 24 | expression::QueryMetadata, 25 | query_builder::Query, 26 | result::*, 27 | sql_types::TypeMetadata, 28 | RunQueryDsl, 29 | }; 30 | 31 | use diesel::{ 32 | connection::{ 33 | AnsiTransactionManager, Connection, ConnectionSealed, Instrumentation, SimpleConnection, 34 | TransactionManager, 35 | }, 36 | query_builder::{QueryFragment, QueryId}, 37 | QueryResult, 38 | }; 39 | use serialized_database::SerializedDatabase; 40 | 41 | use crate::{get_sqlite_unchecked, WasmSqlite, WasmSqliteError}; 42 | 43 | // This relies on the invariant that RawConnection or Statement are never 44 | // leaked. If a reference to one of those was held on a different thread, this 45 | // would not be thread safe. 46 | // Web is in one thread. Web workers can establish & hold a WasmSqliteConnection 47 | // separately. 48 | #[allow(unsafe_code)] 49 | unsafe impl Send for WasmSqliteConnection {} 50 | 51 | pub struct WasmSqliteConnection { 52 | // statement_cache needs to be before raw_connection 53 | // otherwise we will get errors about open statements before closing the 54 | // connection itself 55 | statement_cache: StatementCache, 56 | raw_connection: RawConnection, 57 | transaction_manager: AnsiTransactionManager, 58 | metadata_lookup: (), 59 | // this exists for the sole purpose of implementing `WithMetadataLookup` trait 60 | // and avoiding static mut which will be deprecated in 2024 edition 61 | instrumentation: Box, 62 | } 63 | 64 | impl ConnectionSealed for WasmSqliteConnection {} 65 | 66 | impl SimpleConnection for WasmSqliteConnection { 67 | fn batch_execute(&mut self, query: &str) -> diesel::prelude::QueryResult<()> { 68 | get_sqlite_unchecked() 69 | .exec(&self.raw_connection.internal_connection, query) 70 | .map_err(WasmSqliteError::from) 71 | .map_err(Into::into) 72 | } 73 | } 74 | 75 | impl Connection for WasmSqliteConnection { 76 | type Backend = WasmSqlite; 77 | type TransactionManager = AnsiTransactionManager; 78 | 79 | fn establish(database_url: &str) -> ConnectionResult { 80 | WasmSqliteConnection::establish_inner(database_url) 81 | } 82 | 83 | fn execute_returning_count(&mut self, source: &T) -> QueryResult 84 | where 85 | T: QueryFragment + QueryId, 86 | { 87 | let statement_use = self.prepared_query(source)?; 88 | statement_use 89 | .run() 90 | .map(|_| self.raw_connection.rows_affected_by_last_query()) 91 | } 92 | 93 | fn set_instrumentation(&mut self, instrumentation: impl Instrumentation) { 94 | self.instrumentation = Box::new(instrumentation); 95 | } 96 | 97 | fn instrumentation(&mut self) -> &mut dyn Instrumentation { 98 | self.instrumentation.as_mut() 99 | } 100 | 101 | fn transaction_state(&mut self) -> &mut AnsiTransactionManager 102 | where 103 | Self: Sized, 104 | { 105 | &mut self.transaction_manager 106 | } 107 | 108 | fn set_prepared_statement_cache_size(&mut self, size: diesel::connection::CacheSize) { 109 | self.statement_cache.set_cache_size(size) 110 | } 111 | } 112 | 113 | impl LoadConnection for WasmSqliteConnection { 114 | type Cursor<'conn, 'query> = StatementIterator<'conn, 'query>; 115 | type Row<'conn, 'query> = self::row::SqliteRow<'conn, 'query>; 116 | 117 | fn load<'conn, 'query, T>( 118 | &'conn mut self, 119 | source: T, 120 | ) -> QueryResult> 121 | where 122 | T: Query + QueryFragment + QueryId + 'query, 123 | Self::Backend: QueryMetadata, 124 | { 125 | let statement = self.prepared_query(source)?; 126 | 127 | Ok(StatementIterator::new(statement)) 128 | } 129 | } 130 | 131 | impl WithMetadataLookup for WasmSqliteConnection { 132 | fn metadata_lookup(&mut self) -> &mut ::MetadataLookup { 133 | &mut self.metadata_lookup 134 | } 135 | } 136 | 137 | impl diesel::migration::MigrationConnection for WasmSqliteConnection { 138 | fn setup(&mut self) -> QueryResult { 139 | use diesel::RunQueryDsl; 140 | diesel::sql_query(diesel::migration::CREATE_MIGRATIONS_TABLE).execute(self) 141 | } 142 | } 143 | 144 | #[derive(diesel::QueryId)] 145 | pub(crate) struct CheckConnectionQuery; 146 | 147 | impl QueryFragment for CheckConnectionQuery 148 | where 149 | DB: diesel::backend::Backend, 150 | { 151 | fn walk_ast<'b>( 152 | &'b self, 153 | mut pass: diesel::query_builder::AstPass<'_, 'b, DB>, 154 | ) -> QueryResult<()> { 155 | pass.push_sql("SELECT 1"); 156 | Ok(()) 157 | } 158 | } 159 | 160 | impl Query for CheckConnectionQuery { 161 | type SqlType = diesel::sql_types::Integer; 162 | } 163 | 164 | impl RunQueryDsl for CheckConnectionQuery {} 165 | 166 | #[cfg(feature = "r2d2")] 167 | impl diesel::r2d2::R2D2Connection for crate::connection::WasmSqliteConnection { 168 | fn ping(&mut self) -> QueryResult<()> { 169 | CheckConnectionQuery.execute(self).map(|_| ()) 170 | } 171 | 172 | fn is_broken(&mut self) -> bool { 173 | AnsiTransactionManager::is_broken_transaction_manager(self) 174 | } 175 | } 176 | 177 | impl diesel::connection::MultiConnectionHelper for WasmSqliteConnection { 178 | fn to_any<'a>( 179 | lookup: &mut ::MetadataLookup, 180 | ) -> &mut (dyn std::any::Any + 'a) { 181 | lookup 182 | } 183 | 184 | fn from_any( 185 | lookup: &mut dyn std::any::Any, 186 | ) -> Option<&mut ::MetadataLookup> { 187 | lookup.downcast_mut() 188 | } 189 | } 190 | 191 | impl WasmSqliteConnection { 192 | /// Run a transaction with `BEGIN IMMEDIATE` 193 | /// 194 | /// This method will return an error if a transaction is already open. 195 | /// 196 | /// # Example 197 | /// 198 | /// ```rust 199 | /// # include!("../../doctest_setup.rs"); 200 | /// # 201 | /// # fn main() { 202 | /// # run_test().unwrap(); 203 | /// # } 204 | /// # 205 | /// # fn run_test() -> QueryResult<()> { 206 | /// # let mut conn = SqliteConnection::establish(":memory:").unwrap(); 207 | /// conn.immediate_transaction(|conn| { 208 | /// // Do stuff in a transaction 209 | /// Ok(()) 210 | /// }) 211 | /// # } 212 | /// ``` 213 | pub fn immediate_transaction(&mut self, f: F) -> Result 214 | where 215 | F: FnOnce(&mut Self) -> Result, 216 | E: From, 217 | { 218 | self.transaction_sql(f, "BEGIN IMMEDIATE") 219 | } 220 | 221 | /// Run a transaction with `BEGIN EXCLUSIVE` 222 | /// 223 | /// This method will return an error if a transaction is already open. 224 | /// 225 | /// # Example 226 | /// 227 | /// ```rust 228 | /// # include!("../../doctest_setup.rs"); 229 | /// # 230 | /// # fn main() { 231 | /// # run_test().unwrap(); 232 | /// # } 233 | /// # 234 | /// # fn run_test() -> QueryResult<()> { 235 | /// # let mut conn = SqliteConnection::establish(":memory:").unwrap(); 236 | /// conn.exclusive_transaction(|conn| { 237 | /// // Do stuff in a transaction 238 | /// Ok(()) 239 | /// }) 240 | /// # } 241 | /// ``` 242 | pub fn exclusive_transaction(&mut self, f: F) -> Result 243 | where 244 | F: FnOnce(&mut Self) -> Result, 245 | E: From, 246 | { 247 | self.transaction_sql(f, "BEGIN EXCLUSIVE") 248 | } 249 | 250 | fn transaction_sql(&mut self, f: F, sql: &str) -> Result 251 | where 252 | F: FnOnce(&mut Self) -> Result, 253 | E: From, 254 | { 255 | AnsiTransactionManager::begin_transaction_sql(&mut *self, sql)?; 256 | match f(&mut *self) { 257 | Ok(value) => { 258 | AnsiTransactionManager::commit_transaction(&mut *self)?; 259 | Ok(value) 260 | } 261 | Err(e) => { 262 | AnsiTransactionManager::rollback_transaction(&mut *self)?; 263 | Err(e) 264 | } 265 | } 266 | } 267 | 268 | fn prepared_query<'conn, 'query, T>( 269 | &'conn mut self, 270 | source: T, 271 | ) -> QueryResult> 272 | where 273 | T: QueryFragment + QueryId + 'query, 274 | { 275 | /* 276 | self.instrumentation 277 | .on_connection_event(InstrumentationEvent::StartQuery { 278 | query: &crate::debug_query(&source), 279 | }); 280 | */ 281 | let WasmSqliteConnection { 282 | ref mut raw_connection, 283 | ref mut statement_cache, 284 | ref mut instrumentation, 285 | .. 286 | } = self; 287 | 288 | let statement = match statement_cache.cached_statement( 289 | &source, 290 | &WasmSqlite, 291 | &[], 292 | raw_connection, 293 | Statement::prepare, 294 | &mut *instrumentation, 295 | ) { 296 | Ok(statement) => statement, 297 | Err(e) => { 298 | /* 299 | self.instrumentation 300 | .on_connection_event(InstrumentationEvent::FinishQuery { 301 | query: &crate::debug_query(&source), 302 | error: Some(&e), 303 | }); 304 | */ 305 | return Err(e); 306 | } 307 | }; 308 | 309 | StatementUse::bind(statement, source, instrumentation.as_mut()) 310 | } 311 | 312 | fn establish_inner(database_url: &str) -> Result { 313 | let sqlite3 = crate::get_sqlite_unchecked(); 314 | let raw_connection = RawConnection::establish(database_url).unwrap(); 315 | tracing::debug!( 316 | "Established database at {}", 317 | sqlite3.filename(&raw_connection.internal_connection, "main".into()) 318 | ); 319 | sqlite3 320 | .register_diesel_sql_functions(&raw_connection.internal_connection) 321 | .map_err(WasmSqliteError::from)?; 322 | Ok(Self { 323 | statement_cache: StatementCache::new(), 324 | raw_connection, 325 | transaction_manager: AnsiTransactionManager::default(), 326 | instrumentation: Box::new(Nothing) as Box, 327 | metadata_lookup: (), 328 | }) 329 | } 330 | 331 | pub fn serialize(&self) -> SerializedDatabase { 332 | self.raw_connection.serialize() 333 | } 334 | 335 | pub fn deserialize(&self, data: &[u8]) -> i32 { 336 | self.raw_connection.deserialize(data) 337 | } 338 | } 339 | 340 | pub struct Nothing; 341 | 342 | impl Instrumentation for Nothing { 343 | fn on_connection_event(&mut self, event: diesel::connection::InstrumentationEvent<'_>) { 344 | tracing::trace!("{:?}", event); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/connection/owned_row.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use super::sqlite_value::{OwnedSqliteValue, SqliteValue}; 4 | use crate::WasmSqlite; 5 | use diesel::{ 6 | backend::Backend, 7 | row::{Field, PartialRow, Row, RowIndex, RowSealed}, 8 | }; 9 | 10 | #[derive(Debug)] 11 | pub struct OwnedSqliteRow { 12 | pub(super) values: Vec>, 13 | column_names: Arc<[Option]>, 14 | } 15 | 16 | impl OwnedSqliteRow { 17 | pub(super) fn new( 18 | values: Vec>, 19 | column_names: Arc<[Option]>, 20 | ) -> Self { 21 | OwnedSqliteRow { 22 | values, 23 | column_names, 24 | } 25 | } 26 | } 27 | 28 | impl RowSealed for OwnedSqliteRow {} 29 | 30 | impl<'a> Row<'a, WasmSqlite> for OwnedSqliteRow { 31 | type Field<'field> = OwnedSqliteField<'field> where 'a: 'field, Self: 'field; 32 | type InnerPartialRow = Self; 33 | 34 | fn field_count(&self) -> usize { 35 | self.values.len() 36 | } 37 | 38 | fn get<'field, I>(&'field self, idx: I) -> Option> 39 | where 40 | 'a: 'field, 41 | Self: RowIndex, 42 | { 43 | let idx = self.idx(idx)?; 44 | Some(OwnedSqliteField { 45 | row: self, 46 | col_idx: i32::try_from(idx).ok()?, 47 | }) 48 | } 49 | 50 | fn partial_row(&self, range: std::ops::Range) -> PartialRow<'_, Self::InnerPartialRow> { 51 | PartialRow::new(self, range) 52 | } 53 | } 54 | 55 | impl RowIndex for OwnedSqliteRow { 56 | fn idx(&self, idx: usize) -> Option { 57 | if idx < self.field_count() { 58 | Some(idx) 59 | } else { 60 | None 61 | } 62 | } 63 | } 64 | 65 | impl<'idx> RowIndex<&'idx str> for OwnedSqliteRow { 66 | fn idx(&self, field_name: &'idx str) -> Option { 67 | self.column_names 68 | .iter() 69 | .position(|n| n.as_ref().map(|s| s as &str) == Some(field_name)) 70 | } 71 | } 72 | 73 | #[allow(missing_debug_implementations)] 74 | pub struct OwnedSqliteField<'row> { 75 | pub(super) row: &'row OwnedSqliteRow, 76 | pub(super) col_idx: i32, 77 | } 78 | 79 | impl<'row> Field<'row, WasmSqlite> for OwnedSqliteField<'row> { 80 | fn field_name(&self) -> Option<&str> { 81 | self.row 82 | .column_names 83 | .get(self.col_idx as usize) 84 | .and_then(|o| o.as_ref().map(|s| s.as_ref())) 85 | } 86 | 87 | fn is_null(&self) -> bool { 88 | self.value().is_none() 89 | } 90 | 91 | fn value(&self) -> Option<::RawValue<'row>> { 92 | SqliteValue::from_owned_row(self.row, self.col_idx) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/connection/raw.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | // functions are needed, but missing functionality means they aren't used yet. 3 | 4 | use crate::{ffi, WasmSqlite, WasmSqliteError}; 5 | use diesel::{result::*, sql_types::HasSqlType}; 6 | use wasm_bindgen::{closure::Closure, JsValue}; 7 | 8 | use super::serialized_database::SerializedDatabase; 9 | 10 | #[allow(missing_copy_implementations)] 11 | pub(super) struct RawConnection { 12 | pub(super) internal_connection: JsValue, 13 | } 14 | 15 | impl RawConnection { 16 | pub(super) fn establish(database_url: &str) -> ConnectionResult { 17 | let sqlite3 = crate::get_sqlite_unchecked(); 18 | let database_url = if database_url.starts_with("sqlite://") { 19 | database_url.replacen("sqlite://", "file:", 1) 20 | } else { 21 | database_url.to_string() 22 | }; 23 | 24 | let capi = sqlite3.inner().capi(); 25 | let flags = 26 | capi.SQLITE_OPEN_READWRITE() | capi.SQLITE_OPEN_CREATE() | capi.SQLITE_OPEN_URI(); 27 | 28 | // TODO: flags are ignored for now 29 | Ok(RawConnection { 30 | internal_connection: sqlite3 31 | .open(&database_url, Some(flags as i32)) 32 | .map_err(WasmSqliteError::from) 33 | .map_err(ConnectionError::from)?, 34 | }) 35 | } 36 | 37 | pub(super) fn exec(&self, query: &str) -> QueryResult<()> { 38 | let sqlite3 = crate::get_sqlite_unchecked(); 39 | sqlite3 40 | .exec(&self.internal_connection, query) 41 | .map_err(WasmSqliteError::from)?; 42 | Ok(()) 43 | } 44 | 45 | pub(super) fn rows_affected_by_last_query(&self) -> usize { 46 | let sqlite3 = crate::get_sqlite_unchecked(); 47 | sqlite3.changes(&self.internal_connection) 48 | } 49 | 50 | pub(super) fn register_sql_function( 51 | &self, 52 | fn_name: &str, 53 | num_args: usize, 54 | deterministic: bool, 55 | f: F, 56 | ) -> QueryResult<()> 57 | where 58 | F: FnMut(JsValue, Vec) -> JsValue + 'static, 59 | WasmSqlite: HasSqlType, 60 | { 61 | let sqlite3 = crate::get_sqlite_unchecked(); 62 | let flags = Self::get_flags(deterministic); 63 | 64 | let cb = Closure::new(f); 65 | sqlite3 66 | .create_function( 67 | &self.internal_connection, 68 | fn_name, 69 | num_args 70 | .try_into() 71 | .expect("usize to i32 panicked in register_sql_function"), 72 | flags, 73 | 0, 74 | Some(&cb), 75 | None, 76 | None, 77 | ) 78 | .unwrap(); 79 | Ok(()) 80 | } 81 | 82 | fn get_flags(deterministic: bool) -> i32 { 83 | let capi = crate::get_sqlite_unchecked().inner().capi(); 84 | let mut flags = capi.SQLITE_UTF8(); 85 | if deterministic { 86 | flags |= capi.SQLITE_DETERMINISTIC(); 87 | } 88 | flags as i32 89 | } 90 | 91 | /// Serializes the database from sqlite to be stored by the user/client. 92 | pub(super) fn serialize(&self) -> SerializedDatabase { 93 | let sqlite3 = crate::get_sqlite_unchecked(); 94 | let wasm = sqlite3.inner().wasm(); 95 | 96 | let p_size = wasm.pstack().alloc(std::mem::size_of::() as u32); 97 | let data_ptr = sqlite3.sqlite3_serialize(&self.internal_connection, "main", &p_size, 0); 98 | if data_ptr.is_null() { 99 | panic!("Serialization failed"); 100 | } 101 | 102 | let len = p_size.as_f64().unwrap() as u32; 103 | unsafe { SerializedDatabase::new(data_ptr, len) } 104 | } 105 | 106 | /// Deserializes the database from the data slice given to be loaded 107 | /// by sqlite in the wasm space. 108 | pub(super) fn deserialize(&self, data: &[u8]) -> i32 { 109 | let sqlite3 = crate::get_sqlite_unchecked(); 110 | let wasm = sqlite3.inner().wasm(); 111 | 112 | // allocate the space in wasm, and copy the buffer to the wasm 113 | // memory space. 114 | let p_data = wasm.alloc(data.len() as u32); 115 | ffi::raw_copy_to_sqlite(data, p_data); 116 | 117 | let result = sqlite3.sqlite3_deserialize( 118 | &self.internal_connection, 119 | "main", 120 | p_data, 121 | data.len() as i64, 122 | data.len() as i64, 123 | 0, 124 | ); 125 | 126 | if result != 0 { 127 | panic!("Deserialization failed"); 128 | } 129 | 130 | result 131 | } 132 | } 133 | 134 | impl Drop for RawConnection { 135 | fn drop(&mut self) { 136 | let sqlite3 = crate::get_sqlite_unchecked(); 137 | 138 | let result = sqlite3.close(&self.internal_connection); 139 | if result != *crate::ffi::SQLITE_OK { 140 | let error_message = super::error_message(result); 141 | panic!("Error closing SQLite connection: {}", error_message); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/connection/row.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Ref, RefCell}; 2 | use std::rc::Rc; 3 | use std::sync::Arc; 4 | 5 | use super::owned_row::OwnedSqliteRow; 6 | use super::sqlite_value::{OwnedSqliteValue, SqliteValue}; 7 | use super::stmt::StatementUse; 8 | use crate::WasmSqlite; 9 | use diesel::{ 10 | backend::Backend, 11 | row::{Field, IntoOwnedRow, PartialRow, Row, RowIndex, RowSealed}, 12 | }; 13 | 14 | #[allow(missing_debug_implementations)] 15 | pub struct SqliteRow<'stmt, 'query> { 16 | pub(super) inner: Rc>>, 17 | pub(super) field_count: usize, 18 | } 19 | 20 | pub(super) enum PrivateSqliteRow<'stmt, 'query> { 21 | Direct(StatementUse<'stmt, 'query>), 22 | Duplicated { 23 | values: Vec>, 24 | column_names: Rc<[Option]>, 25 | }, 26 | } 27 | 28 | impl<'stmt, 'query> IntoOwnedRow<'stmt, WasmSqlite> for SqliteRow<'stmt, 'query> { 29 | type OwnedRow = OwnedSqliteRow; 30 | 31 | type Cache = Option]>>; 32 | 33 | fn into_owned(self, column_name_cache: &mut Self::Cache) -> Self::OwnedRow { 34 | self.inner.borrow().moveable(column_name_cache) 35 | } 36 | } 37 | 38 | impl<'stmt, 'query> PrivateSqliteRow<'stmt, 'query> { 39 | pub(super) fn duplicate( 40 | &mut self, 41 | column_names: &mut Option]>>, 42 | ) -> PrivateSqliteRow<'stmt, 'query> { 43 | match self { 44 | PrivateSqliteRow::Direct(stmt) => { 45 | let column_names = if let Some(column_names) = column_names { 46 | column_names.clone() 47 | } else { 48 | let c: Rc<[Option]> = Rc::from( 49 | (0..stmt.column_count()) 50 | .map(|idx| stmt.field_name(idx).map(|s| s.to_owned())) 51 | .collect::>(), 52 | ); 53 | *column_names = Some(c.clone()); 54 | c 55 | }; 56 | PrivateSqliteRow::Duplicated { 57 | values: (0..stmt.column_count()) 58 | .map(|idx| stmt.copy_value(idx)) 59 | .collect(), 60 | column_names, 61 | } 62 | } 63 | PrivateSqliteRow::Duplicated { 64 | values, 65 | column_names, 66 | } => PrivateSqliteRow::Duplicated { 67 | values: values 68 | .iter() 69 | .map(|v| v.as_ref().map(|v| v.duplicate())) 70 | .collect(), 71 | column_names: column_names.clone(), 72 | }, 73 | } 74 | } 75 | 76 | pub(super) fn moveable( 77 | &self, 78 | column_name_cache: &mut Option]>>, 79 | ) -> OwnedSqliteRow { 80 | match self { 81 | PrivateSqliteRow::Direct(stmt) => { 82 | if column_name_cache.is_none() { 83 | *column_name_cache = Some( 84 | (0..stmt.column_count()) 85 | .map(|idx| stmt.field_name(idx).map(|s| s.to_owned())) 86 | .collect::>() 87 | .into(), 88 | ); 89 | } 90 | let column_names = Arc::clone( 91 | column_name_cache 92 | .as_ref() 93 | .expect("This is initialized above"), 94 | ); 95 | OwnedSqliteRow::new( 96 | (0..stmt.column_count()) 97 | .map(|idx| stmt.copy_value(idx)) 98 | .collect(), 99 | column_names, 100 | ) 101 | } 102 | PrivateSqliteRow::Duplicated { 103 | values, 104 | column_names, 105 | } => { 106 | if column_name_cache.is_none() { 107 | *column_name_cache = Some( 108 | (*column_names) 109 | .iter() 110 | .map(|s| s.to_owned()) 111 | .collect::>() 112 | .into(), 113 | ); 114 | } 115 | let column_names = Arc::clone( 116 | column_name_cache 117 | .as_ref() 118 | .expect("This is initialized above"), 119 | ); 120 | OwnedSqliteRow::new( 121 | values 122 | .iter() 123 | .map(|v| v.as_ref().map(|v| v.duplicate())) 124 | .collect(), 125 | column_names, 126 | ) 127 | } 128 | } 129 | } 130 | } 131 | 132 | impl<'stmt, 'query> RowSealed for SqliteRow<'stmt, 'query> {} 133 | 134 | impl<'stmt, 'query> Row<'stmt, WasmSqlite> for SqliteRow<'stmt, 'query> { 135 | type Field<'field> = SqliteField<'field, 'field> where 'stmt: 'field, Self: 'field; 136 | type InnerPartialRow = Self; 137 | 138 | fn field_count(&self) -> usize { 139 | self.field_count 140 | } 141 | 142 | fn get<'field, I>(&'field self, idx: I) -> Option> 143 | where 144 | 'stmt: 'field, 145 | Self: RowIndex, 146 | { 147 | let idx = self.idx(idx)?; 148 | Some(SqliteField { 149 | row: self.inner.borrow(), 150 | col_idx: i32::try_from(idx).ok()?, 151 | }) 152 | } 153 | 154 | fn partial_row(&self, range: std::ops::Range) -> PartialRow<'_, Self::InnerPartialRow> { 155 | PartialRow::new(self, range) 156 | } 157 | } 158 | 159 | impl<'stmt, 'query> RowIndex for SqliteRow<'stmt, 'query> { 160 | fn idx(&self, idx: usize) -> Option { 161 | if idx < self.field_count { 162 | Some(idx) 163 | } else { 164 | None 165 | } 166 | } 167 | } 168 | 169 | impl<'stmt, 'idx, 'query> RowIndex<&'idx str> for SqliteRow<'stmt, 'query> { 170 | fn idx(&self, field_name: &'idx str) -> Option { 171 | match &mut *self.inner.borrow_mut() { 172 | PrivateSqliteRow::Direct(stmt) => stmt.index_for_column_name(field_name), 173 | PrivateSqliteRow::Duplicated { column_names, .. } => column_names 174 | .iter() 175 | .position(|n| n.as_ref().map(|s| s as &str) == Some(field_name)), 176 | } 177 | } 178 | } 179 | 180 | #[allow(missing_debug_implementations)] 181 | pub struct SqliteField<'stmt, 'query> { 182 | pub(super) row: Ref<'stmt, PrivateSqliteRow<'stmt, 'query>>, 183 | pub(super) col_idx: i32, 184 | } 185 | 186 | impl<'stmt, 'query> Field<'stmt, WasmSqlite> for SqliteField<'stmt, 'query> { 187 | fn field_name(&self) -> Option<&str> { 188 | match &*self.row { 189 | PrivateSqliteRow::Direct(stmt) => stmt.field_name(self.col_idx), 190 | PrivateSqliteRow::Duplicated { column_names, .. } => column_names 191 | .get(self.col_idx as usize) 192 | .and_then(|t| t.as_ref().map(|n| n as &str)), 193 | } 194 | } 195 | 196 | fn is_null(&self) -> bool { 197 | self.value().is_none() 198 | } 199 | 200 | fn value(&self) -> Option<::RawValue<'_>> { 201 | SqliteValue::new(Ref::clone(&self.row), self.col_idx) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/connection/serialized_database.rs: -------------------------------------------------------------------------------- 1 | use crate::ffi; 2 | 3 | /// `SerializedDatabase` is a wrapper for a serialized database that is dynamically allocated by calling `sqlite3_serialize`. 4 | /// This RAII wrapper is necessary to deallocate the memory when it goes out of scope with `sqlite3_free`. 5 | #[derive(Debug)] 6 | pub struct SerializedDatabase { 7 | pub data: Vec, 8 | } 9 | 10 | impl SerializedDatabase { 11 | pub(crate) unsafe fn new(data_ptr: *mut u8, len: u32) -> Self { 12 | let mut data = vec![0; len as usize]; 13 | ffi::raw_copy_from_sqlite(data_ptr, len, data.as_mut_slice()); 14 | 15 | crate::get_sqlite_unchecked().sqlite3_free(data_ptr); 16 | 17 | Self { data } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/connection/sqlite_value.rs: -------------------------------------------------------------------------------- 1 | #![allow(unsafe_code)] // ffi calls 2 | 3 | use std::cell::Ref; 4 | 5 | use crate::backend::SqliteType; 6 | use crate::ffi; 7 | 8 | use super::owned_row::OwnedSqliteRow; 9 | use super::row::PrivateSqliteRow; 10 | 11 | /// Raw sqlite value as received from the database 12 | /// 13 | /// Use existing `FromSql` implementations to convert this into 14 | /// rust values 15 | #[allow(missing_debug_implementations, missing_copy_implementations)] 16 | pub struct SqliteValue<'row, 'stmt, 'query> { 17 | // This field exists to ensure that nobody 18 | // can modify the underlying row while we are 19 | // holding a reference to some row value here 20 | _row: Option>>, 21 | // we extract the raw value pointer as part of the constructor 22 | // to safe the match statements for each method 23 | // According to benchmarks this leads to a ~20-30% speedup 24 | // 25 | // 26 | // This is sound as long as nobody calls `stmt.step()` 27 | // while holding this value. We ensure this by including 28 | // a reference to the row above. 29 | /// We are referencing SQLites WASM memory, 30 | /// so we just have to trust its not null rather than using `NonNull` 31 | /// (i dont think that would work unless its our mem) 32 | value: *mut u8, 33 | } 34 | 35 | #[derive(Debug)] 36 | pub(super) struct OwnedSqliteValue { 37 | // maybe make JsValue? 38 | pub(super) value: *mut u8, 39 | } 40 | 41 | impl Drop for OwnedSqliteValue { 42 | fn drop(&mut self) { 43 | let sqlite3 = crate::get_sqlite_unchecked(); 44 | sqlite3.value_free(self.value) 45 | } 46 | } 47 | 48 | // Unsafe Send impl safe since sqlite3_value is built with sqlite3_value_dup 49 | // see https://www.sqlite.org/c3ref/value.html 50 | unsafe impl Send for OwnedSqliteValue {} 51 | 52 | impl<'row, 'stmt, 'query> SqliteValue<'row, 'stmt, 'query> { 53 | pub(super) fn new( 54 | row: Ref<'row, PrivateSqliteRow<'stmt, 'query>>, 55 | col_idx: i32, 56 | ) -> Option> { 57 | let value = match &*row { 58 | PrivateSqliteRow::Direct(stmt) => stmt.column_value(col_idx), 59 | PrivateSqliteRow::Duplicated { values, .. } => { 60 | values.get(col_idx as usize).and_then(|v| v.as_ref())?.value 61 | } 62 | }; 63 | 64 | let ret = Self { 65 | _row: Some(row), 66 | value, 67 | }; 68 | if ret.value_type().is_none() { 69 | None 70 | } else { 71 | Some(ret) 72 | } 73 | } 74 | 75 | pub(super) fn from_owned_row( 76 | row: &'row OwnedSqliteRow, 77 | col_idx: i32, 78 | ) -> Option> { 79 | let value = row 80 | .values 81 | .get(col_idx as usize) 82 | .and_then(|v| v.as_ref())? 83 | .value; 84 | let ret = Self { _row: None, value }; 85 | if ret.value_type().is_none() { 86 | None 87 | } else { 88 | Some(ret) 89 | } 90 | } 91 | 92 | pub(crate) fn read_text(&self) -> String { 93 | self.parse_string(|s| s) 94 | } 95 | 96 | // TODO: If we share memory with SQLITE, we can return a &'value str here rathre than an 97 | // allocated String 98 | pub(crate) fn parse_string(&self, f: impl FnOnce(String) -> R) -> R { 99 | let sqlite3 = crate::get_sqlite_unchecked(); 100 | // TODO: 101 | // for some reason sqlite3_value_text returns the String and not a 102 | // pointer. There's probably a way to make it return a pointer 103 | let s = sqlite3.value_text(self.value); 104 | // let s = unsafe { 105 | // let ptr = sqlite3.value_text(self.value); 106 | // let len = sqlite3.value_bytes(self.value); 107 | // let mut bytes = Vec::with_capacity(len as usize); 108 | // ffi::raw_copy_from_sqlite(ptr, len, bytes.as_mut_slice()); 109 | // unsafe { bytes.set_len(len) }; // not sure we need this 110 | // String::from_utf8_unchecked(bytes) 111 | // }; 112 | f(s) 113 | } 114 | 115 | pub(crate) fn read_blob(&self) -> Vec { 116 | let sqlite3 = crate::get_sqlite_unchecked(); 117 | unsafe { 118 | let ptr = sqlite3.value_blob(self.value); 119 | let len = sqlite3.value_bytes(self.value); 120 | let mut bytes = Vec::with_capacity(len as usize); 121 | bytes.set_len(len as usize); 122 | ffi::raw_copy_from_sqlite(ptr, len, bytes.as_mut_slice()); 123 | bytes 124 | } 125 | } 126 | 127 | pub(crate) fn read_integer(&self) -> i32 { 128 | let sqlite3 = crate::get_sqlite_unchecked(); 129 | sqlite3.value_int(self.value) 130 | } 131 | 132 | pub(crate) fn read_long(&self) -> i64 { 133 | let sqlite3 = crate::get_sqlite_unchecked(); 134 | sqlite3.value_int64(self.value) 135 | } 136 | 137 | pub(crate) fn read_double(&self) -> f64 { 138 | let sqlite3 = crate::get_sqlite_unchecked(); 139 | sqlite3.value_double(self.value) 140 | } 141 | 142 | /// Get the type of the value as returned by sqlite 143 | pub fn value_type(&self) -> Option { 144 | let sqlite3 = crate::get_sqlite_unchecked(); 145 | let tpe = sqlite3.value_type(self.value); 146 | 147 | match tpe { 148 | _ if *ffi::SQLITE_TEXT == tpe => Some(SqliteType::Text), 149 | _ if *ffi::SQLITE_INTEGER == tpe => Some(SqliteType::Long), 150 | _ if *ffi::SQLITE_FLOAT == tpe => Some(SqliteType::Double), 151 | _ if *ffi::SQLITE_BLOB == tpe => Some(SqliteType::Binary), 152 | _ if *ffi::SQLITE_NULL == tpe => None, 153 | _ => unreachable!( 154 | "Sqlite's documentation state that this case ({}) is not reachable. \ 155 | If you ever see this error message please open an issue at \ 156 | https://github.com/diesel-rs/diesel.", 157 | tpe 158 | ), 159 | } 160 | } 161 | } 162 | 163 | impl OwnedSqliteValue { 164 | pub(super) fn copy_from_ptr(ptr: *mut u8) -> Option { 165 | let sqlite3 = crate::get_sqlite_unchecked(); 166 | let tpe = sqlite3.value_type(ptr); 167 | if *ffi::SQLITE_NULL == tpe { 168 | return None; 169 | } 170 | 171 | let value = sqlite3.value_dup(ptr); 172 | 173 | Some(Self { value }) 174 | } 175 | 176 | pub(super) fn duplicate(&self) -> OwnedSqliteValue { 177 | let sqlite3 = crate::get_sqlite_unchecked(); 178 | let value = sqlite3.value_dup(self.value); 179 | OwnedSqliteValue { value } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/connection/statement_iterator.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use super::row::{PrivateSqliteRow, SqliteRow}; 5 | use super::stmt::StatementUse; 6 | use diesel::result::QueryResult; 7 | 8 | #[allow(missing_debug_implementations)] 9 | pub struct StatementIterator<'stmt, 'query> { 10 | inner: PrivateStatementIterator<'stmt, 'query>, 11 | column_names: Option]>>, 12 | field_count: usize, 13 | } 14 | 15 | impl<'stmt, 'query> StatementIterator<'stmt, 'query> { 16 | #[cold] 17 | fn handle_duplicated_row_case( 18 | outer_last_row: &mut Rc>>, 19 | column_names: &mut Option]>>, 20 | field_count: usize, 21 | ) -> Option>> { 22 | // We don't own the statement. There is another existing reference, likely because 23 | // a user stored the row in some long time container before calling next another time 24 | // In this case we copy out the current values into a temporary store and advance 25 | // the statement iterator internally afterwards 26 | let last_row = { 27 | let mut last_row = match outer_last_row.try_borrow_mut() { 28 | Ok(o) => o, 29 | Err(_e) => { 30 | return Some(Err(diesel::result::Error::DeserializationError( 31 | "Failed to reborrow row. Try to release any `SqliteField` or `SqliteValue` \ 32 | that exists at this point" 33 | .into(), 34 | ))); 35 | } 36 | }; 37 | let last_row = &mut *last_row; 38 | let duplicated = last_row.duplicate(column_names); 39 | std::mem::replace(last_row, duplicated) 40 | }; 41 | if let PrivateSqliteRow::Direct(mut stmt) = last_row { 42 | // This is actually safe here as we've already 43 | // performed one step. For the first step we would have 44 | // used `PrivateStatementIterator::NotStarted` where we don't 45 | // have access to `PrivateSqliteRow` at all 46 | let res = stmt.step(false); 47 | *outer_last_row = Rc::new(RefCell::new(PrivateSqliteRow::Direct(stmt))); 48 | match res { 49 | Err(e) => Some(Err(e)), 50 | Ok(false) => None, 51 | Ok(true) => Some(Ok(SqliteRow { 52 | inner: Rc::clone(outer_last_row), 53 | field_count, 54 | })), 55 | } 56 | } else { 57 | // any other state than `PrivateSqliteRow::Direct` is invalid here 58 | // and should not happen. If this ever happens this is a logic error 59 | // in the code above 60 | unreachable!( 61 | "You've reached an impossible internal state. \ 62 | If you ever see this error message please open \ 63 | an issue at https://github.com/diesel-rs/diesel \ 64 | providing example code how to trigger this error." 65 | ) 66 | } 67 | } 68 | } 69 | 70 | enum PrivateStatementIterator<'stmt, 'query> { 71 | NotStarted(Option>), 72 | Started(Rc>>), 73 | } 74 | 75 | impl<'stmt, 'query> StatementIterator<'stmt, 'query> { 76 | pub fn new(stmt: StatementUse<'stmt, 'query>) -> StatementIterator<'stmt, 'query> { 77 | Self { 78 | inner: PrivateStatementIterator::NotStarted(Some(stmt)), 79 | column_names: None, 80 | field_count: 0, 81 | } 82 | } 83 | } 84 | 85 | impl<'stmt, 'query> Iterator for StatementIterator<'stmt, 'query> { 86 | type Item = QueryResult>; 87 | 88 | fn next(&mut self) -> Option { 89 | use PrivateStatementIterator::{NotStarted, Started}; 90 | match &mut self.inner { 91 | NotStarted(ref mut stmt @ Some(_)) => { 92 | let mut stmt = stmt 93 | .take() 94 | .expect("It must be there because we checked that above"); 95 | let step = stmt.step(true); 96 | match step { 97 | Err(e) => Some(Err(e)), 98 | Ok(false) => None, 99 | Ok(true) => { 100 | let field_count = stmt.column_count() as usize; 101 | self.field_count = field_count; 102 | let inner = Rc::new(RefCell::new(PrivateSqliteRow::Direct(stmt))); 103 | self.inner = Started(inner.clone()); 104 | Some(Ok(SqliteRow { inner, field_count })) 105 | } 106 | } 107 | } 108 | Started(ref mut last_row) => { 109 | // There was already at least one iteration step 110 | // We check here if the caller already released the row value or not 111 | // by checking if our Rc owns the data or not 112 | if let Some(last_row_ref) = Rc::get_mut(last_row) { 113 | // We own the statement, there is no other reference here. 114 | // This means we don't need to copy out values from the SQLite-provided 115 | // data structures for now 116 | // We don't need to use the runtime borrowing system of the RefCell here 117 | // as we have a mutable reference, so all of this below is checked at compile time 118 | if let PrivateSqliteRow::Direct(ref mut stmt) = last_row_ref.get_mut() { 119 | let step = stmt.step(false); 120 | 121 | match step { 122 | Err(e) => Some(Err(e)), 123 | Ok(false) => None, 124 | Ok(true) => { 125 | let field_count = self.field_count; 126 | Some(Ok(SqliteRow { 127 | inner: Rc::clone(last_row), 128 | field_count, 129 | })) 130 | } 131 | } 132 | } else { 133 | // any other state than `PrivateSqliteRow::Direct` is invalid here 134 | // and should not happen. If this ever happens this is a logic error 135 | // in the code above 136 | unreachable!( 137 | "You've reached an impossible internal state. \ 138 | If you ever see this error message please open \ 139 | an issue at https://github.com/diesel-rs/diesel \ 140 | providing example code how to trigger this error." 141 | ) 142 | } 143 | } else { 144 | Self::handle_duplicated_row_case( 145 | last_row, 146 | &mut self.column_names, 147 | self.field_count, 148 | ) 149 | } 150 | } 151 | NotStarted(_s) => { 152 | // we likely got an error while executing the other 153 | // `NotStarted` branch above. In this case we just want to stop 154 | // iterating here 155 | None 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | mod constants; 2 | mod wasm; 3 | 4 | use js_sys::{Object, Uint8Array, WebAssembly::Memory}; 5 | use serde::{Deserialize, Serialize}; 6 | use tokio::sync::OnceCell; 7 | use wasm_bindgen::{prelude::*, JsValue}; 8 | 9 | pub use constants::*; 10 | pub use wasm::*; 11 | // WASM is ran in the browser thread, either main or worker`. Tokio is only a single-threaded runtime. 12 | // We need SQLite available globally, so this should be ok until we get threads with WASI or 13 | // something. 14 | unsafe impl Send for SQLite {} 15 | unsafe impl Sync for SQLite {} 16 | 17 | /// The SQLite Library 18 | /// this global constant references the loaded SQLite WASM. 19 | pub(super) static SQLITE: OnceCell = OnceCell::const_new(); 20 | 21 | // it should be possible to: 22 | // - shared WASM memory between us and SQLite, thereby reducing allocation overhead 23 | // - Instantiate the WebAssembly.Module + WebAssembly.Instance from Rust (this could enable sharing 24 | // of memory) 25 | // - SQLite OpfsVfs just needs to be instantiated from WASM 26 | // - OpfsVfs instantiation would be a one-time cost 27 | // - this would make things overall more efficient since we wouldn't 28 | // have to go through JS/browser at all. 29 | 30 | /// the raw WASM bytes 31 | pub(super) const WASM: &[u8] = include_bytes!("js/sqlite3.wasm"); 32 | 33 | /// Options for instantiating memory constraints 34 | #[derive(Serialize, Deserialize)] 35 | struct MemoryOpts { 36 | initial: u32, 37 | maximum: u32, 38 | } 39 | /// Opts for the WASM Module 40 | #[derive(Serialize, Deserialize)] 41 | struct Opts { 42 | /// The Sqlite3 WASM blob, compiled from C 43 | #[serde(rename = "wasmBinary")] 44 | wasm_binary: &'static [u8], 45 | /// the shared WebAssembly Memory buffer 46 | /// this allows us to manipulate the WASM memory from rust 47 | #[serde(with = "serde_wasm_bindgen::preserve", rename = "wasmMemory")] 48 | wasm_memory: Memory, 49 | /// The URI for the OPFS async proxy. 50 | #[serde(rename = "proxyUri")] 51 | proxy_uri: String, 52 | } 53 | 54 | /// Copy the contents of this wasms typed array into SQLite's memory. 55 | /// 56 | /// This function will efficiently copy the memory from a typed 57 | /// array into this wasm module's own linear memory, initializing 58 | /// the memory destination provided. 59 | /// 60 | /// # Safety 61 | /// 62 | /// This function requires `dst` to point to a buffer 63 | /// large enough to fit this array's contents. 64 | pub fn raw_copy_to_sqlite>(bytes: B, dst: *mut u8) { 65 | let wasm = get_sqlite_unchecked().inner().wasm(); 66 | let bytes: Uint8Array = bytes.into(); 67 | let wasm_sqlite_mem = wasm.heap8u(); 68 | let offset = dst as usize / std::mem::size_of::(); 69 | wasm_sqlite_mem.set(&bytes, offset as u32); 70 | } 71 | 72 | /// Copy the contents of this SQLite bytes this Wasms memory. 73 | /// 74 | /// This function will efficiently copy the memory from a typed 75 | /// array into this wasm module's own linear memory, initializing 76 | /// the memory destination provided. 77 | /// 78 | /// # Safety 79 | /// 80 | /// This function requires `buf` to point to a buffer 81 | /// large enough to fit this array's contents. 82 | pub unsafe fn raw_copy_from_sqlite(src: *mut u8, len: u32, buf: &mut [u8]) { 83 | let wasm = crate::get_sqlite_unchecked().inner().wasm(); 84 | let mem = wasm.heap8u(); 85 | // this is safe because we view the slice and immediately copy it into 86 | // our memory. 87 | let view = Uint8Array::new_with_byte_offset_and_length(&mem.buffer(), src as u32, len); 88 | view.raw_copy_to_ptr(buf.as_mut_ptr()) 89 | } 90 | 91 | pub async fn init_sqlite() { 92 | SQLITE 93 | .get_or_init(|| async { 94 | let mem = serde_wasm_bindgen::to_value(&MemoryOpts { 95 | initial: 16_777_216 / 65_536, 96 | maximum: 2_147_483_648 / 65_536, 97 | }) 98 | .expect("Serialization must be infallible for const struct"); 99 | let mem = Memory::new(&js_sys::Object::from(mem)) 100 | .expect("Wasm Memory could not be instantiated"); 101 | let proxy_uri = wasm_bindgen::link_to!(module = "/src/js/sqlite3-opfs-async-proxy.js"); 102 | tracing::debug!("proxy_uri={:?}", proxy_uri); 103 | let opts = serde_wasm_bindgen::to_value(&Opts { 104 | wasm_binary: WASM, 105 | wasm_memory: mem, 106 | proxy_uri, 107 | }) 108 | .expect("serialization must be infallible for const struct"); 109 | let opts = Object::from(opts); 110 | let object = SQLite::init_module(&opts).await; 111 | let sqlite3 = SQLite::new(object); 112 | let version: crate::ffi::Version = serde_wasm_bindgen::from_value(sqlite3.version()) 113 | .expect("Version unexpected format"); 114 | tracing::info!( 115 | "SQLite initialized. version={}, download_version={}", 116 | version.lib_version, 117 | version.download_version 118 | ); 119 | 120 | sqlite3 121 | }) 122 | .await; 123 | } 124 | 125 | pub(super) fn get_sqlite_unchecked() -> &'static SQLite { 126 | SQLITE.get().expect("SQLite is not initialized") 127 | } 128 | 129 | #[wasm_bindgen] 130 | #[derive(Serialize, Deserialize, Debug)] 131 | struct Version { 132 | #[serde(rename = "libVersion")] 133 | lib_version: String, 134 | #[serde(rename = "libVersionNumber")] 135 | lib_version_number: u32, 136 | #[serde(rename = "sourceId")] 137 | source_id: String, 138 | #[serde(rename = "downloadVersion")] 139 | download_version: u32, 140 | } 141 | 142 | /// Direct Sqlite3 bindings 143 | #[wasm_bindgen(module = "/src/js/sqlite3-diesel.js")] 144 | extern "C" { 145 | #[derive(Debug)] 146 | pub type SQLite; 147 | 148 | #[derive(Debug)] 149 | #[wasm_bindgen(extends = SQLite)] 150 | pub type Inner; 151 | 152 | #[wasm_bindgen(method, getter, js_name = "sqlite3")] 153 | pub fn inner(this: &SQLite) -> Inner; 154 | 155 | #[wasm_bindgen(method, getter)] 156 | pub fn wasm(this: &Inner) -> Wasm; 157 | 158 | #[wasm_bindgen(method, getter)] 159 | pub fn capi(this: &Inner) -> CApi; 160 | 161 | #[wasm_bindgen(static_method_of = SQLite)] 162 | pub async fn init_module(module: &Object) -> JsValue; 163 | 164 | #[wasm_bindgen(constructor)] 165 | pub fn new(module: JsValue) -> SQLite; 166 | 167 | #[wasm_bindgen(method)] 168 | pub fn version(this: &SQLite) -> JsValue; 169 | 170 | #[wasm_bindgen(method)] 171 | pub fn filename(this: &SQLite, db: &JsValue, name: String) -> String; 172 | 173 | #[wasm_bindgen(method)] 174 | pub fn errstr(this: &SQLite, code: i32) -> String; 175 | 176 | #[wasm_bindgen(method)] 177 | pub fn errmsg(this: &SQLite, conn: &JsValue) -> String; 178 | 179 | #[wasm_bindgen(method)] 180 | pub fn extended_errcode(this: &SQLite, conn: &JsValue) -> i32; 181 | 182 | #[wasm_bindgen(method)] 183 | pub fn result_text(this: &SQLite, context: i32, value: String); 184 | 185 | #[wasm_bindgen(method)] 186 | pub fn result_int(this: &SQLite, context: i32, value: i32); 187 | 188 | #[wasm_bindgen(method)] 189 | pub fn result_int64(this: &SQLite, context: i32, value: i64); 190 | 191 | #[wasm_bindgen(method)] 192 | pub fn result_double(this: &SQLite, context: i32, value: f64); 193 | 194 | #[wasm_bindgen(method)] 195 | pub fn result_blob(this: &SQLite, context: i32, value: Vec); 196 | 197 | #[wasm_bindgen(method)] 198 | pub fn result_null(this: &SQLite, context: i32); 199 | 200 | #[wasm_bindgen(method)] 201 | pub fn bind_parameter_count(this: &SQLite, stmt: &JsValue) -> i32; 202 | 203 | #[wasm_bindgen(method)] 204 | pub fn bind_parameter_name(this: &SQLite, stmt: &JsValue, idx: i32) -> String; 205 | 206 | #[wasm_bindgen(method)] 207 | pub fn bind_null(this: &SQLite, stmt: &JsValue, idx: i32) -> i32; 208 | 209 | #[wasm_bindgen(method)] 210 | pub fn bind_text( 211 | this: &SQLite, 212 | stmt: &JsValue, 213 | idx: i32, 214 | ptr: *mut u8, 215 | len: i32, 216 | flags: i32, 217 | ) -> i32; 218 | 219 | #[wasm_bindgen(method)] 220 | pub fn bind_blob( 221 | this: &SQLite, 222 | stmt: &JsValue, 223 | idx: i32, 224 | ptr: *mut u8, 225 | len: i32, 226 | flags: i32, 227 | ) -> i32; 228 | 229 | #[wasm_bindgen(method)] 230 | pub fn bind_double(this: &SQLite, stmt: &JsValue, idx: i32, value: f64) -> i32; 231 | 232 | #[wasm_bindgen(method)] 233 | pub fn bind_int(this: &SQLite, stmt: &JsValue, idx: i32, value: i32) -> i32; 234 | 235 | #[wasm_bindgen(method)] 236 | pub fn bind_int64(this: &SQLite, stmt: &JsValue, idx: i32, value: i64) -> i32; 237 | 238 | #[wasm_bindgen(method)] 239 | pub fn reset(this: &SQLite, stmt: &JsValue) -> i32; 240 | 241 | #[wasm_bindgen(method)] 242 | pub fn value_dup(this: &SQLite, pValue: *mut u8) -> *mut u8; 243 | 244 | #[wasm_bindgen(method)] 245 | pub fn value_blob(this: &SQLite, pValue: *mut u8) -> *mut u8; 246 | 247 | #[wasm_bindgen(method)] 248 | pub fn value_bytes(this: &SQLite, pValue: *mut u8) -> u32; 249 | 250 | #[wasm_bindgen(method)] 251 | pub fn value_double(this: &SQLite, pValue: *mut u8) -> f64; 252 | 253 | #[wasm_bindgen(method)] 254 | pub fn value_free(this: &SQLite, pValue: *mut u8); 255 | 256 | #[wasm_bindgen(method)] 257 | pub fn sqlite3_free(this: &SQLite, pValue: *mut u8); 258 | 259 | #[wasm_bindgen(method)] 260 | pub fn value_int(this: &SQLite, pValue: *mut u8) -> i32; 261 | 262 | #[wasm_bindgen(method)] 263 | pub fn value_int64(this: &SQLite, pValue: *mut u8) -> i64; 264 | 265 | #[wasm_bindgen(method)] 266 | pub fn value_text(this: &SQLite, pValue: *mut u8) -> String; 267 | 268 | #[wasm_bindgen(method)] 269 | pub fn value_type(this: &SQLite, pValue: *mut u8) -> i32; 270 | 271 | #[wasm_bindgen(method, catch)] 272 | pub fn open(this: &SQLite, database_url: &str, iflags: Option) 273 | -> Result; 274 | 275 | #[wasm_bindgen(method, catch)] 276 | pub fn exec(this: &SQLite, database: &JsValue, query: &str) -> Result<(), JsValue>; 277 | 278 | #[wasm_bindgen(method, catch)] 279 | pub fn finalize(this: &SQLite, stmt: &JsValue) -> Result<(), JsValue>; 280 | 281 | #[wasm_bindgen(method)] 282 | pub fn changes(this: &SQLite, database: &JsValue) -> usize; 283 | 284 | #[wasm_bindgen(method, catch)] 285 | pub fn get_stmt_from_iterator(this: &SQLite, iterator: &JsValue) -> Result; 286 | 287 | #[wasm_bindgen(method)] 288 | pub fn step(this: &SQLite, stmt: &JsValue) -> i32; 289 | 290 | #[wasm_bindgen(method)] 291 | pub fn clear_bindings(this: &SQLite, stmt: &JsValue) -> i32; 292 | 293 | #[wasm_bindgen(method)] 294 | pub fn close(this: &SQLite, database: &JsValue) -> i32; 295 | 296 | #[wasm_bindgen(method)] 297 | pub fn db_handle(this: &SQLite, stmt: &JsValue) -> JsValue; 298 | 299 | #[wasm_bindgen(method)] 300 | pub fn column_value(this: &SQLite, stmt: &JsValue, idx: i32) -> *mut u8; 301 | 302 | #[wasm_bindgen(method)] 303 | pub fn prepare_v3( 304 | this: &SQLite, 305 | database: &JsValue, 306 | sql: *mut u8, 307 | n_byte: i32, 308 | prep_flags: u32, 309 | stmt: &JsValue, 310 | pzTail: &JsValue, 311 | ) -> i32; 312 | 313 | #[wasm_bindgen(method)] 314 | pub fn column_name(this: &SQLite, stmt: &JsValue, idx: i32) -> String; 315 | 316 | #[wasm_bindgen(method)] 317 | pub fn column_count(this: &SQLite, stmt: &JsValue) -> i32; 318 | 319 | #[wasm_bindgen(method, catch)] 320 | pub fn create_function( 321 | this: &SQLite, 322 | database: &JsValue, 323 | functionName: &str, 324 | n_arg: i32, 325 | textRep: i32, 326 | pApp: i32, //ignored 327 | x_func: Option<&Closure) -> JsValue>>, 328 | x_step: Option<&Closure) -> JsValue>>, 329 | x_final: Option<&Closure>, 330 | ) -> Result<(), JsValue>; 331 | 332 | #[wasm_bindgen(method, catch)] 333 | pub fn register_diesel_sql_functions(this: &SQLite, database: &JsValue) -> Result<(), JsValue>; 334 | 335 | #[wasm_bindgen(method)] 336 | pub fn sqlite3_serialize( 337 | this: &SQLite, 338 | database: &JsValue, 339 | z_schema: &str, 340 | p_size: &JsValue, 341 | m_flags: u32, 342 | ) -> *mut u8; 343 | 344 | #[wasm_bindgen(method)] 345 | pub fn sqlite3_deserialize( 346 | this: &SQLite, 347 | database: &JsValue, 348 | z_schema: &str, 349 | p_data: *mut u8, 350 | sz_database: i64, 351 | sz_buffer: i64, 352 | m_flags: u32, 353 | ) -> i32; 354 | } 355 | -------------------------------------------------------------------------------- /src/ffi/constants.rs: -------------------------------------------------------------------------------- 1 | //! WASM Constant bindings 2 | use std::sync::LazyLock; 3 | use wasm_bindgen::prelude::*; 4 | 5 | /// Normally, statics or methods cannot be pattern-matched. 6 | /// Pattern matching also does not automatically dereference. 7 | /// constants are imported into wasm-bindgen as statics and/or const 8 | /// methods. We use a combination of `LazyCell` 9 | /// and exported wasm getters to achieve some kind of 10 | /// pattern-matching syntax 11 | /// ``` 12 | /// match variable { 13 | /// v if *ffi::SQLITE_DONE == v { 14 | /// /* SQLITE_DONE */ 15 | /// }, 16 | /// v if *ffi::SQLITE_ROW == v { 17 | /// /* SQLITE_ROW */ 18 | /// } 19 | /// } 20 | /// ``` 21 | /// 22 | /// This is also a micro-optimization, 23 | /// These constants will be initialized exactly once, rather 24 | /// than on every access thus reducing the context-switching between wasm-js barrier. 25 | macro_rules! generate_sqlite_constant { 26 | ($fn_name:ident, $ty: ident) => { 27 | pub static $fn_name: LazyLock<$ty> = LazyLock::new(|| { 28 | let capi: CApi = crate::get_sqlite_unchecked().inner().capi(); 29 | CApi::$fn_name(&capi) 30 | }); 31 | }; 32 | } 33 | 34 | generate_sqlite_constant!(SQLITE_OK, i32); 35 | 36 | generate_sqlite_constant!(SQLITE_DONE, i32); 37 | generate_sqlite_constant!(SQLITE_ROW, i32); 38 | 39 | generate_sqlite_constant!(SQLITE_INTEGER, i32); 40 | generate_sqlite_constant!(SQLITE_FLOAT, i32); 41 | generate_sqlite_constant!(SQLITE_TEXT, i32); 42 | generate_sqlite_constant!(SQLITE_BLOB, i32); 43 | generate_sqlite_constant!(SQLITE_NULL, i32); 44 | 45 | generate_sqlite_constant!(SQLITE_PREPARE_PERSISTENT, u32); 46 | 47 | generate_sqlite_constant!(SQLITE_CONSTRAINT_PRIMARYKEY, i32); 48 | generate_sqlite_constant!(SQLITE_CONSTRAINT_UNIQUE, i32); 49 | generate_sqlite_constant!(SQLITE_CONSTRAINT_FOREIGNKEY, i32); 50 | generate_sqlite_constant!(SQLITE_CONSTRAINT_NOTNULL, i32); 51 | generate_sqlite_constant!(SQLITE_CONSTRAINT_CHECK, i32); 52 | 53 | generate_sqlite_constant!(SQLITE_STATIC, i32); 54 | 55 | // C-Style API Constants 56 | #[wasm_bindgen] 57 | extern "C" { 58 | /// C-Api Style bindings 59 | #[wasm_bindgen(extends = super::Inner)] 60 | pub type CApi; 61 | 62 | #[wasm_bindgen(method, getter)] 63 | pub const fn SQLITE_OK(this: &CApi) -> i32; 64 | 65 | /// SQLite statement returns 66 | #[wasm_bindgen(method, getter)] 67 | pub const fn SQLITE_DONE(this: &CApi) -> i32; 68 | #[wasm_bindgen(method, getter)] 69 | pub const fn SQLITE_ROW(this: &CApi) -> i32; 70 | 71 | // Fundamental datatypes. 72 | // https://www.sqlite.org/c3ref/c_blob.html 73 | #[wasm_bindgen(method, getter)] 74 | pub const fn SQLITE_INTEGER(this: &CApi) -> i32; 75 | #[wasm_bindgen(method, getter)] 76 | pub const fn SQLITE_FLOAT(this: &CApi) -> i32; 77 | #[wasm_bindgen(method, getter)] 78 | pub const fn SQLITE_TEXT(this: &CApi) -> i32; 79 | #[wasm_bindgen(method, getter)] 80 | pub const fn SQLITE_BLOB(this: &CApi) -> i32; 81 | #[wasm_bindgen(method, getter)] 82 | pub const fn SQLITE_NULL(this: &CApi) -> i32; 83 | 84 | /// SQLite Open Flags 85 | #[wasm_bindgen(method, getter)] 86 | pub const fn SQLITE_OPEN_READONLY(this: &CApi) -> i32; 87 | #[wasm_bindgen(method, getter)] 88 | pub const fn SQLITE_OPEN_READWRITE(this: &CApi) -> i32; 89 | #[wasm_bindgen(method, getter)] 90 | pub const fn SQLITE_OPEN_CREATE(this: &CApi) -> i32; 91 | #[wasm_bindgen(method, getter)] 92 | pub const fn SQLITE_OPEN_DELETEONCLOSE(this: &CApi) -> i32; 93 | #[wasm_bindgen(method, getter)] 94 | pub const fn SQLITE_OPEN_EXCLUSIVE(this: &CApi) -> i32; 95 | #[wasm_bindgen(method, getter)] 96 | pub const fn SQLITE_OPEN_AUTOPROXY(this: &CApi) -> i32; 97 | #[wasm_bindgen(method, getter)] 98 | pub const fn SQLITE_OPEN_URI(this: &CApi) -> i32; 99 | #[wasm_bindgen(method, getter)] 100 | pub const fn SQLITE_OPEN_MEMORY(this: &CApi) -> i32; 101 | #[wasm_bindgen(method, getter)] 102 | pub const fn SQLITE_OPEN_MAIN_DB(this: &CApi) -> i32; 103 | #[wasm_bindgen(method, getter)] 104 | pub const fn SQLITE_OPEN_TEMP_DB(this: &CApi) -> i32; 105 | #[wasm_bindgen(method, getter)] 106 | pub const fn SQLITE_OPEN_TRANSIENT_DB(this: &CApi) -> i32; 107 | #[wasm_bindgen(method, getter)] 108 | pub const fn SQLITE_OPEN_MAIN_JOURNAL(this: &CApi) -> i32; 109 | #[wasm_bindgen(method, getter)] 110 | pub const fn SQLITE_OPEN_TEMP_JOURNAL(this: &CApi) -> i32; 111 | #[wasm_bindgen(method, getter)] 112 | pub const fn SQLITE_OPEN_SUBJOURNAL(this: &CApi) -> i32; 113 | #[wasm_bindgen(method, getter)] 114 | pub const fn SQLITE_OPEN_SUPER_JOURNAL(this: &CApi) -> i32; 115 | #[wasm_bindgen(method, getter)] 116 | pub const fn SQLITE_OPEN_NOMUTEX(this: &CApi) -> i32; 117 | #[wasm_bindgen(method, getter)] 118 | pub const fn SQLITE_OPEN_FULLMUTEX(this: &CApi) -> i32; 119 | #[wasm_bindgen(method, getter)] 120 | pub const fn SQLITE_OPEN_SHAREDCACHE(this: &CApi) -> i32; 121 | #[wasm_bindgen(method, getter)] 122 | pub const fn SQLITE_OPEN_PRIVATECACHE(this: &CApi) -> i32; 123 | #[wasm_bindgen(method, getter)] 124 | pub const fn SQLITE_OPEN_WAL(this: &CApi) -> i32; 125 | #[wasm_bindgen(method, getter)] 126 | pub const fn SQLITE_OPEN_NOFOLLOW(this: &CApi) -> i32; 127 | #[wasm_bindgen(method, getter)] 128 | pub const fn SQLITE_OPEN_EXRESCODE(this: &CApi) -> i32; 129 | 130 | // SQLite Text Encodings https://www.sqlite.org/capi3ref.html#SQLITE_ANY 131 | #[wasm_bindgen(method, getter)] 132 | pub const fn SQLITE_UTF8(this: &CApi) -> i32; 133 | #[wasm_bindgen(method, getter)] 134 | pub const fn SQLITE_UTF16LE(this: &CApi) -> i32; 135 | #[wasm_bindgen(method, getter)] 136 | pub const fn SQLITE_UTF16BE(this: &CApi) -> i32; 137 | #[wasm_bindgen(method, getter)] 138 | pub const fn SQLITE_UTF16(this: &CApi) -> i32; 139 | #[wasm_bindgen(method, getter)] 140 | pub const fn SQLITE_ANY(this: &CApi) -> i32; 141 | #[wasm_bindgen(method, getter)] 142 | pub const fn SQLITE_UTF16_ALIGNED(this: &CApi) -> i32; 143 | 144 | /// SQLite Function Flags https://www.sqlite.org/capi3ref.html#sqlitedeterministic 145 | #[wasm_bindgen(method, getter)] 146 | pub const fn SQLITE_DETERMINISTIC(this: &CApi) -> i32; 147 | #[wasm_bindgen(method, getter)] 148 | pub const fn SQLITE_DIRECTONLY(this: &CApi) -> i32; 149 | #[wasm_bindgen(method, getter)] 150 | pub const fn SQLITE_SUBTYPE(this: &CApi) -> i32; 151 | #[wasm_bindgen(method, getter)] 152 | pub const fn SQLITE_INNOCUOUS(this: &CApi) -> i32; 153 | #[wasm_bindgen(method, getter)] 154 | pub const fn SQLITE_RESULT_SUBTYPE(this: &CApi) -> i32; 155 | 156 | // SQLite Prepare Flags https://www.sqlite.org/c3ref/c_prepare_normalize.html#sqlitepreparepersistent 157 | #[wasm_bindgen(method, getter)] 158 | pub const fn SQLITE_PREPARE_PERSISTENT(this: &CApi) -> u32; 159 | #[wasm_bindgen(method, getter)] 160 | pub fn SQLITE_PREPARE_NORMALIZE(this: &CApi) -> u32; 161 | #[wasm_bindgen(method, getter)] 162 | pub fn SQLITE_PREPARE_NO_VTAB(this: &CApi) -> u32; 163 | 164 | /// Constraint 165 | 166 | #[wasm_bindgen(method, getter)] 167 | pub fn SQLITE_CONSTRAINT_UNIQUE(this: &CApi) -> i32; 168 | #[wasm_bindgen(method, getter)] 169 | pub fn SQLITE_CONSTRAINT_PRIMARYKEY(this: &CApi) -> i32; 170 | #[wasm_bindgen(method, getter)] 171 | pub fn SQLITE_CONSTRAINT_FOREIGNKEY(this: &CApi) -> i32; 172 | #[wasm_bindgen(method, getter)] 173 | pub fn SQLITE_CONSTRAINT_NOTNULL(this: &CApi) -> i32; 174 | #[wasm_bindgen(method, getter)] 175 | pub fn SQLITE_CONSTRAINT_CHECK(this: &CApi) -> i32; 176 | 177 | /// Binds 178 | #[wasm_bindgen(method, getter)] 179 | pub fn SQLITE_STATIC(this: &CApi) -> i32; 180 | } 181 | -------------------------------------------------------------------------------- /src/ffi/wasm.rs: -------------------------------------------------------------------------------- 1 | //! WASM bindings for memory management 2 | use std::ptr::NonNull; 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | extern "C" { 7 | #[derive(Debug)] 8 | #[wasm_bindgen(extends = super::Inner)] 9 | pub type Wasm; 10 | 11 | #[wasm_bindgen(method, js_name = "peekPtr")] 12 | pub fn peek_ptr(this: &Wasm, stmt: &JsValue) -> JsValue; 13 | /// The "pstack" (pseudo-stack) API is a special-purpose allocator 14 | /// intended solely for use with allocating small amounts of memory such 15 | /// as that needed for output pointers. 16 | /// It is more efficient than the scoped allocation API, 17 | /// and covers many of the use cases for that API, but it 18 | /// has a tiny static memory limit (with an unspecified total size no less than 4kb). 19 | #[wasm_bindgen(method, getter)] 20 | pub fn pstack(this: &Wasm) -> PStack; 21 | 22 | #[wasm_bindgen(method)] 23 | pub fn alloc(this: &Wasm, bytes: u32) -> *mut u8; 24 | 25 | #[wasm_bindgen(method, getter, js_name = "alloc")] 26 | pub fn alloc_inner(this: &Wasm) -> Alloc; 27 | 28 | /// Uses alloc() to allocate enough memory for the byte-length of the given JS string, 29 | /// plus 1 (for a NUL terminator), copies the given JS string to that memory using jstrcpy(), 30 | /// NUL-terminates it, and returns the pointer to that C-string. 31 | /// Ownership of the pointer is transfered to the caller, who must eventually pass the pointer to dealloc() to free it. 32 | //TODO: Avoid using this since it allocates in JS and other webassembly. Instead use technique 33 | // used in Statement::prepare 34 | #[wasm_bindgen(method, js_name = "allocCString")] 35 | pub fn alloc_cstring(this: &Wasm, string: String) -> *mut u8; 36 | 37 | /// Allocates one or more pointers as a single chunk of memory and zeroes them out. 38 | /// The first argument is the number of pointers to allocate. 39 | /// The second specifies whether they should use a "safe" pointer size (8 bytes) 40 | /// or whether they may use the default pointer size (typically 4 but also possibly 8). 41 | /// How the result is returned depends on its first argument: if passed 1, it returns the allocated memory address. 42 | /// If passed more than one then an array of pointer addresses is returned 43 | #[wasm_bindgen(method, js_name = "allocPtr")] 44 | pub fn alloc_ptr(this: &Wasm, how_many: u32, safe_ptr_size: bool) -> *mut u8; 45 | 46 | #[wasm_bindgen(method)] 47 | pub fn dealloc(this: &Wasm, ptr: NonNull); 48 | 49 | /// View into the wasm memory reprsented as unsigned 8-bit integers 50 | #[wasm_bindgen(method)] 51 | pub fn heap8u(this: &Wasm) -> js_sys::Uint8Array; 52 | } 53 | 54 | #[wasm_bindgen] 55 | extern "C" { 56 | #[wasm_bindgen(extends = Wasm)] 57 | pub type PStack; 58 | 59 | /// allocate some memory on the PStack 60 | #[wasm_bindgen(method)] 61 | pub fn alloc(this: &PStack, bytes: u32) -> JsValue; 62 | 63 | /// Resolves the current pstack position pointer. 64 | /// should only be used in argument for `restore` 65 | #[wasm_bindgen(method, getter)] 66 | pub fn pointer(this: &PStack) -> JsValue; 67 | 68 | /// resolves to total number of bytes available in pstack, including any 69 | /// space currently allocated. compile-time constant 70 | #[wasm_bindgen(method, getter)] 71 | pub fn quota(this: &PStack) -> u32; 72 | 73 | // Property resolves to the amount of space remaining in the pstack 74 | #[wasm_bindgen(method, getter)] 75 | pub fn remaining(this: &PStack) -> u32; 76 | 77 | /// sets current pstack 78 | #[wasm_bindgen(method)] 79 | pub fn restore(this: &PStack, ptr: &JsValue); 80 | 81 | } 82 | 83 | #[wasm_bindgen] 84 | extern "C" { 85 | pub type Alloc; 86 | 87 | /// Non-throwing version of `Wasm::Alloc` 88 | /// returns NULL pointer if cannot allocate 89 | #[wasm_bindgen(method, js_name = "impl")] 90 | pub fn alloc_impl(this: &Alloc, bytes: u32) -> *mut u8; 91 | } 92 | -------------------------------------------------------------------------------- /src/js/sqlite3-opfs-async-proxy.js: -------------------------------------------------------------------------------- 1 | const e=(e,...t)=>postMessage({type:e,payload:t}),t=function(){const t=function(...e){throw new Error(e.join(" "))};globalThis.window===globalThis?t("This code cannot run from the main thread.","Load it as a Worker from a separate Worker."):navigator?.storage?.getDirectory||t("This API requires navigator.storage.getDirectory.");const n=Object.create(null);n.verbose=1;const s={0:console.error.bind(console),1:console.warn.bind(console),2:console.log.bind(console)},a=(e,...t)=>{n.verbose>e&&s[e]("OPFS asyncer:",...t)},i=(...e)=>a(2,...e),o=(...e)=>a(1,...e),r=(...e)=>a(0,...e),c=Object.create(null),l=new Set,d=function(e,t){const n=new URL(e,"file://irrelevant").pathname;return t?n.split("/").filter((e=>!!e)):n},f=async function(e,t=!1){const s=d(e,!0),a=s.pop();let i=n.rootDir;for(const e of s)e&&(i=await i.getDirectoryHandle(e,{create:!!t}));return[i,a]},y=async e=>{if(e.syncHandle){i("Closing sync handle for",e.filenameAbs);const t=e.syncHandle;return delete e.syncHandle,delete e.xLock,l.delete(e.fid),t.close()}},u=async e=>{try{await y(e)}catch(t){o("closeSyncHandleNoThrow() ignoring:",t,e)}},E=async()=>{if(l.size)for(const e of l){const t=c[e];await u(t),i("Auto-unlocked",e,t.filenameAbs)}},b=async e=>{if(e.releaseImplicitLocks&&l.has(e.fid))return u(e)};class O extends Error{constructor(e,...t){super([...t,": "+e.name+":",e.message].join(" "),{cause:e}),this.name="GetSyncHandleError"}}O.convertRc=(e,t)=>{if(e instanceof O){if("NoModificationAllowedError"===e.cause.name||"DOMException"===e.cause.name&&0===e.cause.message.indexOf("Access Handles cannot"))return n.sq3Codes.SQLITE_BUSY;if("NotFoundError"===e.cause.name)return n.sq3Codes.SQLITE_CANTOPEN}else if("NotFoundError"===e?.name)return n.sq3Codes.SQLITE_CANTOPEN;return t};const g=async(e,t)=>{if(!e.syncHandle){const s=performance.now();i("Acquiring sync handle for",e.filenameAbs);const a=6,r=2*n.asyncIdleWaitTime;let c=1,d=r;for(;;d=r*++c)try{e.syncHandle=await e.fileHandle.createSyncAccessHandle();break}catch(s){if(c===a)throw new O(s,"Error getting sync handle for",t+"().",a,"attempts failed.",e.filenameAbs);o("Error getting sync handle for",t+"(). Waiting",d,"ms and trying again.",e.filenameAbs,s),Atomics.wait(n.sabOPView,n.opIds.retry,0,d)}i("Got",t+"() sync handle for",e.filenameAbs,"in",performance.now()-s,"ms"),e.xLock||(l.add(e.fid),i("Acquired implicit lock for",t+"()",e.fid,e.filenameAbs))}return e.syncHandle},h=(e,t)=>{i(e+"() => notify(",t,")"),Atomics.store(n.sabOPView,n.opIds.rc,t),Atomics.notify(n.sabOPView,n.opIds.rc)},w=function(e,n){n.readOnly&&t(e+"(): File is read-only: "+n.filenameAbs)};let p=!1;const I={"opfs-async-shutdown":async()=>{p=!0,h("opfs-async-shutdown",0)},mkdir:async e=>{let t=0;try{await f(e+"/filepart",!0)}catch(e){n.s11n.storeException(2,e),t=n.sq3Codes.SQLITE_IOERR}h("mkdir",t)},xAccess:async e=>{let t=0;try{const[t,n]=await f(e);await t.getFileHandle(n)}catch(e){n.s11n.storeException(2,e),t=n.sq3Codes.SQLITE_IOERR}h("xAccess",t)},xClose:async function(e){l.delete(e);const t=c[e];let s=0;if(t){if(delete c[e],await y(t),t.deleteOnClose)try{await t.dirHandle.removeEntry(t.filenamePart)}catch(e){o("Ignoring dirHandle.removeEntry() failure of",t,e)}}else n.s11n.serialize(),s=n.sq3Codes.SQLITE_NOTFOUND;h("xClose",s)},xDelete:async function(...e){const t=await I.xDeleteNoWait(...e);h("xDelete",t)},xDeleteNoWait:async function(e,t=0,s=!1){let a=0;try{for(;e;){const[n,a]=await f(e,!1);if(!a)break;if(await n.removeEntry(a,{recursive:s}),4660!==t)break;s=!1,(e=d(e,!0)).pop(),e=e.join("/")}}catch(e){n.s11n.storeException(2,e),a=n.sq3Codes.SQLITE_IOERR_DELETE}return a},xFileSize:async function(e){const t=c[e];let s=0;try{const e=await(await g(t,"xFileSize")).getSize();n.s11n.serialize(Number(e))}catch(e){n.s11n.storeException(1,e),s=O.convertRc(e,n.sq3Codes.SQLITE_IOERR)}await b(t),h("xFileSize",s)},xLock:async function(e,t){const s=c[e];let a=0;const i=s.xLock;if(s.xLock=t,!s.syncHandle)try{await g(s,"xLock"),l.delete(e)}catch(e){n.s11n.storeException(1,e),a=O.convertRc(e,n.sq3Codes.SQLITE_IOERR_LOCK),s.xLock=i}h("xLock",a)},xOpen:async function(e,t,s,a){const i="xOpen",o=n.sq3Codes.SQLITE_OPEN_CREATE&s;try{let r,l;try{[r,l]=await f(t,!!o)}catch(e){return n.s11n.storeException(1,e),void h(i,n.sq3Codes.SQLITE_NOTFOUND)}if(n.opfsFlags.OPFS_UNLINK_BEFORE_OPEN&a)try{await r.removeEntry(l)}catch(e){}const d=await r.getFileHandle(l,{create:o}),y=Object.assign(Object.create(null),{fid:e,filenameAbs:t,filenamePart:l,dirHandle:r,fileHandle:d,sabView:n.sabFileBufView,readOnly:!o&&n.sq3Codes.SQLITE_OPEN_READONLY&s,deleteOnClose:!!(n.sq3Codes.SQLITE_OPEN_DELETEONCLOSE&s)});y.releaseImplicitLocks=a&n.opfsFlags.OPFS_UNLOCK_ASAP||n.opfsFlags.defaultUnlockAsap,c[e]=y,h(i,0)}catch(e){r(i,e),n.s11n.storeException(1,e),h(i,n.sq3Codes.SQLITE_IOERR)}},xRead:async function(e,t,s){let a,i=0;const o=c[e];try{a=(await g(o,"xRead")).read(o.sabView.subarray(0,t),{at:Number(s)}),a{Number.isFinite(n.opIds[e])||t("Maintenance required: missing state.opIds[",e,"]")})),(()=>{if(n.s11n)return n.s11n;const e=new TextDecoder,s=new TextEncoder("utf-8"),a=new Uint8Array(n.sabIO,n.sabS11nOffset,n.sabS11nSize),i=new DataView(n.sabIO,n.sabS11nOffset,n.sabS11nSize);n.s11n=Object.create(null);const o=Object.create(null);o.number={id:1,size:8,getter:"getFloat64",setter:"setFloat64"},o.bigint={id:2,size:8,getter:"getBigInt64",setter:"setBigInt64"},o.boolean={id:3,size:4,getter:"getInt32",setter:"setInt32"},o.string={id:4};const r=e=>{switch(e){case o.number.id:return o.number;case o.bigint.id:return o.bigint;case o.boolean.id:return o.boolean;case o.string.id:return o.string;default:t("Invalid type ID:",e)}};n.s11n.deserialize=function(t=!1){const s=a[0],o=s?[]:null;if(s){const t=[];let c,l,d,f=1;for(c=0;c{e<=n.asyncS11nExceptions&&n.s11n.serialize([t.name,": ",t.message].join(""))}:()=>{},n.s11n})(),i("init state",n),e("opfs-async-inited"),m();break}case"opfs-async-restart":p&&(o("Restarting after opfs-async-shutdown. Might or might not work."),p=!1,m())}},e("opfs-async-loaded")})).catch((e=>r("error initializing OPFS asyncer:",e)))};globalThis.SharedArrayBuffer?globalThis.Atomics?globalThis.FileSystemHandle&&globalThis.FileSystemDirectoryHandle&&globalThis.FileSystemFileHandle&&globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle&&navigator?.storage?.getDirectory?t():e("opfs-unavailable","Missing required OPFS APIs."):e("opfs-unavailable","Missing Atomics API.","The server must emit the COOP/COEP response headers to enable that."):e("opfs-unavailable","Missing SharedArrayBuffer API.","The server must emit the COOP/COEP response headers to enable that."); 2 | -------------------------------------------------------------------------------- /src/js/sqlite3.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmtp/sqlite-web-rs/HEAD/src/js/sqlite3.wasm -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Module for an SQLite backend accesible from the web. 2 | pub mod backend; 3 | pub mod connection; 4 | pub mod ffi; 5 | pub mod query_builder; 6 | pub mod sqlite_fixes; 7 | pub mod sqlite_types; 8 | 9 | #[global_allocator] 10 | static ALLOCATOR: talc::TalckWasm = unsafe { talc::TalckWasm::new_global() }; 11 | 12 | #[cfg(any(feature = "unsafe-debug-query", test))] 13 | pub use query_builder::insert_with_default_sqlite::unsafe_debug_query::DebugQueryWrapper; 14 | 15 | #[cfg(not(target_arch = "wasm32"))] 16 | compile_error!("This crate only suports the `wasm32-unknown-unknown` target"); 17 | 18 | use wasm_bindgen::JsValue; 19 | 20 | pub use backend::{SqliteType, WasmSqlite}; 21 | pub(crate) use ffi::get_sqlite_unchecked; 22 | pub use ffi::init_sqlite; 23 | pub use sqlite_fixes::dsl; 24 | 25 | #[derive(thiserror::Error, Debug)] 26 | pub enum WasmSqliteError { 27 | #[error("JS Bridge Error {0:?}")] 28 | Js(JsValue), 29 | #[error(transparent)] 30 | Diesel(#[from] diesel::result::Error), 31 | #[error(transparent)] 32 | Bindgen(#[from] serde_wasm_bindgen::Error), 33 | } 34 | 35 | impl From for diesel::result::Error { 36 | fn from(value: WasmSqliteError) -> diesel::result::Error { 37 | tracing::error!("NOT IMPLEMENTED, {:?}", value); 38 | diesel::result::Error::NotFound 39 | } 40 | } 41 | 42 | impl From for diesel::result::ConnectionError { 43 | fn from(value: WasmSqliteError) -> diesel::result::ConnectionError { 44 | tracing::error!("{:?}", value); 45 | diesel::result::ConnectionError::BadConnection("Not implemented".to_string()) 46 | } 47 | } 48 | 49 | impl From for WasmSqliteError { 50 | fn from(err: JsValue) -> WasmSqliteError { 51 | WasmSqliteError::Js(err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/query_builder/limit_offset.rs: -------------------------------------------------------------------------------- 1 | use crate::WasmSqlite; 2 | use diesel::query_builder::{AstPass, IntoBoxedClause, QueryFragment}; 3 | use diesel::query_builder::{BoxedLimitOffsetClause, LimitOffsetClause}; 4 | use diesel::query_builder::{LimitClause, NoLimitClause}; 5 | use diesel::query_builder::{NoOffsetClause, OffsetClause}; 6 | use diesel::result::QueryResult; 7 | 8 | impl QueryFragment for LimitOffsetClause { 9 | fn walk_ast<'b>(&'b self, _out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 10 | Ok(()) 11 | } 12 | } 13 | 14 | impl QueryFragment for LimitOffsetClause, NoOffsetClause> 15 | where 16 | LimitClause: QueryFragment, 17 | { 18 | fn walk_ast<'b>(&'b self, out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 19 | self.limit_clause.walk_ast(out)?; 20 | Ok(()) 21 | } 22 | } 23 | 24 | impl QueryFragment for LimitOffsetClause> 25 | where 26 | OffsetClause: QueryFragment, 27 | { 28 | fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 29 | // Sqlite requires a limit clause in front of any offset clause 30 | // using `LIMIT -1` is the same as not having any limit clause 31 | // https://sqlite.org/lang_select.html 32 | out.push_sql(" LIMIT -1 "); 33 | self.offset_clause.walk_ast(out)?; 34 | Ok(()) 35 | } 36 | } 37 | 38 | impl QueryFragment for LimitOffsetClause, OffsetClause> 39 | where 40 | LimitClause: QueryFragment, 41 | OffsetClause: QueryFragment, 42 | { 43 | fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 44 | self.limit_clause.walk_ast(out.reborrow())?; 45 | self.offset_clause.walk_ast(out.reborrow())?; 46 | Ok(()) 47 | } 48 | } 49 | 50 | impl<'a> QueryFragment for BoxedLimitOffsetClause<'a, WasmSqlite> { 51 | fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 52 | match (self.limit.as_ref(), self.offset.as_ref()) { 53 | (Some(limit), Some(offset)) => { 54 | limit.walk_ast(out.reborrow())?; 55 | offset.walk_ast(out.reborrow())?; 56 | } 57 | (Some(limit), None) => { 58 | limit.walk_ast(out.reborrow())?; 59 | } 60 | (None, Some(offset)) => { 61 | // See the `QueryFragment` implementation for `LimitOffsetClause` for details. 62 | out.push_sql(" LIMIT -1 "); 63 | offset.walk_ast(out.reborrow())?; 64 | } 65 | (None, None) => {} 66 | } 67 | Ok(()) 68 | } 69 | } 70 | 71 | // Have explicit impls here because we need to set `Some`/`None` for the clauses 72 | // correspondingly, otherwise we cannot match on it in the `QueryFragment` impl 73 | // above 74 | impl<'a> IntoBoxedClause<'a, WasmSqlite> for LimitOffsetClause { 75 | type BoxedClause = BoxedLimitOffsetClause<'a, WasmSqlite>; 76 | 77 | fn into_boxed(self) -> Self::BoxedClause { 78 | BoxedLimitOffsetClause { 79 | limit: None, 80 | offset: None, 81 | } 82 | } 83 | } 84 | 85 | impl<'a, L> IntoBoxedClause<'a, WasmSqlite> for LimitOffsetClause, NoOffsetClause> 86 | where 87 | L: QueryFragment + Send + 'a, 88 | { 89 | type BoxedClause = BoxedLimitOffsetClause<'a, WasmSqlite>; 90 | 91 | fn into_boxed(self) -> Self::BoxedClause { 92 | BoxedLimitOffsetClause { 93 | limit: Some(Box::new(self.limit_clause)), 94 | offset: None, 95 | } 96 | } 97 | } 98 | 99 | impl<'a, O> IntoBoxedClause<'a, WasmSqlite> for LimitOffsetClause> 100 | where 101 | O: QueryFragment + Send + 'a, 102 | { 103 | type BoxedClause = BoxedLimitOffsetClause<'a, WasmSqlite>; 104 | 105 | fn into_boxed(self) -> Self::BoxedClause { 106 | BoxedLimitOffsetClause { 107 | limit: None, 108 | offset: Some(Box::new(self.offset_clause)), 109 | } 110 | } 111 | } 112 | 113 | impl<'a, L, O> IntoBoxedClause<'a, WasmSqlite> 114 | for LimitOffsetClause, OffsetClause> 115 | where 116 | L: QueryFragment + Send + 'a, 117 | O: QueryFragment + Send + 'a, 118 | { 119 | type BoxedClause = BoxedLimitOffsetClause<'a, WasmSqlite>; 120 | 121 | fn into_boxed(self) -> Self::BoxedClause { 122 | BoxedLimitOffsetClause { 123 | limit: Some(Box::new(self.limit_clause)), 124 | offset: Some(Box::new(self.offset_clause)), 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/query_builder/mod.rs: -------------------------------------------------------------------------------- 1 | //! The SQLite query builder 2 | 3 | use super::backend::WasmSqlite; 4 | use diesel::query_builder::QueryBuilder; 5 | use diesel::result::QueryResult; 6 | 7 | pub(super) mod insert_with_default_sqlite; 8 | mod limit_offset; 9 | // mod query_fragment_impls; 10 | mod returning; 11 | 12 | /// Constructs SQL queries for use with the SQLite backend 13 | #[allow(missing_debug_implementations)] 14 | #[derive(Default)] 15 | pub struct SqliteQueryBuilder { 16 | sql: String, 17 | } 18 | 19 | impl SqliteQueryBuilder { 20 | /// Construct a new query builder with an empty query 21 | pub fn new() -> Self { 22 | SqliteQueryBuilder::default() 23 | } 24 | } 25 | 26 | impl QueryBuilder for SqliteQueryBuilder { 27 | fn push_sql(&mut self, sql: &str) { 28 | self.sql.push_str(sql); 29 | } 30 | 31 | fn push_identifier(&mut self, identifier: &str) -> QueryResult<()> { 32 | self.push_sql("`"); 33 | self.push_sql(&identifier.replace('`', "``")); 34 | self.push_sql("`"); 35 | Ok(()) 36 | } 37 | 38 | fn push_bind_param(&mut self) { 39 | self.push_sql("?"); 40 | } 41 | 42 | fn finish(self) -> String { 43 | self.sql 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/query_builder/query_fragment_impls.rs: -------------------------------------------------------------------------------- 1 | use crate::WasmSqlite; 2 | use diesel::query_builder::into_conflict_clause::OnConflictSelectWrapper; 3 | use diesel::query_builder::where_clause::BoxedWhereClause; 4 | use diesel::query_builder::where_clause::WhereClause; 5 | use diesel::query_builder::AstPass; 6 | use diesel::query_builder::BoxedSelectStatement; 7 | use diesel::query_builder::QueryFragment; 8 | use diesel::query_builder::SelectStatement; 9 | use diesel::result::QueryResult; 10 | use diesel::QueryId; 11 | 12 | // The corresponding impl for`NoWhereClause` is missing because of 13 | // https://www.sqlite.org/lang_UPSERT.html (Parsing Ambiguity) 14 | impl QueryFragment 15 | for OnConflictSelectWrapper, O, LOf, G, H, LC>> 16 | where 17 | SelectStatement, O, LOf, G, H, LC>: QueryFragment, 18 | { 19 | fn walk_ast<'b>(&'b self, out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 20 | self.0.walk_ast(out) 21 | } 22 | } 23 | 24 | impl<'a, ST, QS, GB> QueryFragment 25 | for OnConflictSelectWrapper> 26 | where 27 | BoxedSelectStatement<'a, ST, QS, WasmSqlite, GB>: QueryFragment, 28 | QS: QueryFragment, 29 | { 30 | fn walk_ast<'b>(&'b self, pass: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 31 | // https://www.sqlite.org/lang_UPSERT.html (Parsing Ambiguity) 32 | self.0.build_query(pass, |where_clause, mut pass| { 33 | match where_clause { 34 | BoxedWhereClause::None => pass.push_sql(" WHERE 1=1 "), 35 | w => w.walk_ast(pass.reborrow())?, 36 | } 37 | Ok(()) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/query_builder/returning.rs: -------------------------------------------------------------------------------- 1 | use crate::backend::{SqliteReturningClause, WasmSqlite}; 2 | use diesel::query_builder::ReturningClause; 3 | use diesel::query_builder::{AstPass, QueryFragment}; 4 | use diesel::result::QueryResult; 5 | 6 | impl QueryFragment for ReturningClause 7 | where 8 | Expr: QueryFragment, 9 | { 10 | fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 11 | out.skip_from(true); 12 | out.push_sql(" RETURNING "); 13 | self.0.walk_ast(out.reborrow())?; 14 | Ok(()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/sqlite_fixes.rs: -------------------------------------------------------------------------------- 1 | use crate::{connection::WasmSqliteConnection, WasmSqlite}; 2 | use diesel::{ 3 | associations::HasTable, 4 | dsl::{Find, Update}, 5 | expression::{is_aggregate, MixedAggregates, ValidGrouping}, 6 | insertable::{ColumnInsertValue, DefaultableColumnInsertValue, InsertValues}, 7 | prelude::{AsChangeset, Identifiable}, 8 | query_builder::{ 9 | AstPass, InsertOrIgnore, IntoUpdateTarget, NoFromClause, QueryFragment, Replace, 10 | }, 11 | query_dsl::{ 12 | methods::{ExecuteDsl, FindDsl, LoadQuery}, 13 | UpdateAndFetchResults, 14 | }, 15 | AppearsOnTable, Column, Expression, QueryResult, RunQueryDsl, Table, 16 | }; 17 | 18 | /// We re-define Dsl traits to make `insert_with_default_sqlite.rs` generic over all `Connection` 19 | /// implementations with `WasmSqlite` backend`. This works around Rusts orphan rules. 20 | pub mod dsl { 21 | use diesel::{ 22 | backend::Backend, 23 | dsl::Limit, 24 | query_builder::{QueryFragment, QueryId}, 25 | query_dsl::methods::{LimitDsl, LoadQuery}, 26 | Connection, QueryResult, 27 | }; 28 | 29 | pub trait ExecuteDsl< 30 | Conn: Connection, 31 | DB: Backend = ::Backend, 32 | >: Sized 33 | { 34 | fn execute(query: Self, conn: &mut Conn) -> QueryResult; 35 | } 36 | 37 | impl ExecuteDsl for T 38 | where 39 | Conn: Connection, 40 | DB: Backend, 41 | T: QueryFragment + QueryId, 42 | { 43 | fn execute(query: T, conn: &mut Conn) -> QueryResult { 44 | conn.execute_returning_count(&query) 45 | } 46 | } 47 | 48 | pub trait RunQueryDsl: Sized + diesel::query_dsl::RunQueryDsl { 49 | fn execute(self, conn: &mut Conn) -> QueryResult 50 | where 51 | Conn: Connection, 52 | Self: ExecuteDsl, 53 | { 54 | ExecuteDsl::execute(self, conn) 55 | } 56 | 57 | fn load<'query, U>(self, conn: &mut Conn) -> QueryResult> 58 | where 59 | Self: LoadQuery<'query, Conn, U>, 60 | { 61 | >::load(self, conn) 62 | } 63 | fn load_iter<'conn, 'query: 'conn, U, B>( 64 | self, 65 | conn: &'conn mut Conn, 66 | ) -> QueryResult> 67 | where 68 | U: 'conn, 69 | Self: LoadQuery<'query, Conn, U, B> + 'conn, 70 | { 71 | >::load_iter(self, conn) 72 | } 73 | fn get_result<'query, U>(self, conn: &mut Conn) -> QueryResult 74 | where 75 | Self: LoadQuery<'query, Conn, U>, 76 | { 77 | >::get_result(self, conn) 78 | } 79 | fn get_results<'query, U>(self, conn: &mut Conn) -> QueryResult> 80 | where 81 | Self: LoadQuery<'query, Conn, U>, 82 | { 83 | >::get_results(self, conn) 84 | } 85 | fn first<'query, U>(self, conn: &mut Conn) -> QueryResult 86 | where 87 | Self: LimitDsl, 88 | Limit: LoadQuery<'query, Conn, U>, 89 | { 90 | >::first(self, conn) 91 | } 92 | } 93 | 94 | impl RunQueryDsl for T where T: diesel::query_dsl::RunQueryDsl {} 95 | } 96 | 97 | impl InsertValues 98 | for DefaultableColumnInsertValue> 99 | where 100 | Col: Column, 101 | Expr: Expression + AppearsOnTable, 102 | Self: QueryFragment, 103 | { 104 | fn column_names(&self, mut out: AstPass<'_, '_, WasmSqlite>) -> QueryResult<()> { 105 | if let Self::Expression(..) = *self { 106 | out.push_identifier(Col::NAME)?; 107 | } 108 | Ok(()) 109 | } 110 | } 111 | 112 | impl 113 | QueryFragment< 114 | WasmSqlite, 115 | diesel::backend::sql_dialect::default_keyword_for_insert::DoesNotSupportDefaultKeyword, 116 | > for DefaultableColumnInsertValue> 117 | where 118 | Expr: QueryFragment, 119 | { 120 | fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 121 | if let Self::Expression(ref inner) = *self { 122 | inner.walk_ast(out.reborrow())?; 123 | } 124 | Ok(()) 125 | } 126 | } 127 | 128 | impl QueryFragment for InsertOrIgnore { 129 | fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 130 | out.push_sql("INSERT OR IGNORE"); 131 | Ok(()) 132 | } 133 | } 134 | 135 | impl QueryFragment for Replace { 136 | fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 137 | out.push_sql("REPLACE"); 138 | Ok(()) 139 | } 140 | } 141 | 142 | impl<'b, Changes, Output> UpdateAndFetchResults for WasmSqliteConnection 143 | where 144 | Changes: Copy + Identifiable, 145 | Changes: AsChangeset::Table> + IntoUpdateTarget, 146 | Changes::Table: FindDsl, 147 | Update: ExecuteDsl, 148 | Find: LoadQuery<'b, WasmSqliteConnection, Output>, 149 | ::AllColumns: ValidGrouping<()>, 150 | <::AllColumns as ValidGrouping<()>>::IsAggregate: 151 | MixedAggregates, 152 | { 153 | fn update_and_fetch(&mut self, changeset: Changes) -> QueryResult { 154 | diesel::update(changeset).set(changeset).execute(self)?; 155 | Changes::table().find(changeset.id()).get_result(self) 156 | } 157 | } 158 | 159 | mod parenthesis_wrapper { 160 | use super::*; 161 | 162 | use crate::WasmSqlite; 163 | // use diesel::query_builder::combination_clause::SupportsCombinationClause; 164 | use diesel::query_builder::{ 165 | All, AstPass, Distinct, Except, Intersect, ParenthesisWrapper, QueryFragment, 166 | SupportsCombinationClause, Union, 167 | }; 168 | 169 | impl> QueryFragment for ParenthesisWrapper { 170 | fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, WasmSqlite>) -> QueryResult<()> { 171 | // SQLite does not support parenthesis around this clause 172 | // we can emulate this by construct a fake outer 173 | // SELECT * FROM (inner_query) statement 174 | out.push_sql("SELECT * FROM ("); 175 | self.inner.walk_ast(out.reborrow())?; 176 | out.push_sql(")"); 177 | Ok(()) 178 | } 179 | } 180 | 181 | impl SupportsCombinationClause for WasmSqlite {} 182 | impl SupportsCombinationClause for WasmSqlite {} 183 | impl SupportsCombinationClause for WasmSqlite {} 184 | impl SupportsCombinationClause for WasmSqlite {} 185 | } 186 | 187 | // Anything commented here are implementation present in diesel 188 | // but not possible because parts of it exist as private types in diesel. 189 | 190 | /* 191 | impl AsExpression for now { 192 | type Expression = Coerce; 193 | 194 | fn as_expression(self) -> Self::Expression { 195 | Coerce::new(self) 196 | } 197 | } 198 | 199 | impl AsExpression> for now { 200 | type Expression = Coerce>; 201 | 202 | fn as_expression(self) -> Self::Expression { 203 | Coerce::new(self) 204 | } 205 | } 206 | */ 207 | 208 | /* 209 | use diesel::dsl; 210 | use diesel::expression::grouped::Grouped; 211 | use diesel::expression::AsExpression; 212 | use diesel::operators::*; 213 | use diesel::sql_types::SqlType; 214 | 215 | /// Sqlite specific methods which are present on all expressions. 216 | pub trait SqliteExpressionMethods: Expression + Sized { 217 | /// Creates a Sqlite `IS` expression. 218 | /// 219 | /// The `IS` operator work like = except when one or both of the operands are NULL. 220 | /// In this case, if both operands are NULL, then the `IS` operator evaluates to true. 221 | /// If one operand is NULL and the other is not, then the `IS` operator evaluates to false. 222 | /// It is not possible for an `IS` expression to evaluate to NULL. 223 | /// 224 | /// # Example 225 | /// 226 | /// ```rust 227 | /// # include!("../../doctest_setup.rs"); 228 | /// # 229 | /// # fn main() { 230 | /// # run_test().unwrap(); 231 | /// # } 232 | /// # 233 | /// # fn run_test() -> QueryResult<()> { 234 | /// # use schema::animals::dsl::*; 235 | /// # let connection = &mut establish_connection(); 236 | /// let jack_is_a_dog = animals 237 | /// .select(name) 238 | /// .filter(species.is("dog")) 239 | /// .get_results::>(connection)?; 240 | /// assert_eq!(vec![Some("Jack".to_string())], jack_is_a_dog); 241 | /// # Ok(()) 242 | /// # } 243 | /// ``` 244 | fn is(self, other: T) -> dsl::Is 245 | where 246 | Self::SqlType: SqlType, 247 | T: AsExpression, 248 | { 249 | Grouped(Is::new(self, other.as_expression())) 250 | } 251 | 252 | /// Creates a Sqlite `IS NOT` expression. 253 | /// 254 | /// The `IS NOT` operator work like != except when one or both of the operands are NULL. 255 | /// In this case, if both operands are NULL, then the `IS NOT` operator evaluates to false. 256 | /// If one operand is NULL and the other is not, then the `IS NOT` operator is true. 257 | /// It is not possible for an `IS NOT` expression to evaluate to NULL. 258 | /// 259 | /// # Example 260 | /// 261 | /// ```rust 262 | /// # include!("../../doctest_setup.rs"); 263 | /// # 264 | /// # fn main() { 265 | /// # run_test().unwrap(); 266 | /// # } 267 | /// # 268 | /// # fn run_test() -> QueryResult<()> { 269 | /// # use schema::animals::dsl::*; 270 | /// # let connection = &mut establish_connection(); 271 | /// let jack_is_not_a_spider = animals 272 | /// .select(name) 273 | /// .filter(species.is_not("spider")) 274 | /// .get_results::>(connection)?; 275 | /// assert_eq!(vec![Some("Jack".to_string())], jack_is_not_a_spider); 276 | /// # Ok(()) 277 | /// # } 278 | /// ``` 279 | #[allow(clippy::wrong_self_convention)] // This is named after the sql operator 280 | fn is_not(self, other: T) -> dsl::IsNot 281 | where 282 | Self::SqlType: SqlType, 283 | T: AsExpression, 284 | { 285 | Grouped(IsNot::new(self, other.as_expression())) 286 | } 287 | } 288 | 289 | impl SqliteExpressionMethods for T {} 290 | */ 291 | -------------------------------------------------------------------------------- /src/sqlite_types.rs: -------------------------------------------------------------------------------- 1 | use super::backend::{SqliteType, WasmSqlite}; 2 | use diesel::sql_types::*; 3 | 4 | pub mod to_sql; 5 | 6 | //TODO These Database Types are defined in the wasm file and should be imported. 7 | // this is easier for now because of quirks with converting from JsValue to integer within extern 8 | // "C" declaration. 9 | macro_rules! impl_has_sql_type { 10 | ($type:ty, $sql_type:expr) => { 11 | impl HasSqlType<$type> for WasmSqlite { 12 | fn metadata(_: &mut ()) -> SqliteType { 13 | $sql_type 14 | } 15 | } 16 | }; 17 | } 18 | 19 | impl_has_sql_type!(Bool, SqliteType::Integer); 20 | impl_has_sql_type!(SmallInt, SqliteType::SmallInt); 21 | impl_has_sql_type!(Integer, SqliteType::Integer); 22 | impl_has_sql_type!(BigInt, SqliteType::Long); 23 | impl_has_sql_type!(Float, SqliteType::Float); 24 | impl_has_sql_type!(Double, SqliteType::Double); 25 | impl_has_sql_type!(Numeric, SqliteType::Double); 26 | impl_has_sql_type!(Text, SqliteType::Text); 27 | impl_has_sql_type!(Binary, SqliteType::Binary); 28 | impl_has_sql_type!(Date, SqliteType::Text); 29 | impl_has_sql_type!(Time, SqliteType::Text); 30 | impl_has_sql_type!(Timestamp, SqliteType::Text); 31 | -------------------------------------------------------------------------------- /src/sqlite_types/to_sql.rs: -------------------------------------------------------------------------------- 1 | // mod date_and_time; 2 | // mod numeric; 3 | 4 | //TODO: CODE CAN BE SHARED (pretty muhch exactly the same) 5 | use crate::connection::SqliteValue; 6 | use crate::WasmSqlite; 7 | use diesel::deserialize::{self, FromSql}; 8 | use diesel::query_builder::QueryId; 9 | use diesel::serialize::{self, IsNull, Output, ToSql}; 10 | use diesel::sql_types; 11 | use diesel::sql_types::SqlType; 12 | 13 | /// The returned pointer is *only* valid for the lifetime to the argument of 14 | /// `from_sql`. This impl is intended for uses where you want to write a new 15 | /// impl in terms of `String`, but don't want to allocate. 16 | /// 17 | /// FIXME: 18 | /// We have to return a 19 | /// raw pointer instead of a reference with a lifetime due to the structure of 20 | /// `FromSql` because we allocate a string in `read_text` (since SQLite memory is not shared with 21 | /// us). So this function would 22 | /// produce a dangling pointer. 23 | /* 24 | // Not posible until we share mem with sqlite. There's no 25 | // way to avoid an allocation into our host memory until then. 26 | 27 | impl FromSql for *const str { 28 | fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result { 29 | tracing::debug!("IN FROM SQL"); 30 | let text = value.read_text(); 31 | let text = text.as_str(); 32 | Ok(text as *const _) 33 | } 34 | } 35 | */ 36 | 37 | impl FromSql for String { 38 | fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result { 39 | Ok(value.read_text()) 40 | } 41 | } 42 | 43 | /* Not Possible until we share mem with SQLite 44 | impl Queryable for *const str { 45 | type Row = Self; 46 | 47 | fn build(row: Self::Row) -> deserialize::Result { 48 | Ok(row) 49 | } 50 | } 51 | */ 52 | 53 | impl FromSql for Vec { 54 | fn from_sql(bytes: SqliteValue<'_, '_, '_>) -> deserialize::Result { 55 | Ok(bytes.read_blob()) 56 | } 57 | } 58 | 59 | /// The returned pointer is *only* valid for the lifetime to the argument of 60 | /// `from_sql`. This impl is intended for uses where you want to write a new 61 | /// impl in terms of `Vec`, but don't want to allocate. We have to return a 62 | /// raw pointer instead of a reference with a lifetime due to the structure of 63 | /// `FromSql` 64 | /* Not possible until we share mem with SQLite 65 | impl FromSql for *const [u8] { 66 | fn from_sql(bytes: SqliteValue<'_, '_, '_>) -> deserialize::Result { 67 | let bytes = bytes.read_blob(); 68 | let bytes = bytes.as_slice(); 69 | Ok(bytes as *const _) 70 | } 71 | } 72 | 73 | impl Queryable for *const [u8] { 74 | type Row = Self; 75 | 76 | fn build(row: Self::Row) -> deserialize::Result { 77 | Ok(row) 78 | } 79 | } 80 | */ 81 | 82 | impl FromSql for i16 { 83 | fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result { 84 | Ok(value.read_integer() as i16) 85 | } 86 | } 87 | 88 | impl FromSql for i32 { 89 | fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result { 90 | Ok(value.read_integer()) 91 | } 92 | } 93 | 94 | impl FromSql for bool { 95 | fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result { 96 | Ok(value.read_integer() != 0) 97 | } 98 | } 99 | 100 | impl FromSql for i64 { 101 | fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result { 102 | Ok(value.read_long()) 103 | } 104 | } 105 | 106 | impl FromSql for f32 { 107 | fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result { 108 | Ok(value.read_double() as f32) 109 | } 110 | } 111 | 112 | impl FromSql for f64 { 113 | fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result { 114 | Ok(value.read_double()) 115 | } 116 | } 117 | 118 | impl ToSql for bool { 119 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, WasmSqlite>) -> serialize::Result { 120 | let int_value = if *self { &1 } else { &0 }; 121 | >::to_sql(int_value, out) 122 | } 123 | } 124 | 125 | impl ToSql for str { 126 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, WasmSqlite>) -> serialize::Result { 127 | out.set_value(self); 128 | Ok(IsNull::No) 129 | } 130 | } 131 | 132 | impl ToSql for [u8] { 133 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, WasmSqlite>) -> serialize::Result { 134 | out.set_value(self); 135 | Ok(IsNull::No) 136 | } 137 | } 138 | 139 | impl ToSql for i16 { 140 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, WasmSqlite>) -> serialize::Result { 141 | out.set_value(*self as i32); 142 | Ok(IsNull::No) 143 | } 144 | } 145 | 146 | impl ToSql for i32 { 147 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, WasmSqlite>) -> serialize::Result { 148 | out.set_value(*self); 149 | Ok(IsNull::No) 150 | } 151 | } 152 | 153 | impl ToSql for i64 { 154 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, WasmSqlite>) -> serialize::Result { 155 | out.set_value(*self); 156 | Ok(IsNull::No) 157 | } 158 | } 159 | 160 | impl ToSql for f32 { 161 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, WasmSqlite>) -> serialize::Result { 162 | out.set_value(*self as f64); 163 | Ok(IsNull::No) 164 | } 165 | } 166 | 167 | impl ToSql for f64 { 168 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, WasmSqlite>) -> serialize::Result { 169 | out.set_value(*self); 170 | Ok(IsNull::No) 171 | } 172 | } 173 | 174 | /// The SQLite timestamp with time zone type 175 | /// 176 | /// ### [`ToSql`] impls 177 | /// 178 | /// - [`chrono::NaiveDateTime`] with `feature = "chrono"` 179 | /// - [`chrono::DateTime`] with `feature = "chrono"` 180 | /// - [`time::PrimitiveDateTime`] with `feature = "time"` 181 | /// - [`time::OffsetDateTime`] with `feature = "time"` 182 | /// 183 | /// ### [`FromSql`] impls 184 | /// 185 | /// - [`chrono::NaiveDateTime`] with `feature = "chrono"` 186 | /// - [`chrono::DateTime`] with `feature = "chrono"` 187 | /// - [`time::PrimitiveDateTime`] with `feature = "time"` 188 | /// - [`time::OffsetDateTime`] with `feature = "time"` 189 | /// 190 | /// [`ToSql`]: crate::serialize::ToSql 191 | /// [`FromSql`]: crate::deserialize::FromSql 192 | #[derive(Debug, Clone, Copy, Default, QueryId, SqlType)] 193 | #[diesel(sqlite_type(name = "Text"))] 194 | pub struct Timestamptz; 195 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | //! Common utilities/imports amongst WebAssembly tests 2 | use prelude::*; 3 | 4 | use tokio::sync::OnceCell; 5 | 6 | static INIT: OnceCell<()> = OnceCell::const_new(); 7 | 8 | pub async fn init() { 9 | INIT.get_or_init(|| async { 10 | console::log_1(&"INIT".into()); 11 | let config = tracing_wasm::WASMLayerConfigBuilder::default() 12 | .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) 13 | .build(); 14 | tracing_wasm::set_as_global_default_with_config(config); 15 | console_error_panic_hook::set_once(); 16 | sqlite_web::init_sqlite().await; 17 | }) 18 | .await; 19 | } 20 | 21 | pub async fn connection() -> WasmSqliteConnection { 22 | sqlite_web::init_sqlite().await; 23 | WasmSqliteConnection::establish(":memory:").unwrap() 24 | } 25 | 26 | // re-exports used in tests 27 | #[allow(unused)] 28 | pub mod prelude { 29 | pub(crate) use super::init; 30 | pub(crate) use diesel::{ 31 | connection::{Connection, LoadConnection}, 32 | debug_query, 33 | deserialize::{self, FromSql, FromSqlRow}, 34 | insert_into, 35 | prelude::*, 36 | sql_types::{Integer, Nullable, Text}, 37 | }; 38 | pub(crate) use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; 39 | pub(crate) use sqlite_web::{ 40 | connection::WasmSqliteConnection, WasmSqlite, 41 | }; 42 | pub(crate) use serde::Deserialize; 43 | pub(crate) use wasm_bindgen_test::*; 44 | pub(crate) use web_sys::console; 45 | #[cfg(feature = "unsafe-debug-query")] 46 | pub(crate) use sqlite_web::DebugQueryWrapper; 47 | } 48 | -------------------------------------------------------------------------------- /tests/dedicated_web_worker.rs: -------------------------------------------------------------------------------- 1 | //! Integration Test Organization in rust is little bit non-standard comapred to normal 2 | //! organization in a library/binary. 3 | //! In order to avoid being compiled as a test, common functions must be defined in 4 | //! `common/mod.rs` 5 | //! 6 | //! Tests are separated into separate files in the module `test`. 7 | #![recursion_limit = "256"] 8 | #![cfg(target_arch = "wasm32")] 9 | 10 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker); 11 | 12 | mod common; 13 | 14 | mod test { 15 | mod row; 16 | mod web; 17 | } 18 | -------------------------------------------------------------------------------- /tests/migrations/2024-08-20-203551_create_books/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE books 2 | -------------------------------------------------------------------------------- /tests/migrations/2024-08-20-203551_create_books/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE books ( 2 | id INTEGER PRIMARY KEY NOT NULL, 3 | title TEXT NOT NULL, 4 | author TEXT 5 | ) 6 | -------------------------------------------------------------------------------- /tests/migrations/2024-09-03-193701_test_table/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | -------------------------------------------------------------------------------- /tests/migrations/2024-09-03-193701_test_table/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE test_table( 2 | -- The inbox_id the update refers to 3 | "id" text NOT NULL, 4 | -- The sequence_id of the update 5 | "id2" bigint NOT NULL, 6 | -- Based on the timestamp given by the server 7 | "timestamp_ns" bigint NOT NULL, 8 | -- Random ID generated by group creator 9 | "payload" BLOB NOT NULL, 10 | -- Compound primary key of the `inbox_id` and `sequence_id` 11 | PRIMARY KEY (id, id2) 12 | ); 13 | 14 | 15 | CREATE INDEX idx_test_table_id_id2_asc ON test_table(id, id2 ASC); 16 | -------------------------------------------------------------------------------- /tests/test/row.rs: -------------------------------------------------------------------------------- 1 | use crate::common::{connection, prelude::*}; 2 | use sqlite_web::dsl::RunQueryDsl; 3 | 4 | // test copied from diesel 5 | #[wasm_bindgen_test] 6 | async fn fun_with_row_iters() { 7 | sqlite_web::init_sqlite().await; 8 | 9 | diesel::table! { 10 | #[allow(unused_parens)] 11 | users(id) { 12 | id -> Integer, 13 | name -> Text, 14 | } 15 | } 16 | 17 | use diesel::connection::LoadConnection; 18 | use diesel::deserialize::{FromSql, FromSqlRow}; 19 | use diesel::row::{Field, Row}; 20 | use diesel::sql_types; 21 | 22 | let conn: &mut WasmSqliteConnection = &mut connection().await; 23 | 24 | diesel::sql_query("CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT NOT NULL);") 25 | .execute(conn) 26 | .unwrap(); 27 | 28 | diesel::insert_into(users::table) 29 | .values(vec![ 30 | (users::id.eq(1), users::name.eq("Sean")), 31 | (users::id.eq(2), users::name.eq("Tess")), 32 | ]) 33 | .execute(conn) 34 | .unwrap(); 35 | 36 | let query = users::table.select((users::id, users::name)); 37 | 38 | let expected = vec![(1, String::from("Sean")), (2, String::from("Tess"))]; 39 | let row_iter = conn.load(query).unwrap(); 40 | for (row, expected) in row_iter.zip(&expected) { 41 | let row = row.expect("Unwrap failed"); 42 | 43 | let deserialized = <(i32, String) as FromSqlRow< 44 | (sql_types::Integer, sql_types::Text), 45 | _, 46 | >>::build_from_row(&row) 47 | .unwrap(); 48 | 49 | assert_eq!(&deserialized, expected); 50 | } 51 | 52 | { 53 | let collected_rows = conn.load(query).unwrap().collect::>(); 54 | 55 | for (row, expected) in collected_rows.iter().zip(&expected) { 56 | let deserialized = row 57 | .as_ref() 58 | .map(|row| { 59 | <(i32, String) as FromSqlRow< 60 | (sql_types::Integer, sql_types::Text), 61 | _, 62 | >>::build_from_row(row).unwrap() 63 | }) 64 | .unwrap(); 65 | 66 | assert_eq!(&deserialized, expected); 67 | } 68 | } 69 | 70 | let mut row_iter = conn.load(query).unwrap(); 71 | 72 | let first_row = row_iter.next().unwrap().unwrap(); 73 | let first_fields = (first_row.get(0).unwrap(), first_row.get(1).unwrap()); 74 | let first_values = (first_fields.0.value(), first_fields.1.value()); 75 | 76 | assert!(row_iter.next().unwrap().is_err()); 77 | std::mem::drop(first_values); 78 | assert!(row_iter.next().unwrap().is_err()); 79 | std::mem::drop(first_fields); 80 | 81 | let second_row = row_iter.next().unwrap().unwrap(); 82 | let second_fields = (second_row.get(0).unwrap(), second_row.get(1).unwrap()); 83 | let second_values = (second_fields.0.value(), second_fields.1.value()); 84 | 85 | assert!(row_iter.next().unwrap().is_err()); 86 | std::mem::drop(second_values); 87 | assert!(row_iter.next().unwrap().is_err()); 88 | std::mem::drop(second_fields); 89 | 90 | assert!(row_iter.next().is_none()); 91 | 92 | let first_fields = (first_row.get(0).unwrap(), first_row.get(1).unwrap()); 93 | let second_fields = (second_row.get(0).unwrap(), second_row.get(1).unwrap()); 94 | 95 | let first_values = (first_fields.0.value(), first_fields.1.value()); 96 | let second_values = (second_fields.0.value(), second_fields.1.value()); 97 | 98 | assert_eq!( 99 | >::from_nullable_sql(first_values.0) 100 | .unwrap(), 101 | expected[0].0 102 | ); 103 | assert_eq!( 104 | >::from_nullable_sql(first_values.1) 105 | .unwrap(), 106 | expected[0].1 107 | ); 108 | 109 | assert_eq!( 110 | >::from_nullable_sql(second_values.0) 111 | .unwrap(), 112 | expected[1].0 113 | ); 114 | assert_eq!( 115 | >::from_nullable_sql(second_values.1) 116 | .unwrap(), 117 | expected[1].1 118 | ); 119 | 120 | let first_fields = (first_row.get(0).unwrap(), first_row.get(1).unwrap()); 121 | let first_values = (first_fields.0.value(), first_fields.1.value()); 122 | 123 | assert_eq!( 124 | >::from_nullable_sql(first_values.0) 125 | .unwrap(), 126 | expected[0].0 127 | ); 128 | assert_eq!( 129 | >::from_nullable_sql(first_values.1) 130 | .unwrap(), 131 | expected[0].1 132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /tests/test/web.rs: -------------------------------------------------------------------------------- 1 | //! General tests for migrations/diesel ORM/persistent databases 2 | use crate::common::prelude::*; 3 | use sqlite_web::dsl::RunQueryDsl; 4 | 5 | pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./tests/migrations/"); 6 | 7 | mod schema { 8 | diesel::table! { 9 | books { 10 | id -> Integer, 11 | title -> Text, 12 | author -> Nullable, 13 | // published_year -> Timestamp, 14 | } 15 | } 16 | 17 | diesel::table! { 18 | test_table (id, id2) { 19 | id -> Text, 20 | id2 -> BigInt, 21 | timestamp_ns -> BigInt, 22 | payload -> Binary, 23 | } 24 | } 25 | } 26 | 27 | use schema::{books, test_table}; 28 | 29 | #[derive(Deserialize, Insertable, Debug, PartialEq, Clone)] 30 | #[diesel(table_name = books)] 31 | pub struct BookForm { 32 | title: String, 33 | author: Option, 34 | // published_year: NaiveDateTime, 35 | } 36 | 37 | #[derive(Queryable, QueryableByName, Selectable, PartialEq, Debug)] 38 | #[diesel(table_name = books, check_for_backend(sqlite_web::WasmSqlite))] 39 | pub struct StoredBook { 40 | #[diesel(sql_type = Integer)] 41 | id: i32, 42 | #[diesel(sql_type = Text)] 43 | title: String, 44 | #[diesel(sql_type = Nullable)] 45 | author: Option, 46 | // published_year: NaiveDateTime, 47 | } 48 | 49 | async fn establish_connection() -> WasmSqliteConnection { 50 | sqlite_web::init_sqlite().await; 51 | 52 | let rng: u16 = rand::random(); 53 | let result = WasmSqliteConnection::establish(&format!("test-{}", rng)); 54 | let mut conn = result.unwrap(); 55 | tracing::info!("running migrations..."); 56 | conn.run_pending_migrations(MIGRATIONS).unwrap(); 57 | conn 58 | } 59 | 60 | fn insert_books(conn: &mut WasmSqliteConnection, new_books: Vec) -> QueryResult { 61 | use schema::books::dsl::*; 62 | let query = insert_into(books).values(new_books); 63 | // let sql = DebugQueryWrapper::<_, WasmSqlite>::new(&query).to_string(); 64 | // tracing::info!("QUERY = {}", sql); 65 | let rows_changed = query.execute(conn)?; 66 | Ok(rows_changed) 67 | } 68 | 69 | /* 70 | #[wasm_bindgen_test] 71 | fn examine_sql_from_insert_default_values() { 72 | use schema::books::dsl::*; 73 | 74 | let query = insert_into(books).default_values(); 75 | let sql = "INSERT INTO `books` DEFAULT VALUES -- binds: []"; 76 | assert_eq!(sql, debug_query::(&query).to_string()); 77 | console::log_1(&debug_query::(&query).to_string().into()); 78 | } 79 | */ 80 | 81 | #[wasm_bindgen_test] 82 | async fn test_orm_insert() { 83 | init().await; 84 | let mut conn = establish_connection().await; 85 | let new_books = vec![ 86 | BookForm { 87 | title: "Game of Thrones".into(), 88 | author: Some("George R.R".into()), 89 | // published_year: NaiveDate::from_ymd_opt(2015, 5, 3).unwrap(), 90 | }, 91 | BookForm { 92 | title: "The Hobbit".into(), 93 | author: Some("J.R.R. Tolkien".into()), 94 | // published_year: NaiveDate::from_ymd_opt(1937, 9, 21).unwrap(), 95 | }, 96 | BookForm { 97 | title: "To Kill a Mockingbird".into(), 98 | author: Some("Harper Lee".into()), 99 | // published_year: NaiveDate::from_ymd_opt(1960, 7, 11).unwrap(), 100 | }, 101 | BookForm { 102 | title: "1984".into(), 103 | author: Some("George Orwell".into()), 104 | // published_year: NaiveDate::from_ymd_opt(1949, 6, 8).unwrap(), 105 | }, 106 | BookForm { 107 | title: "Pride and Prejudice".into(), 108 | author: Some("Jane Austen".into()), 109 | // published_year: NaiveDate::from_ymd_opt(1813, 1, 28).unwrap(), 110 | }, 111 | BookForm { 112 | title: "Moby-Dick".into(), 113 | author: Some("Herman Melville".into()), 114 | // published_year: NaiveDate::from_ymd_opt(1851, 10, 18).unwrap(), 115 | }, 116 | ]; 117 | let rows_changed = insert_books(&mut conn, new_books).unwrap(); 118 | assert_eq!(rows_changed, 6); 119 | tracing::info!("{} rows changed", rows_changed); 120 | console::log_1(&"Showing Users".into()); 121 | let title = schema::books::table 122 | .select(schema::books::title) 123 | .filter(schema::books::id.eq(2)) 124 | .first::(&mut conn) 125 | .unwrap(); 126 | assert_eq!(title, "The Hobbit"); 127 | 128 | let author = schema::books::table 129 | .select(schema::books::author) 130 | .filter(schema::books::id.eq(1)) 131 | .first::>(&mut conn) 132 | .unwrap(); 133 | assert_eq!(author, Some("George R.R".into())); 134 | 135 | let id = schema::books::table 136 | .select(schema::books::id) 137 | .filter(schema::books::id.eq(1)) 138 | .first::(&mut conn) 139 | .unwrap(); 140 | assert_eq!(id, 1); 141 | 142 | let loaded_books = schema::books::dsl::books 143 | .select(StoredBook::as_select()) 144 | .limit(5) 145 | .load(&mut conn); 146 | assert_eq!( 147 | loaded_books.unwrap(), 148 | vec![ 149 | StoredBook { 150 | id: 1, 151 | title: "Game of Thrones".into(), 152 | author: Some("George R.R".into()), 153 | }, 154 | StoredBook { 155 | id: 2, 156 | title: "The Hobbit".into(), 157 | author: Some("J.R.R. Tolkien".into()), 158 | }, 159 | StoredBook { 160 | id: 3, 161 | title: "To Kill a Mockingbird".into(), 162 | author: Some("Harper Lee".into()), 163 | }, 164 | StoredBook { 165 | id: 4, 166 | title: "1984".into(), 167 | author: Some("George Orwell".into()), 168 | }, 169 | StoredBook { 170 | id: 5, 171 | title: "Pride and Prejudice".into(), 172 | author: Some("Jane Austen".into()), 173 | }, 174 | ] 175 | ) 176 | } 177 | 178 | /// StoredIdentityUpdate holds a serialized IdentityUpdate record 179 | #[derive(Insertable, Identifiable, Queryable, Selectable, Debug, Clone, PartialEq, Eq)] 180 | #[diesel(table_name = test_table)] 181 | #[diesel(primary_key(id, id2))] 182 | pub struct Item { 183 | pub id: String, 184 | pub id2: i64, 185 | pub timestamp_ns: i64, 186 | pub payload: Vec, 187 | } 188 | 189 | fn insert_or_ignore(updates: &[Item], conn: &mut WasmSqliteConnection) { 190 | use schema::test_table::dsl::*; 191 | 192 | diesel::insert_or_ignore_into(test_table) 193 | .values(updates) 194 | .execute(conn) 195 | .unwrap(); 196 | } 197 | 198 | #[wasm_bindgen_test] 199 | async fn can_insert_or_ignore() { 200 | init().await; 201 | let mut conn = establish_connection().await; 202 | let updates = vec![ 203 | Item { 204 | id: "test".into(), 205 | id2: 13, 206 | timestamp_ns: 1231232, 207 | payload: b"testing 1".to_vec(), 208 | }, 209 | Item { 210 | id: "test2".into(), 211 | id2: 14, 212 | timestamp_ns: 1201222, 213 | payload: b"testing 2".to_vec(), 214 | }, 215 | ]; 216 | insert_or_ignore(&updates, &mut conn); 217 | } 218 | 219 | #[wasm_bindgen_test] 220 | async fn can_retrieve_blob() { 221 | init().await; 222 | let mut conn = establish_connection().await; 223 | let updates = vec![ 224 | Item { 225 | id: "test".into(), 226 | id2: 13, 227 | timestamp_ns: 1231232, 228 | payload: b"testing 1".to_vec(), 229 | }, 230 | Item { 231 | id: "test2".into(), 232 | id2: 14, 233 | timestamp_ns: 1201222, 234 | payload: b"testing 2".to_vec(), 235 | }, 236 | ]; 237 | insert_or_ignore(&updates, &mut conn); 238 | 239 | let res = schema::test_table::dsl::test_table 240 | .select(Item::as_select()) 241 | .load(&mut conn) 242 | .unwrap(); 243 | 244 | assert_eq!(res[0].payload, b"testing 1"); 245 | assert_eq!(res[1].payload, b"testing 2"); 246 | 247 | } 248 | 249 | #[wasm_bindgen_test] 250 | async fn serializing() { 251 | init().await; 252 | let mut conn = establish_connection().await; 253 | let new_books = vec![BookForm { 254 | title: "Game of Thrones".into(), 255 | author: Some("George R.R".into()), 256 | }]; 257 | let rows_changed = insert_books(&mut conn, new_books).unwrap(); 258 | assert_eq!(rows_changed, 1); 259 | 260 | let serialized = conn.serialize(); 261 | 262 | let loaded_books = schema::books::dsl::books 263 | .select(StoredBook::as_select()) 264 | .load(&mut conn); 265 | assert_eq!(loaded_books.unwrap().len(), 1); 266 | 267 | // delete all the books 268 | diesel::delete(schema::books::table) 269 | .execute(&mut conn) 270 | .unwrap(); 271 | 272 | let loaded_books = schema::books::dsl::books 273 | .select(StoredBook::as_select()) 274 | .load(&mut conn); 275 | assert_eq!(loaded_books.unwrap().len(), 0); 276 | 277 | let result = conn.deserialize(&serialized.data); 278 | assert_eq!(result, 0); 279 | 280 | let loaded_books = schema::books::dsl::books 281 | .select(StoredBook::as_select()) 282 | .load(&mut conn); 283 | assert_eq!( 284 | loaded_books.unwrap(), 285 | vec![StoredBook { 286 | id: 1, 287 | title: "Game of Thrones".into(), 288 | author: Some("George R.R".into()), 289 | },] 290 | ); 291 | } 292 | 293 | #[wasm_bindgen_test] 294 | async fn can_find() { 295 | use schema::{books::dsl, self}; 296 | 297 | init().await; 298 | let mut conn = establish_connection().await; 299 | let new_books = vec![ 300 | BookForm { 301 | title: "Game of Thrones".into(), 302 | author: Some("George R.R".into()), 303 | }, 304 | BookForm { 305 | title: "1984".into(), 306 | author: Some("George Orwell".into()), 307 | } 308 | ]; 309 | 310 | let changed = insert_books(&mut conn, new_books).unwrap(); 311 | tracing::info!("{changed} rows changed"); 312 | 313 | let res: Option = dsl::books.find(1).first(&mut conn).optional().unwrap(); 314 | 315 | let res: Vec = diesel::sql_query("SELECT * FROM books where (id = 1)") 316 | .load::(&mut conn).unwrap(); 317 | tracing::debug!("SQL_QUERY RES: {:?}", res); 318 | /* 319 | assert_eq!(res, vec![ 320 | StoredBook { id: 0, title: "Game of Thrones".into(), author: Some("George R.R".into()) } 321 | ]); 322 | */ 323 | let stored_books = schema::books::dsl::books 324 | .select(StoredBook::as_select()) 325 | .load(&mut conn); 326 | tracing::debug!("Books: {:?}", stored_books); 327 | 328 | let first: Option = dsl::books.first(&mut conn).optional().unwrap(); 329 | tracing::debug!("FIRST BOOK {:?}", first) 330 | } 331 | --------------------------------------------------------------------------------