Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ pkg/sqlite_web_bg.wasm.b64

# macOS
.DS_Store

.claude
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,23 @@ wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
"Blob",
"BlobPropertyBag",
"BlobPropertyBag",
"Url",
"Worker",
"WorkerOptions",
"WorkerType",
"BroadcastChannel",
"MessageEvent",
"MessageEvent",
"DedicatedWorkerGlobalScope",
"Navigator",
"Window",
"Location"
"Location",
"StorageManager",
"FileSystemDirectoryHandle",
"FileSystemHandle",
"FileSystemRemoveOptions",
"FileSystemGetDirectoryOptions",
"DomException"
]}
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
136 changes: 122 additions & 14 deletions packages/sqlite-web/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use web_sys::Worker;

use crate::errors::SQLiteWasmDatabaseError;
use crate::messages::WORKER_ERROR_TYPE_INITIALIZATION_PENDING;
use crate::opfs::delete_opfs_sahpool_directory;
use crate::params::normalize_params_js;
use crate::ready::{InitializationState, ReadySignal};
use crate::utils::describe_js_value;
Expand All @@ -19,7 +20,8 @@ use crate::worker_template::generate_self_contained_worker;

#[wasm_bindgen]
pub struct SQLiteWasmDatabase {
worker: Worker,
worker: Rc<RefCell<Worker>>,
db_name: String,
pending_queries: Rc<RefCell<HashMap<u32, (js_sys::Function, js_sys::Function)>>>,
next_request_id: Rc<RefCell<u32>>,
ready_signal: ReadySignal,
Expand Down Expand Up @@ -63,7 +65,8 @@ impl SQLiteWasmDatabase {
let next_request_id = Rc::new(RefCell::new(1u32));

Ok(SQLiteWasmDatabase {
worker,
worker: Rc::new(RefCell::new(worker)),
db_name: db_name.to_string(),
pending_queries,
next_request_id,
ready_signal,
Expand Down Expand Up @@ -112,7 +115,7 @@ impl SQLiteWasmDatabase {
sql: &str,
params: Option<Array>,
) -> Result<String, SQLiteWasmDatabaseError> {
let worker = &self.worker;
let worker = Rc::clone(&self.worker);
let pending_queries = Rc::clone(&self.pending_queries);
let sql = sql.to_string();
let params_array = Self::normalize_params(params)?;
Expand Down Expand Up @@ -154,17 +157,19 @@ impl SQLiteWasmDatabase {
}

let rid_for_insert = request_id;
let promise =
js_sys::Promise::new(&mut |resolve, reject| match worker.post_message(&message) {
Ok(()) => {
pending_queries
.borrow_mut()
.insert(rid_for_insert, (resolve, reject));
}
Err(err) => {
let _ = reject.call1(&JsValue::NULL, &err);
}
});
let promise = js_sys::Promise::new(&mut |resolve, reject| match worker
.borrow()
.post_message(&message)
{
Ok(()) => {
pending_queries
.borrow_mut()
.insert(rid_for_insert, (resolve, reject));
}
Err(err) => {
let _ = reject.call1(&JsValue::NULL, &err);
}
});

let result = match JsFuture::from(promise).await {
Ok(value) => value,
Expand All @@ -177,6 +182,36 @@ impl SQLiteWasmDatabase {
};
Ok(result.as_string().unwrap_or_else(|| format!("{result:?}")))
}

#[wasm_export(js_name = "wipeAndRecreate", unchecked_return_type = "void")]
pub async fn wipe_and_recreate(&self) -> Result<(), SQLiteWasmDatabaseError> {
self.worker.borrow().terminate();

for (_, (_, reject)) in self.pending_queries.borrow_mut().drain() {
let err = JsValue::from_str("Database wipe in progress");
let _ = reject.call1(&JsValue::NULL, &err);
}

self.ready_signal.reset();

let deletion_result = delete_opfs_sahpool_directory().await;

let worker_code = generate_self_contained_worker(&self.db_name);
let new_worker =
create_worker_from_code(&worker_code).map_err(SQLiteWasmDatabaseError::JsError)?;

install_onmessage_handler(
&new_worker,
Rc::clone(&self.pending_queries),
self.ready_signal.clone(),
);

*self.worker.borrow_mut() = new_worker;

self.wait_until_ready().await?;

deletion_result
}
}

fn is_initialization_pending_error(err: &JsValue) -> bool {
Expand Down Expand Up @@ -294,4 +329,77 @@ mod tests {
let js_val = JsValue::from_str(WORKER_ERROR_TYPE_INITIALIZATION_PENDING);
assert!(is_initialization_pending_error(&js_val));
}

#[wasm_bindgen_test(async)]
async fn wipe_and_recreate_tests() {
let db = SQLiteWasmDatabase::new("test_wipe").await.unwrap();
db.wipe_and_recreate().await.unwrap();

db.query(
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
None,
)
.await
.unwrap();
db.query("INSERT INTO users (name) VALUES ('Alice')", None)
.await
.unwrap();

let result = db
.query("SELECT COUNT(*) as count FROM users", None)
.await
.unwrap();
assert!(result.contains("\"count\": 1"));

db.wipe_and_recreate().await.unwrap();

let result = db.query("SELECT * FROM users", None).await;
assert!(result.is_err() || result.unwrap().contains("no such table"));

let create_result = db
.query(
"CREATE TABLE new_table (id INTEGER PRIMARY KEY, value TEXT)",
None,
)
.await;
assert!(create_result.is_ok());

let insert_result = db
.query("INSERT INTO new_table (value) VALUES ('test')", None)
.await;
assert!(insert_result.is_ok());

let select_result = db.query("SELECT * FROM new_table", None).await.unwrap();
assert!(select_result.contains("test"));

for i in 0..3 {
db.query(&format!("CREATE TABLE t{} (id INTEGER)", i), None)
.await
.unwrap();
db.wipe_and_recreate().await.unwrap();
}

let result = db
.query("SELECT name FROM sqlite_master WHERE type='table'", None)
.await
.unwrap();
assert!(!result.contains("t0"));
assert!(!result.contains("t1"));
assert!(!result.contains("t2"));

let arr = Array::new();
arr.push(&JsValue::from_f64(f64::NAN));
let res = db.query("SELECT ?", Some(arr)).await;
assert!(res.is_err(), "NaN should be rejected");

let arr = Array::new();
arr.push(&JsValue::from_f64(f64::INFINITY));
let res = db.query("SELECT ?", Some(arr)).await;
assert!(res.is_err(), "+Infinity should be rejected");

let arr = Array::new();
arr.push(&JsValue::from_f64(f64::NEG_INFINITY));
let res = db.query("SELECT ?", Some(arr)).await;
assert!(res.is_err(), "-Infinity should be rejected");
}
Comment on lines +333 to +404
Copy link

@coderabbitai coderabbitai bot Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider splitting the test into focused test cases.

This test bundles wipe/recreate scenarios, consecutive wipes, and parameter validation (NaN/Infinity) into a single function. While functional, isolated tests improve debuggability and make failures easier to diagnose. The invalid parameter tests (lines 390-403) are unrelated to wipe_and_recreate and could be a separate test.

🤖 Prompt for AI Agents
In `@packages/sqlite-web/src/db.rs` around lines 333 - 404, Split the large
wasm_bindgen_test function wipe_and_recreate_tests into focused tests: keep one
async test (e.g., wipe_and_recreate_happy_path) that exercises
SQLiteWasmDatabase::new, SQLiteWasmDatabase::wipe_and_recreate,
creation/insertion/select, repeated wipes (the t0..t2 loop) and checks on
sqlite_master; move the parameter validation checks that call db.query with
NaN/Infinity into a separate async test (e.g., query_rejects_invalid_numbers)
that only asserts errors for JsValue::from_f64(NaN/INFINITY/NEG_INFINITY).
Ensure both functions retain #[wasm_bindgen_test(async)] and reuse the same
SQLiteWasmDatabase::new("test_wipe") setup or create distinct named DBs to avoid
interference.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have opted to use a single test because the database persists between the tests and it was causing us issues. By doing it this way, we make sure that we have we are testing everything properly.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@findolor, thank you for the clarification! That makes perfect sense—if the database state persists between test functions in your WASM test environment, consolidating the related scenarios into a single test is the right approach to ensure proper isolation and test correctness. I appreciate you explaining the reasoning behind this design decision.


✏️ Learnings added
Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 27
File: packages/sqlite-web/src/db.rs:333-404
Timestamp: 2026-01-27T10:13:33.826Z
Learning: In the sqlite-web WASM test environment, database state can persist between test functions, causing test interference. To ensure proper test isolation, related test scenarios for features like wipe_and_recreate should be consolidated into a single test function rather than split into multiple independent test cases.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web/lib.rs : Expose an async, promise-based query interface from the SQLiteWasmDatabase public API

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 14
File: svelte-test/tests/database-functions/float-is-zero.test.ts:39-57
Timestamp: 2025-09-19T07:06:20.173Z
Learning: PRAGMA function_list is not available in the WASM SQLite test environment used by svelte-test, so availability testing should rely on direct function calls rather than PRAGMA queries.

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web-core/src/database_functions.rs : Integrate and expose rain.math.float-backed custom database functions for SQLite queries

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web/worker_template.rs : Ensure the worker JavaScript generated by worker_template.rs embeds the WASM (base64) and does not fetch external WASM files

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 9
File: packages/sqlite-web-core/src/coordination.rs:52-65
Timestamp: 2025-09-15T06:11:31.781Z
Learning: In packages/sqlite-web-core/src/coordination.rs, the pattern of using `db.borrow_mut().take()` followed by async database operations and then putting the database back with `*db.borrow_mut() = Some(database)` is safe in their use case and doesn't cause concurrent access issues or "Database not initialized" errors.

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web-core/src/worker.rs : Keep worker.rs as the main worker entry point invoked by worker_main()

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 6
File: svelte-test/tests/database-functions/float-sum.test.ts:78-86
Timestamp: 2025-09-17T08:04:44.062Z
Learning: In svelte-test/tests/database-functions/float-sum.test.ts, the schema defines `amount TEXT NOT NULL` but the NULL value handling tests attempt to insert NULL values, creating a constraint violation that prevents the tests from passing.

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 15
File: svelte-test/tests/integration/error-handling.test.ts:418-434
Timestamp: 2025-09-17T17:44:54.855Z
Learning: In the sqlite-web test harness, parameter binding is not available for the db.query() method, so string interpolation with manual SQL escaping using value.replace(/'/g, "''") is the necessary approach for handling special characters in test data.

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 5
File: packages/sqlite-web-core/src/database_functions/bigint_sum.rs:76-93
Timestamp: 2025-08-27T05:55:40.481Z
Learning: In the sqlite-web-core codebase, for SQLite aggregate function context initialization, the maintainer prefers using byte-scanning to detect zero-initialized memory rather than using explicit initialization flags or Option wrappers.

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web-core/src/messages.rs : Use structured message types for worker communication as defined in messages.rs

Learnt from: findolor
Repo: rainlanguage/sqlite-web PR: 12
File: svelte-test/tests/fixtures/test-helpers.ts:11-11
Timestamp: 2025-09-15T10:01:17.743Z
Learning: In the sqlite-web project, test database names in createTestDatabase function do not need timestamp suffixes for uniqueness. The maintainer has confirmed that test isolation via unique naming is not required.

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to packages/sqlite-web/embedded_worker.js : Do not manually edit embedded_worker.js; it is generated by the build and should be treated as read-only

Learnt from: CR
Repo: rainlanguage/sqlite-web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Maintain self-contained workers with no external WASM file dependencies across the project

}
2 changes: 2 additions & 0 deletions packages/sqlite-web/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub enum SQLiteWasmDatabaseError {
InitializationPending,
#[error("Initialization failed: {0}")]
InitializationFailed(String),
#[error("OPFS deletion failed: {0}")]
OpfsDeletionFailed(String),
}

impl From<JsValue> for SQLiteWasmDatabaseError {
Expand Down
1 change: 1 addition & 0 deletions packages/sqlite-web/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod db;
mod errors;
mod messages;
mod opfs;
mod params;
mod ready;
mod utils;
Expand Down
Loading