Skip to content

Conversation

@findolor
Copy link
Collaborator

@findolor findolor commented Oct 20, 2025

Motivation

See issue: #18

Solution

Checks

By submitting this for review, I'm confirming I've done the following:

  • made this PR as small as possible
  • unit-tested any new functionality
  • linked any relevant issues or PRs
  • included screenshots (if this involves a front-end change)

Summary by CodeRabbit

  • New Features

    • Query API now accepts optional parameters end-to-end, with support for BigInt and binary/blob values and structured worker query results.
  • Bug Fixes / Reliability

    • Improved parameter validation/binding, clearer parameter-related errors, single-statement enforcement for parameterized queries, and more robust worker-side error reporting and timeouts.
  • Tests

    • Expanded integration and wasm tests covering positional/numbered params, types, edge cases, multi-statement and cross-worker routing.
  • Chores

    • Test fixtures updated and JS interop helpers added for param normalization and value handling; package artifact bumped.

@findolor findolor requested review from 0xgleb and hardyjosh October 20, 2025 12:04
@findolor findolor self-assigned this Oct 20, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 20, 2025

Walkthrough

Adds optional SQL query parameters end-to-end: JS-side param normalization (BigInt, binary→base64), marshaling of params in worker/channel messages, coordination changes to carry params, new exec_with_params with placeholder parsing and binding, utility helpers, tests, and a base64 crate dependency.

Changes

Cohort / File(s) Summary
Dependency & Packaging
packages/sqlite-web-core/Cargo.toml, svelte-test/package.json, pkg/package.json
Added base64 crate; bumped local npm tarball reference to alpha.6; added trailing newline to pkg/package.json.
Message Protocol
packages/sqlite-web-core/src/messages.rs
ChannelMessage::QueryRequest, WorkerMessage::ExecuteQuery, and MainThreadMessage::QueryResult now include params/request_id fields; serde skip/default used for optional params. Tests updated to match new fields.
Coordination Layer
packages/sqlite-web-core/src/coordination.rs
execute_query now accepts params: Option<Vec<serde_json::Value>>; setup_channel_listener and attempt_leadership return Result; message handling centralized and params propagated through leader/follower and timeout paths.
Database Parameter Binding
packages/sqlite-web-core/src/database.rs
Added exec_with_params(sql, params) with placeholder-mode detection (plain vs numbered), param parsing/validation, binders for NULL/BOOL/I64/F64/TEXT/BLOB/bigint-as-string, BoundBuffers/StmtGuard lifetime management, single-statement enforcement, deterministic row JSON output, and wasm tests.
Worker Layer
packages/sqlite-web-core/src/worker.rs
Centralized incoming message handling; structured query-result and worker-error messages; ExecuteQuery handling now runs state.execute_query(sql, params).await and reports errors via standardized messages.
Public JS API
packages/sqlite-web/src/lib.rs
query(&self, sql, params: Option<js_sys::Array>) introduced; param normalization helpers (BigInt, Uint8Array/ArrayBuffer→base64); worker creation/onmessage wiring refactored; pending_queries changed to HashMap<u32, (resolve, reject)> and next_request_id: Rc<RefCell<u32>> added.
Utilities
packages/sqlite-web-core/src/util.rs
Added set_js_property(target, key, value) -> Result<(), JsValue> and js_value_to_string(value) -> String with wasm tests.
DB Function & CString Safety
packages/sqlite-web-core/src/database_functions/*, packages/sqlite-web-core/src/database_functions/mod.rs
Replaced CString::new(...).unwrap() with error-checked construction; static zero-result bytes for bigint_sum; safer CString error handling in float_sum; tests gated to wasm target.
Tests & Test Helpers
svelte-test/tests/fixtures/test-helpers.ts, svelte-test/tests/integration/*.test.ts
Added comprehensive parameter-binding.test.ts; adjusted integer test to use Number.MAX_SAFE_INTEGER; extended cleanup tables; updated many tests for new params/request_id shapes.
Misc
packages/sqlite-web-core/src/lib.rs
Removed crate-level #[cfg(test)] wasm test module (deleted no-op test).

Sequence Diagram(s)

sequenceDiagram
    rect rgba(240,248,255,0.6)
    participant App as App / Test
    participant JS as JS API (query)
    participant Norm as normalize_params_js
    participant Worker as Worker (embedded)
    participant Coord as Coordination
    participant DB as SQLiteDatabase
    end

    App->>JS: query(sql, params?)
    JS->>Norm: normalize params (BigInt, Uint8Array/ArrayBuffer→base64)
    Norm-->>JS: Option<Array>

    alt Leader (in-process)
        JS->>Coord: execute_query(sql, params?)
        Coord->>DB: exec_with_params(sql, params?) or exec(sql)
        DB-->>Coord: result / error
        Coord-->>JS: return result / error
    else Follower (via Worker)
        JS->>Worker: post QueryRequest { query_id, sql, params? }
        Worker->>Coord: forward QueryRequest
        Coord->>DB: exec_with_params(sql, params?) or exec(sql)
        DB-->>Coord: QueryResponse
        Coord-->>Worker: QueryResponse
        Worker-->>JS: post query-result { requestId, result|null, error|null }
    end

    JS-->>App: resolve Promise / reject with error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus areas:
    • Placeholder parsing, numbered-index validation, and param-count enforcement in packages/sqlite-web-core/src/database.rs.
    • Lifetime management for BoundBuffers and StmtGuard.
    • JS param normalization, base64 encoding of binary, and consistent serialization/deserialization across worker/channel boundaries.
    • Error propagation and new Result-returning APIs in coordination/worker layers.

Possibly related PRs

Suggested reviewers

  • 0xgleb
  • hardyjosh

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.11% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Implement parameter binding for the query function" directly and accurately describes the primary objective of this changeset. The PR introduces comprehensive parameter binding support across multiple files: database.rs adds the exec_with_params method for parameter-bound SQL execution, coordination.rs and messages.rs propagate parameters through the system, lib.rs updates the query function signature to accept optional parameters, and a new integration test file validates parameter binding across various scenarios. The title is specific about what is being implemented (parameter binding) and where (the query function), avoiding vague terminology while remaining appropriately concise. A developer scanning the commit history would clearly understand this PR's purpose.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch parameter-binding

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eafbbd2 and 65c7b23.

⛔ Files ignored due to path filters (1)
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • packages/sqlite-web-core/src/messages.rs (9 hunks)
  • packages/sqlite-web-core/src/worker.rs (4 hunks)
  • packages/sqlite-web/src/lib.rs (6 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
packages/sqlite-web-core/src/worker.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Keep worker.rs as the main worker entry point invoked by worker_main()

Files:

  • packages/sqlite-web-core/src/worker.rs
packages/sqlite-web-core/src/messages.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Use structured message types for worker communication as defined in messages.rs

Files:

  • packages/sqlite-web-core/src/messages.rs
🧠 Learnings (4)
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
  • packages/sqlite-web-core/src/messages.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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()

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
🧬 Code graph analysis (2)
packages/sqlite-web-core/src/worker.rs (2)
packages/sqlite-web-core/src/util.rs (2)
  • js_value_to_string (39-60)
  • set_js_property (29-37)
packages/sqlite-web-core/src/coordination.rs (2)
  • new (44-78)
  • setup_channel_listener (80-97)
packages/sqlite-web/src/lib.rs (1)
packages/sqlite-web-core/src/coordination.rs (1)
  • new (44-78)
🔇 Additional comments (15)
packages/sqlite-web-core/src/worker.rs (5)

19-70: LGTM! Clean helper abstractions for worker messaging.

The error reporting and message building helpers follow a consistent pattern with proper error propagation. The requestId field in make_query_result_message enables correct correlation of responses to requests.


72-124: Message handling properly implements requestId correlation.

The centralized handle_incoming_value correctly deserializes WorkerMessage and passes through request_id for response correlation. The deserialization error path (lines 107-122) constructs a query-result without requestId—this is consistent with the maintainer's earlier assessment that such failures won't occur in normal operation since messages are constructed entirely in Rust.


127-160: LGTM! Proper error propagation in worker initialization.

The main function now correctly propagates errors from setup_channel_listener and handles leadership failures with appropriate error reporting via send_worker_error before throwing.


393-394: LGTM! Tests correctly updated for Result-returning signature.

Test assertions properly verify that setup_channel_listener() succeeds.


19-160: Proper error handling throughout non-test code.

All non-test code segments use Result types and the ? operator for error propagation. The .unwrap() calls are limited to test code (lines 162+) where they are acceptable. This addresses the earlier concern about unwraps in production code.

packages/sqlite-web-core/src/messages.rs (2)

18-20: LGTM! Message structures consistently extended for parameter binding and request correlation.

The additions are well-structured:

  • ChannelMessage::QueryRequest and WorkerMessage::ExecuteQuery properly handle optional params with #[serde(default)] and #[serde(skip_serializing_if)]
  • WorkerMessage::ExecuteQuery and MainThreadMessage::QueryResult include request_id for proper correlation
  • Field renaming to requestId maintains JavaScript naming conventions

Also applies to: 36-43, 52-53


100-100: LGTM! Tests comprehensively updated for new message fields.

All test cases properly initialize params: None where needed, correctly destructure the new ExecuteQuery struct variant, and verify requestId serialization in responses.

Also applies to: 132-148, 155-173, 192-192, 201-201

packages/sqlite-web/src/lib.rs (8)

16-28: LGTM! Worker creation properly cleans up blob URL.

The function correctly revokes the object URL regardless of whether Worker::new succeeds or fails, preventing URL leaks.


30-59: LGTM! Clean message handler installation and routing.

The handlers properly separate control messages (worker-ready, worker-error) from query results, enabling clear flow control.


61-100: LGTM! RequestId-based correlation correctly implemented.

The function properly looks up pending callbacks by requestId from the HashMap, addressing the out-of-order response issue that would occur with a FIFO queue. This implementation correctly handles concurrent queries.


102-166: LGTM! Comprehensive parameter normalization with proper type handling.

The encoding helpers correctly handle:

  • BigInt serialization to string representation
  • Binary data (Uint8Array/ArrayBuffer) to base64
  • Rejection of non-finite numbers (NaN/Infinity)
  • Consistent error messages using 1-based "position" terminology

171-172: LGTM! Struct properly updated for requestId correlation.

The change from Vec to HashMap<u32, ...> and addition of next_request_id enable correct request-response correlation for concurrent queries.


240-282: LGTM! Clean functional approach to parameter validation and normalization.

The iterator-based implementation using try_fold (lines 277-281) provides good readability and properly separates validation (ensure_array) from normalization (normalize_one_param). This addresses the refactoring suggestion from reviewer 0xgleb.


253-349: LGTM! Query method properly implements parameterized queries with requestId correlation.

Key strengths:

  • RequestId generation (lines 308-313) uses wrapping arithmetic with .max(1) to ensure non-zero IDs
  • Message construction propagates errors via ? operator
  • Params field is only included when non-empty (lines 326-330)
  • Promise callbacks are only stored in pending_queries after successful post_message (lines 336-344), preventing hung promises on post failures

470-684: LGTM! Comprehensive test coverage for parameter normalization.

The test suite thoroughly validates:

  • All supported parameter types (number, string, boolean, null, BigInt, Uint8Array, ArrayBuffer)
  • Sparse array holes mapping to NULL
  • Proper encoding of BigInt and binary data
  • Params field omission for None/empty arrays
  • Rejection of NaN and Infinity values

The postMessage spy approach (lines 470-518) enables validation of the actual messages sent to the worker.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sqlite-web-core/src/worker.rs (1)

32-36: Add "worker-ready" emission to worker initialization.

The worker never posts WorkerReady after setup, but the main thread explicitly waits for it at lib.rs:127. This initialization signal is missing. Add the proposed emission after onmessage.forget() at the end of worker.rs::main():

@@ pub fn main() -> Result<(), JsValue> {
     worker_scope.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
     onmessage.forget();
+
+    // Notify main thread that worker is ready
+    let ready = crate::messages::MainThreadMessage::WorkerReady;
+    let ready_js = serde_wasm_bindgen::to_value(&ready).unwrap();
+    let _ = worker_scope.post_message(&ready_js);
 
     Ok(())
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6286fc1 and 1a2d4f9.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • packages/sqlite-web-core/Cargo.toml (1 hunks)
  • packages/sqlite-web-core/src/coordination.rs (6 hunks)
  • packages/sqlite-web-core/src/database.rs (3 hunks)
  • packages/sqlite-web-core/src/messages.rs (7 hunks)
  • packages/sqlite-web-core/src/worker.rs (1 hunks)
  • packages/sqlite-web/src/lib.rs (4 hunks)
  • svelte-test/package.json (1 hunks)
  • svelte-test/tests/fixtures/test-helpers.ts (1 hunks)
  • svelte-test/tests/integration/database-basic.test.ts (2 hunks)
  • svelte-test/tests/integration/parameter-binding.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
packages/sqlite-web-core/src/messages.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Use structured message types for worker communication as defined in messages.rs

Files:

  • packages/sqlite-web-core/src/messages.rs
packages/sqlite-web-core/src/worker.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Keep worker.rs as the main worker entry point invoked by worker_main()

Files:

  • packages/sqlite-web-core/src/worker.rs
svelte-test/package.json

📄 CodeRabbit inference engine (CLAUDE.md)

In the Svelte test app, depend on the sqlite-web package via the locally packed tarball produced by npm pack

Files:

  • svelte-test/package.json
🧠 Learnings (6)
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/messages.rs
  • packages/sqlite-web-core/src/worker.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/Cargo.toml
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
  • packages/sqlite-web-core/src/worker.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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()

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-17T06:55:20.178Z
Learning: Applies to svelte-test/package.json : In the Svelte test app, depend on the sqlite-web package via the locally packed tarball produced by npm pack

Applied to files:

  • svelte-test/package.json
🧬 Code graph analysis (5)
packages/sqlite-web-core/src/coordination.rs (1)
packages/sqlite-web/src/lib.rs (1)
  • query (292-335)
packages/sqlite-web-core/src/database.rs (2)
packages/sqlite-web/src/lib.rs (4)
  • new (88-180)
  • v (204-204)
  • v (222-222)
  • v (239-239)
packages/sqlite-web-core/src/coordination.rs (1)
  • new (25-59)
svelte-test/tests/integration/parameter-binding.test.ts (1)
svelte-test/tests/fixtures/test-helpers.ts (2)
  • createTestDatabase (6-22)
  • cleanupDatabase (173-205)
packages/sqlite-web/src/lib.rs (1)
packages/sqlite-web-core/src/coordination.rs (1)
  • new (25-59)
packages/sqlite-web-core/src/worker.rs (1)
packages/sqlite-web/src/lib.rs (4)
  • v (204-204)
  • v (222-222)
  • v (239-239)
  • new (88-180)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (12)
svelte-test/tests/integration/database-basic.test.ts (1)

348-348: LGTM: Correct fix for JavaScript safe integer limits.

This change appropriately updates the test to use Number.MAX_SAFE_INTEGER (2^53 - 1 = 9007199254740991) instead of SQLite's maximum signed 64-bit integer (2^63 - 1 = 9223372036854775807). JavaScript uses IEEE 754 doubles for all numbers, so integers beyond MAX_SAFE_INTEGER lose precision and cannot be reliably represented. The inline comment enhances code clarity. Verification confirms other integer tests in the codebase (including parameter-binding.test.ts:218) also correctly use MAX_SAFE_INTEGER.

svelte-test/tests/fixtures/test-helpers.ts (1)

191-193: LGTM! Cleanup list properly extended for parameter binding tests.

All parameter binding test tables are correctly added to the cleanup list, ensuring proper test isolation. The table names match those created in the new parameter-binding.test.ts file.

svelte-test/package.json (1)

44-44: Version bump looks correct.

The dependency update from alpha.4 to alpha.6 aligns with the parameter binding feature additions in this PR and follows the established pattern for local tarball dependencies.

svelte-test/tests/integration/parameter-binding.test.ts (5)

1-81: Excellent test coverage for positional placeholders.

The setup and positional placeholder tests are well-structured:

  • Proper test isolation with beforeEach/afterEach
  • Comprehensive error handling that checks both exceptions and result.error
  • Good coverage of success and error cases

83-145: Numbered placeholder tests are comprehensive and well-structured.

Excellent coverage of:

  • Basic ?N binding and parameter reuse
  • Mixed form detection
  • Invalid indices (?0)
  • Extra parameter detection

Error handling is thorough and consistent throughout.

Also applies to: 150-181


183-286: Comprehensive type mapping coverage.

Excellent testing of:

  • Null, boolean, number, and string conversions
  • BigInt within valid i64 range and out-of-range rejection
  • Blob data preservation via Uint8Array
  • NaN/Infinity validation at normalization
  • Boolean false → 0 conversion

All type conversions are correctly validated.


288-336: Strong validation and security tests.

Excellent coverage of security concerns:

  • Named parameters properly rejected when params provided
  • Multi-statement SQL blocked with parameters (prevents batch execution risks)
  • SQL injection test confirms parameters are properly escaped/bound

The SQL injection test is particularly well-done, verifying both that the malicious string is stored literally and that the table still exists.


338-383: Excellent cross-worker communication tests.

The Follower/Leader routing tests properly verify:

  • Parameter propagation through BroadcastChannel between DB instances
  • Error propagation across workers with parameters
  • Proper cleanup in finally blocks for the second DB instance

This ensures the parameter binding feature works correctly in the distributed worker architecture.

packages/sqlite-web-core/src/database.rs (1)

264-271: Blob binding lifetime: OK, but call out safety assumptions.

Using None destructor maps to SQLITE_STATIC; BoundBuffers keeps inner Vec alive until finalize. This is safe given finalize occurs before dropping BoundBuffers.

Document this invariant in code to prevent future refactors from moving finalize past the guard’s scope.

packages/sqlite-web-core/src/coordination.rs (1)

72-90: Code correctly ensures params is always set in messages; verification confirms implementation matches requirement.

The main-thread API in packages/sqlite-web/src/lib.rs (lines 321-326) already implements the requested behavior:

  • When params has content: sets the field to the normalized array
  • When params is empty: explicitly sets the field to JsValue::UNDEFINED

This ensures the params field is always present in the message object sent to the worker, allowing the worker's parsing logic (which checks for is_undefined() || is_null()) to handle both cases correctly without ambiguity.

packages/sqlite-web-core/Cargo.toml (1)

22-22: Verification complete: base64 dependency correctly configured for WASM.

The workspace correctly pins base64 to version 0.21, ensuring reproducible builds. Both packages/sqlite-web/src/lib.rs and packages/sqlite-web-core/src/database.rs correctly import and use the base64 0.21 API pattern (base64::engine::general_purpose::STANDARD.encode() / .decode()), which matches the breaking API changes from version 0.21. No explicit features are configured on the dependency, leaving defaults active—appropriate for wasm32-unknown-unknown targets. No issues found.

packages/sqlite-web/src/lib.rs (1)

1-1: LGTM: correct trait import for base64 Engine.

Required for STANDARD.encode/encode_string usage.

Comment on lines 24 to 71
fn is_trivia_tail_only(tail: *const i8) -> bool {
if tail.is_null() {
return true;
}
// Safe because we created the input as a NUL-terminated CString and SQLite returns a pointer into it
let rest_c = unsafe { CStr::from_ptr(tail) };
let rest = rest_c.to_bytes();

// Simple scanner: skip whitespace and comments only
let mut i = 0usize;
while i < rest.len() {
match rest[i] {
b' ' | b'\t' | b'\r' | b'\n' => {
i += 1;
}
b'-' => {
if i + 1 < rest.len() && rest[i + 1] == b'-' {
// line comment -- ... until newline
i += 2;
while i < rest.len() && rest[i] != b'\n' {
i += 1;
}
} else {
return false;
}
}
b'/' => {
if i + 1 < rest.len() && rest[i + 1] == b'*' {
// block comment /* ... */
i += 2;
while i + 1 < rest.len() && !(rest[i] == b'*' && rest[i + 1] == b'/') {
i += 1;
}
if i + 1 < rest.len() {
i += 2; // consume */
} else {
// unterminated comment: treat as non-trivia to be safe
return false;
}
} else {
return false;
}
}
_ => return false,
}
}
true
}
Copy link

@coderabbitai coderabbitai bot Oct 20, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Tail scanner: works for whitespace/comments; add note on scope.

is_trivia_tail_only ignores quotes/strings, which is acceptable since tail begins after first complete statement. Add a brief comment clarifying assumption to avoid accidental reuse elsewhere.

🤖 Prompt for AI Agents
In packages/sqlite-web-core/src/database.rs around lines 24 to 71, the
is_trivia_tail_only function intentionally ignores quotes/strings because it
assumes the tail pointer starts immediately after the end of the first complete
SQL statement; add a brief clarifying comment at the top of this function
stating that the tail is guaranteed to begin after a complete statement (so
string/quote handling is out of scope) and warn that the function should not be
reused for arbitrary SQL fragments that may begin inside a quoted string. Ensure
the comment is concise and placed before the function signature so future
readers won't mistakenly reuse it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it's fine

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! I'll skip this suggestion.

Comment on lines +18 to 21
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
params: Option<Vec<serde_json::Value>>,
},
Copy link

@coderabbitai coderabbitai bot Oct 20, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Params field shape looks correct and backwards compatible.

  • ChannelMessage::QueryRequest has params: Option<Vec<serde_json::Value>> with default + skip_serializing_if: good.
  • WorkerMessage::ExecuteQuery switched to struct variant with optional params: good.
  • Tests cover absent params; consider adding a case with Some([...]) to assert roundtrip.

Add a test with parameters:

+    #[wasm_bindgen_test]
+    fn test_worker_message_with_params_roundtrip() {
+        let msg = WorkerMessage::ExecuteQuery {
+            sql: "SELECT ?1, ?2".into(),
+            params: Some(vec![serde_json::json!(1), serde_json::json!("a")]),
+        };
+        let json = serde_json::to_string(&msg).unwrap();
+        let back: WorkerMessage = serde_json::from_str(&json).unwrap();
+        match back {
+            WorkerMessage::ExecuteQuery { sql, params } => {
+                assert_eq!(sql, "SELECT ?1, ?2");
+                assert_eq!(params.unwrap().len(), 2);
+            }
+        }
+    }

As per coding guidelines.

Also applies to: 36-41, 93-101, 126-142, 177-191

🤖 Prompt for AI Agents
packages/sqlite-web-core/src/messages.rs around lines 18-21 (also apply similar
changes to ranges 36-41, 93-101, 126-142, 177-191): add unit tests that assert a
roundtrip (serialize -> deserialize) for message variants that include params =
Some(vec![...]) to ensure the optional params with serde
default/skip_serializing_if behave correctly; create test cases that construct
the relevant ChannelMessage::QueryRequest and WorkerMessage::ExecuteQuery (and
other affected variants), populate params with a small Vec<serde_json::Value>
(e.g., primitives and an object), serialize to JSON, deserialize back, and
assert equality of the params field and entire message structure.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not needed

Copy link

Choose a reason for hiding this comment

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

@findolor, understood. I've noted your decision.


🧠 Learnings used
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Comment on lines 78 to 113
match result {
Ok(res) => {
js_sys::Reflect::set(
&response,
&JsValue::from_str("result"),
&JsValue::from_str(&res),
)
.unwrap();
js_sys::Reflect::set(
&response,
&JsValue::from_str("error"),
&JsValue::NULL,
)
.unwrap();
}
Err(err) => {
js_sys::Reflect::set(
&response,
&JsValue::from_str("result"),
&JsValue::NULL,
)
.unwrap();
js_sys::Reflect::set(
&response,
&JsValue::from_str("error"),
&JsValue::from_str(&err),
)
.unwrap();
}
};

let global = js_sys::global();
let worker_scope: DedicatedWorkerGlobalScope =
global.unchecked_into();
let _ = worker_scope.post_message(&response);
});
Copy link

@coderabbitai coderabbitai bot Oct 20, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unify response serialization via MainThreadMessage and reduce duplication.

You manually construct JS objects for both success and error. Prefer sending MainThreadMessage::QueryResult via serde_wasm_bindgen to keep types centralized.

Minimal change:

- let response = js_sys::Object::new();
- js_sys::Reflect::set(&response, &JsValue::from_str("type"), &JsValue::from_str("query-result")).unwrap();
- match result {
-   Ok(res) => { /* set result/error fields */ }
-   Err(err) => { /* set result/error fields */ }
- }
- let _ = worker_scope.post_message(&response);
+ let response = match result {
+   Ok(res) => crate::messages::MainThreadMessage::QueryResult { result: Some(res), error: None },
+   Err(err) => crate::messages::MainThreadMessage::QueryResult { result: None, error: Some(err) },
+ };
+ let response_js = serde_wasm_bindgen::to_value(&response).unwrap();
+ let _ = worker_scope.post_message(&response_js);

As per coding guidelines.

Also applies to: 117-144

🤖 Prompt for AI Agents
In packages/sqlite-web-core/src/worker.rs around lines 78 to 113, you are
manually constructing a JS response object for success and error using
js_sys::Reflect; replace this with creating a MainThreadMessage::QueryResult
(populating its result or error fields accordingly) and serialize it with
serde_wasm_bindgen::to_value (or to_value(&message).unwrap()) then post that
Value via worker_scope.post_message; do the same refactor for the other
occurrence at lines ~117-144 so all responses use the centralized
MainThreadMessage enum and serde-based serialization instead of duplicated
js_sys::Reflect calls.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not really needed

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! I'll keep the current implementation as-is.


🧠 Learnings used
Learnt from: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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

Comment on lines +146 to +149
const check = (msg: string) => {
const regexOk = /(Missing parameter index \?1|Expected\s+2\s+parameters)/i.test(msg);
const mixingOk = msg.includes("Mixing '?' and '?N' placeholders is not supported.");
expect(regexOk || mixingOk).toBe(true);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the error acceptance logic for missing numbered indices.

The test accepts three different error messages for SELECT ?2 as b with [99]:

  1. "Missing parameter index ?1" ✓
  2. "Expected 2 parameters" ✓
  3. "Mixing '?' and '?N' placeholders" ❓

The third option doesn't make sense for this case since there's only ?2 (numbered form) with no ? (positional form). This overly permissive matching could hide implementation issues.

Is this intentional to accommodate different SQLite implementations, or should the error matching be tightened to only accept the first two cases?


🏁 Script executed:

#!/bin/bash
# Search for similar error matching patterns in other tests
rg -n -C3 "mixingOk|Mixing.*placeholders" --type=ts

Length of output: 1633


I need to examine the full test case context to understand what query and parameters are being used at lines 146-149.

I need to examine the full test case context around lines 146-149 to see what query and parameters are being tested.

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Extract full context of test case lines 115-160
sed -n '115,160p' svelte-test/tests/integration/parameter-binding.test.ts

Length of output: 1692


Tighten error matching to remove incorrect mixing placeholder check.

The test query SELECT ?2 as b with [99] uses only numbered placeholders—there is no mixing with positional placeholders. Accepting the "Mixing '?' and '?N' placeholders" error here is overly permissive and could mask bugs.

The earlier test at line 104 correctly expects this mixing error for a query that actually mixes both forms (SELECT ?1, ?2 as b WHERE id = ?). Remove the mixingOk check from lines 148-149 and keep only the regex validation for "Missing parameter index ?1" or "Expected 2 parameters".

🤖 Prompt for AI Agents
In svelte-test/tests/integration/parameter-binding.test.ts around lines 146 to
149, the test currently allows a "Mixing '?' and '?N' placeholders" error for
the query `SELECT ?2 as b` with `[99]`, which is incorrect; remove the mixingOk
check and only assert the regex that matches "Missing parameter index ?1" or
"Expected 2 parameters" so the test accepts only those errors for
numbered-placeholder-only queries.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (4)
packages/sqlite-web/src/lib.rs (3)

436-449: Add comprehensive test coverage for parameter binding types and edge cases.

The current test only covers the None params case. The normalize_params_js function implements complex type conversion logic (null, undefined, numbers, booleans, strings, BigInt, Uint8Array, ArrayBuffer) that should be thoroughly tested.

Add wasm_bindgen tests that cover:

  1. Type coverage: numeric, string, boolean, null, undefined (mapped to NULL), BigInt, Uint8Array, ArrayBuffer
  2. Edge cases:
    • Array with holes/undefined elements (should map to SQL NULL)
    • NaN and Infinity (should error)
    • Empty array vs None (verify message structure)
    • Mixed type arrays
  3. Error cases:
    • Invalid types
    • Non-array params
    • Unsupported parameter types

Example test structure:

#[wasm_bindgen_test]
async fn test_query_with_various_param_types() {
    if let Ok(db) = SQLiteWasmDatabase::new("testdb") {
        let params = js_sys::Array::new();
        params.push(&JsValue::from_f64(42.0));
        params.push(&JsValue::from_str("text"));
        params.push(&JsValue::from_bool(true));
        params.push(&JsValue::NULL);
        
        let result = db.query("SELECT ?, ?, ?, ?", Some(params)).await;
        // Assert based on expected behavior
    }
}

#[wasm_bindgen_test]
async fn test_query_with_bigint() {
    if let Ok(db) = SQLiteWasmDatabase::new("testdb") {
        let params = js_sys::Array::new();
        let bigint = js_sys::BigInt::new(&JsValue::from_str("9007199254740991")).unwrap();
        params.push(&bigint);
        
        let result = db.query("SELECT ?", Some(params)).await;
        // Assert
    }
}

#[wasm_bindgen_test]
async fn test_query_with_blob() {
    if let Ok(db) = SQLiteWasmDatabase::new("testdb") {
        let params = js_sys::Array::new();
        let bytes = js_sys::Uint8Array::new_with_length(4);
        bytes.copy_from(&[1, 2, 3, 4]);
        params.push(&bytes);
        
        let result = db.query("SELECT ?", Some(params)).await;
        // Assert
    }
}

327-347: Omit the params field entirely when empty instead of setting to undefined.

When params_array is empty, the code explicitly sets the params field to JsValue::UNDEFINED (lines 337-346). This is unnecessary—the more idiomatic approach is to omit the field entirely when there's no data. The worker-side code already handles missing fields correctly.

Apply this diff:

             if params_array.length() > 0 {
                 let params_js = JsValue::from(params_array.clone());
                 if let Err(e) =
                     js_sys::Reflect::set(&message, &JsValue::from_str("params"), &params_js)
                 {
                     let err_js: JsValue = SQLiteWasmDatabaseError::from(e).into();
                     let _ = reject.call1(&JsValue::NULL, &err_js);
                     return;
                 }
-            } else {
-                // Explicitly set undefined for clarity
-                if let Err(e) = js_sys::Reflect::set(
-                    &message,
-                    &JsValue::from_str("params"),
-                    &JsValue::UNDEFINED,
-                ) {
-                    let err_js: JsValue = SQLiteWasmDatabaseError::from(e).into();
-                    let _ = reject.call1(&JsValue::NULL, &err_js);
-                    return;
-                }
             }

224-257: Optimize blob encoding with encode_string and eliminate duplication.

The current implementation has two issues:

  1. Allocation inefficiency: Using .encode() allocates a new String on each call. The encode_string() method appends to an existing String, reducing heap allocations—especially beneficial when processing multiple blob parameters.

  2. Code duplication: The ArrayBuffer branch (lines 241-257) converts to Uint8Array then duplicates the exact same encoding logic as the Uint8Array branch (lines 224-239).

Apply this diff to optimize and deduplicate:

             // Uint8Array
             if let Ok(typed) = v.clone().dyn_into::<js_sys::Uint8Array>() {
-                let rust_vec = typed.to_vec();
-                let b64 = base64::engine::general_purpose::STANDARD.encode(rust_vec);
+                let bytes = typed.to_vec();
+                let mut b64 = String::new();
+                base64::engine::general_purpose::STANDARD.encode_string(&bytes, &mut b64);
                 let obj = js_sys::Object::new();
                 js_sys::Reflect::set(
                     &obj,
                     &JsValue::from_str("__type"),
                     &JsValue::from_str("blob"),
                 )
                 .map_err(SQLiteWasmDatabaseError::from)?;
                 js_sys::Reflect::set(&obj, &JsValue::from_str("base64"), &JsValue::from_str(&b64))
                     .map_err(SQLiteWasmDatabaseError::from)?;
                 normalized.push(&obj);
                 continue;
             }
 
             // ArrayBuffer
             if let Ok(buf) = v.clone().dyn_into::<js_sys::ArrayBuffer>() {
-                let typed = js_sys::Uint8Array::new(&buf);
-                let rust_vec = typed.to_vec();
-                let b64 = base64::engine::general_purpose::STANDARD.encode(rust_vec);
-                let obj = js_sys::Object::new();
-                js_sys::Reflect::set(
-                    &obj,
-                    &JsValue::from_str("__type"),
-                    &JsValue::from_str("blob"),
-                )
-                .map_err(SQLiteWasmDatabaseError::from)?;
-                js_sys::Reflect::set(&obj, &JsValue::from_str("base64"), &JsValue::from_str(&b64))
-                    .map_err(SQLiteWasmDatabaseError::from)?;
-                normalized.push(&obj);
-                continue;
+                // Convert to Uint8Array and fall through to the branch above
+                let typed = js_sys::Uint8Array::new(&buf);
+                let v_as_typed: JsValue = typed.into();
+                // Retry with Uint8Array
+                if let Ok(typed) = v_as_typed.dyn_into::<js_sys::Uint8Array>() {
+                    let bytes = typed.to_vec();
+                    let mut b64 = String::new();
+                    base64::engine::general_purpose::STANDARD.encode_string(&bytes, &mut b64);
+                    let obj = js_sys::Object::new();
+                    js_sys::Reflect::set(
+                        &obj,
+                        &JsValue::from_str("__type"),
+                        &JsValue::from_str("blob"),
+                    )
+                    .map_err(SQLiteWasmDatabaseError::from)?;
+                    js_sys::Reflect::set(&obj, &JsValue::from_str("base64"), &JsValue::from_str(&b64))
+                        .map_err(SQLiteWasmDatabaseError::from)?;
+                    normalized.push(&obj);
+                    continue;
+                }
             }

Alternatively, extract the blob encoding logic into a helper function to fully eliminate duplication.

Based on learnings.

packages/sqlite-web-core/src/worker.rs (1)

49-92: Unify response serialization via MainThreadMessage to reduce duplication.

Manual Reflect sets are repeated and brittle. Serialize MainThreadMessage::QueryResult via serde_wasm_bindgen to keep types centralized.

-                            // Send response as plain JavaScript object
-                            let response = js_sys::Object::new();
-                            js_sys::Reflect::set(&response, &JsValue::from_str("type"), &JsValue::from_str("query-result")).unwrap();
-                            match result {
-                                Ok(res) => {
-                                    js_sys::Reflect::set(&response, &JsValue::from_str("result"), &JsValue::from_str(&res)).unwrap();
-                                    js_sys::Reflect::set(&response, &JsValue::from_str("error"), &JsValue::NULL).unwrap();
-                                }
-                                Err(err) => {
-                                    js_sys::Reflect::set(&response, &JsValue::from_str("result"), &JsValue::NULL).unwrap();
-                                    js_sys::Reflect::set(&response, &JsValue::from_str("error"), &JsValue::from_str(&err)).unwrap();
-                                }
-                            };
-                            let global = js_sys::global();
-                            let worker_scope: DedicatedWorkerGlobalScope = global.unchecked_into();
-                            let _ = worker_scope.post_message(&response);
+                            let msg = match result {
+                                Ok(res) => crate::messages::MainThreadMessage::QueryResult { result: Some(res), error: None },
+                                Err(err) => crate::messages::MainThreadMessage::QueryResult { result: None, error: Some(err) },
+                            };
+                            let js = serde_wasm_bindgen::to_value(&msg).unwrap();
+                            let scope: DedicatedWorkerGlobalScope = js_sys::global().unchecked_into();
+                            let _ = scope.post_message(&js);

Additional import (outside this hunk):

use crate::messages::MainThreadMessage;

As per coding guidelines (structured message types).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a2d4f9 and b189d4a.

⛔ Files ignored due to path filters (1)
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • packages/sqlite-web-core/src/database.rs (5 hunks)
  • packages/sqlite-web-core/src/worker.rs (2 hunks)
  • packages/sqlite-web/src/lib.rs (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
packages/sqlite-web-core/src/worker.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Keep worker.rs as the main worker entry point invoked by worker_main()

Files:

  • packages/sqlite-web-core/src/worker.rs
🧠 Learnings (6)
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
  • packages/sqlite-web-core/src/worker.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-10-20T12:24:07.846Z
Learnt from: findolor
PR: rainlanguage/sqlite-web#19
File: packages/sqlite-web-core/src/database.rs:122-157
Timestamp: 2025-10-20T12:24:07.846Z
Learning: In packages/sqlite-web-core/src/database.rs, named parameters (:name, name, $name) are intentionally not supported for SQL parameter binding. Only plain '?' and numbered '?N' placeholders are supported.

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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()

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
🧬 Code graph analysis (2)
packages/sqlite-web-core/src/database.rs (1)
packages/sqlite-web/src/lib.rs (4)
  • new (88-180)
  • v (206-206)
  • v (225-225)
  • v (242-242)
packages/sqlite-web-core/src/worker.rs (2)
packages/sqlite-web/src/lib.rs (1)
  • new (88-180)
packages/sqlite-web-core/src/coordination.rs (1)
  • new (25-59)
🔇 Additional comments (11)
packages/sqlite-web/src/lib.rs (2)

1-1: LGTM: Base64 Engine trait import is correct.

The base64::Engine trait import is necessary for calling encoding methods on the STANDARD engine used in parameter normalization.


309-350: Good: Error handling prevents panics and ensures callback consistency.

The replacement of .unwrap() calls with explicit error handling (lines 309-346) correctly prevents WASM panics. The pattern of checking each Reflect::set() result and calling reject on error is appropriate within the Promise constructor closure context where ? operator cannot be used.

Pushing callbacks to the queue only after successful message construction (line 350) ensures the callback queue stays consistent with messages actually sent to the worker.

packages/sqlite-web-core/src/database.rs (7)

3-3: LGTM: Imports are correctly used.

The base64::Engine trait and c_void type are properly utilized in the parameter binding implementation.

Also applies to: 6-6


18-46: Excellent RAII pattern fixes statement leak.

The StmtGuard ensures SQLite statements are finalized on all paths, including early returns from bind errors. The take() method correctly transfers ownership when execution succeeds. This addresses the statement leak flagged in the previous review.


49-96: LGTM: Trivia scanner is correct for its context.

The whitespace and comment scanner correctly validates that the tail contains no executable SQL. Unterminated comments safely return false.


276-328: LGTM: Extended type binding is correct.

Base64 blob decoding uses the standard engine (per learnings), and bigint values are correctly limited to the i64 range with clear error messages. The 1-based index in error messages aids debugging.

Based on learnings


364-426: LGTM: Parameterized execution is safe and correct.

The StmtGuard ensures finalization on all paths, including bind errors. The single-statement validation prevents ambiguity with multiple statements. Parameter count validation provides clear error messages.


738-757: LGTM: Public API is clean and consistent.

The API mirrors the existing exec() method, maintaining consistency. Transaction state tracking via sqlite3_get_autocommit is correct.


1002-1158: Excellent test coverage for parameter binding.

The integration tests cover the scenarios requested in the previous review:

  • Positional placeholders with multiple types (null, bool, int, float, text)
  • Numbered placeholders including duplicate indices and gap detection
  • Blob via base64 object and bigint via string
packages/sqlite-web-core/src/worker.rs (2)

9-9: Import looks right.

Brings in the canonical WorkerMessage type for typed deserialization.


41-47: Review comment is incorrect. Current match statement is already exhaustive and properly future-proofed.

The WorkerMessage enum contains only one variant (ExecuteQuery), making the current match exhaustive. With a single-variant enum, Rust's compiler requires all variants to be matched—the current code satisfies this requirement without a catch-all. The Err arm is already present and properly handles deserialization errors.

Adding an Ok(_) catch-all would actually violate Rust's exhaustiveness checking principle. When a new WorkerMessage variant is added later, the compiler will enforce that this match statement be updated. With a catch-all, new variants would silently be ignored—creating maintenance issues rather than preventing them. The current implementation correctly leverages Rust's type system to catch missing handlers at compile time.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sqlite-web/src/lib.rs (1)

120-171: Out-of-order resolution risk: use requestId + map; also handle postMessage/worker-error.

Current flow pairs requests/responses via LIFO pop without IDs; concurrent queries can resolve the wrong Promise. Also, worker-error messages are ignored and postMessage errors aren’t rejected.

Prefer correlating by a monotonically increasing requestId and storing callbacks in a HashMap. Reject on worker-error or send failure.

Option A (recommended): requestId correlation and robust error handling.

--- a/packages/sqlite-web/src/lib.rs
+++ b/packages/sqlite-web/src/lib.rs
@@
-use std::cell::RefCell;
+use std::cell::{Cell, RefCell};
+use std::collections::HashMap;
@@
 pub struct SQLiteWasmDatabase {
     worker: Worker,
-    pending_queries: Rc<RefCell<Vec<(js_sys::Function, js_sys::Function)>>>,
+    pending_queries: Rc<RefCell<HashMap<u32, (js_sys::Function, js_sys::Function)>>>,
+    next_request_id: Rc<Cell<u32>>,
 }
@@
-        let pending_queries: Rc<RefCell<Vec<(js_sys::Function, js_sys::Function)>>> =
-            Rc::new(RefCell::new(Vec::new()));
+        let pending_queries: Rc<RefCell<HashMap<u32, (js_sys::Function, js_sys::Function)>>> =
+            Rc::new(RefCell::new(HashMap::new()));
         let pending_queries_clone = Rc::clone(&pending_queries);
+        let next_request_id = Rc::new(Cell::new(1u32));
+        let next_request_id_clone = Rc::clone(&next_request_id);
@@
-            // Handle worker ready message
+            // Handle worker ready / error
             if let Ok(obj) = js_sys::Reflect::get(&data, &JsValue::from_str("type")) {
                 if let Some(msg_type) = obj.as_string() {
                     if msg_type == "worker-ready" {
                         return;
                     } else if msg_type == "worker-error" {
-                        if let Ok(_error) = js_sys::Reflect::get(&data, &JsValue::from_str("error"))
-                        {
-                        }
+                        let _ = if let Ok(id_val) = js_sys::Reflect::get(&data, &JsValue::from_str("requestId")) {
+                            if let Some(id) = id_val.as_f64() {
+                                pending_queries_clone
+                                    .borrow_mut()
+                                    .remove(&(id as u32))
+                                    .and_then(|(_, reject)| {
+                                        let err = js_sys::Reflect::get(&data, &JsValue::from_str("error")).ok()
+                                            .and_then(|v| v.as_string())
+                                            .unwrap_or_else(|| "Worker error".to_string());
+                                        reject.call1(&JsValue::NULL, &JsValue::from_str(&err)).ok()
+                                    });
+                            }
+                        };
                         return;
                     }
                 }
             }
@@
-                    if msg_type == "query-result" {
-                        if let Some((resolve, reject)) = pending_queries_clone.borrow_mut().pop() {
+                    if msg_type == "query-result" {
+                        let id_val = js_sys::Reflect::get(&data, &JsValue::from_str("requestId")).ok();
+                        if let Some(id_js) = id_val {
+                            if let Some((resolve, reject)) = id_js.as_f64()
+                                .and_then(|id| pending_queries_clone.borrow_mut().remove(&(id as u32))) {
                             // Check for error first
@@
-                        }
+                            }
+                        }
                     }
                 }
             }
@@
-        Ok(SQLiteWasmDatabase {
-            worker,
-            pending_queries,
-        })
+        Ok(SQLiteWasmDatabase { worker, pending_queries, next_request_id })
     }
@@
     pub async fn query(
         &self,
         sql: &str,
         params: Option<js_sys::Array>,
     ) -> Result<String, SQLiteWasmDatabaseError> {
@@
-        // Create the Promise that will resolve/reject when the worker responds
-        let promise = js_sys::Promise::new(&mut |resolve, reject| {
-            // Only store callbacks after successful message construction
-            pending_queries.borrow_mut().push((resolve, reject));
-
-            let _ = worker.post_message(&message);
-        });
+        // Create the Promise that will resolve/reject when the worker responds
+        let promise = js_sys::Promise::new(&mut |resolve, reject| {
+            // Assign requestId
+            let id = self.next_request_id.get();
+            self.next_request_id.set(id.wrapping_add(1));
+            let _ = js_sys::Reflect::set(&message, &JsValue::from_str("requestId"), &JsValue::from_f64(id as f64));
+            // Track resolver/rejector
+            pending_queries.borrow_mut().insert(id, (resolve.clone(), reject.clone()));
+            // Send and handle immediate failure
+            match worker.post_message(&message) {
+                Ok(()) => {}
+                Err(e) => {
+                    pending_queries.borrow_mut().remove(&id);
+                    let _ = reject.call1(&JsValue::NULL, &e);
+                }
+            }
+        });

Option B (minimal): switch to FIFO (VecDeque, pop_front) to maintain order if worker guarantees ordered processing.

Also applies to: 326-333

♻️ Duplicate comments (1)
packages/sqlite-web-core/src/database.rs (1)

242-260: Handle u64 JSON numbers with range checks.

serde_json::Number can be u64. Add an as_u64 branch; bind as int64 if <= i64::MAX, otherwise error clearly.

-                serde_json::Value::Number(num) => {
-                    if let Some(v) = num.as_i64() {
+                serde_json::Value::Number(num) => {
+                    if let Some(v) = num.as_i64() {
                         let rc = unsafe { sqlite3_bind_int64(stmt, i, v) };
@@
-                    } else if let Some(v) = num.as_f64() {
+                    } else if let Some(u) = num.as_u64() {
+                        if u <= i64::MAX as u64 {
+                            let rc = unsafe { sqlite3_bind_int64(stmt, i, u as i64) };
+                            if rc != SQLITE_OK {
+                                return Err(format!("Failed to bind int64 at {i}"));
+                            }
+                        } else {
+                            return Err(format!(
+                                "Unsigned integer at index {} exceeds i64 range.",
+                                target_index + 1
+                            ));
+                        }
+                    } else if let Some(v) = num.as_f64() {
                         // serde_json never represents NaN/Infinity
                         let rc = unsafe { sqlite3_bind_double(stmt, i, v) };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b189d4a and 528d9e8.

⛔ Files ignored due to path filters (1)
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • packages/sqlite-web-core/src/database.rs (5 hunks)
  • packages/sqlite-web/src/lib.rs (5 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-10-20T12:24:07.846Z
Learnt from: findolor
PR: rainlanguage/sqlite-web#19
File: packages/sqlite-web-core/src/database.rs:122-157
Timestamp: 2025-10-20T12:24:07.846Z
Learning: In packages/sqlite-web-core/src/database.rs, named parameters (:name, name, $name) are intentionally not supported for SQL parameter binding. Only plain '?' and numbered '?N' placeholders are supported.

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
🧬 Code graph analysis (2)
packages/sqlite-web/src/lib.rs (1)
packages/sqlite-web-core/src/coordination.rs (1)
  • new (25-59)
packages/sqlite-web-core/src/database.rs (1)
packages/sqlite-web/src/lib.rs (4)
  • new (88-180)
  • v (206-206)
  • v (225-225)
  • v (242-242)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (1)
packages/sqlite-web-core/src/database.rs (1)

407-431: Good: RAII ensures stmt finalization on all paths.

StmtGuard + BoundBuffers lifetime correctly prevent leaks on bind/exec errors. No action needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 528d9e8 and 6f31313.

⛔ Files ignored due to path filters (1)
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • packages/sqlite-web/src/lib.rs (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (5)
packages/sqlite-web/src/lib.rs (5)

1-1: LGTM: base64::Engine import added.

Required for the parameter normalization that encodes binary data (Uint8Array/ArrayBuffer) as base64 strings for transmission to the worker.


182-289: Strong implementation with comprehensive type handling.

The normalization correctly handles:

  • Sparse array holes via arr.get(i) returning undefined
  • All major JS types (primitives, BigInt, binary data)
  • Proper error propagation throughout (past review concern addressed)
  • NaN/Infinity rejection for numeric values

291-336: Well-structured query implementation with proper parameter handling.

Key improvements:

  • Optional parameters maintain backward compatibility
  • Params field only included in message when non-empty (past review addressed)
  • Consistent error propagation via map_err throughout message construction
  • Clear documentation of parameter handling behavior

457-505: Clever test helpers for message inspection.

The spy pattern allows validation of message construction without requiring a fully functional worker. Proper cleanup via uninstall_post_message_spy() and defensive try-catch for missing Worker API make this robust for the test environment.


507-671: Excellent test coverage addressing all past review suggestions.

The tests comprehensively verify:

  • All supported parameter types (primitives, BigInt, binary data)
  • Sparse array holes normalize to SQL NULL (index 4 in first test)
  • Message params field presence/absence behavior
  • NaN/Infinity rejection for numeric values

The sparse array test construction (lines 522-537) correctly creates a hole at index 4 by pushing indices 0-3, then using Reflect::set for index 5, leaving 4 undefined.

true
}

fn bind_params_for_stmt(
Copy link

Choose a reason for hiding this comment

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

this function is seven screens long. can we, please, refactor it? Also, I reckon a lot of the mutability and looping can be replaced with functional-style iterator method chaining, which could be easier to read/comprehend, so consider using that as well

Copy link

Choose a reason for hiding this comment

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

is_trivia_tail_only is not quite seven screens long but can probably be refactored to be easier to comprehend as well

match db_opt {
Some(mut database) => {
let result = database.exec(&sql).await;
let result = match params {
Copy link

Choose a reason for hiding this comment

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

this is a lot of levels of nesting. consider refactoring to reduce

let _ = worker_scope.post_message(&response);
});
// Deserialize into canonical WorkerMessage and handle variants
match serde_wasm_bindgen::from_value::<WorkerMessage>(data) {
Copy link

Choose a reason for hiding this comment

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

i see multiple .unwrap's in here. this isn't test code, is it?

@findolor findolor requested a review from 0xgleb October 23, 2025 13:13
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sqlite-web-core/src/database.rs (1)

1-1842: Consider file refactoring to address length concerns.

The parameter binding implementation is functionally correct and well-tested. However, past reviews noted concerns about file length ("seven screens long") and suggested refactoring with functional-style iterator chaining where appropriate. The file has grown to 1842 lines. Consider splitting into separate modules:

  • database/core.rs - core database operations
  • database/params.rs - parameter parsing and binding
  • database/placeholders.rs - placeholder detection and validation
  • database/execution.rs - statement execution logic

This would improve maintainability without changing functionality.

Based on learnings.

♻️ Duplicate comments (2)
packages/sqlite-web-core/src/messages.rs (1)

62-101: Add a test with params = Some([...]) to assert roundtrip.

Covers the positive path for optional params serialization.

+    #[wasm_bindgen_test]
+    fn test_query_request_with_params_roundtrip() {
+        let msg = ChannelMessage::QueryRequest {
+            query_id: "q1".into(),
+            sql: "SELECT ?1, ?2".into(),
+            params: Some(vec![serde_json::json!(1), serde_json::json!("a")]),
+        };
+        assert_serialization_roundtrip(msg, "query-request", |json| {
+            assert!(json.contains("\"params\":[1,\"a\"]"));
+        });
+    }
+
+    #[wasm_bindgen_test]
+    fn test_worker_execute_query_with_params_roundtrip() {
+        let msg = WorkerMessage::ExecuteQuery {
+            sql: "SELECT ?".into(),
+            params: Some(vec![serde_json::json!({"k": "v"})]),
+        };
+        let s = serde_json::to_string(&msg).unwrap();
+        let back: WorkerMessage = serde_json::from_str(&s).unwrap();
+        assert_eq!(msg, back);
+    }

Also applies to: 125-142, 168-191

packages/sqlite-web-core/src/worker.rs (1)

98-115: Refactor error classification: emit worker-error, not query-result, for malformed inbound messages.

The concern is valid. Deserialization failures occur before any user query is associated, so they should not emit query-result messages. The current code causes the main thread to pop from pending_queries (line 66-67 in lib.rs) for an error unrelated to any pending query, creating a queue mismatch where the wrong resolver receives the error and actual pending queries hang.

The proposed refactor is correct:

  • send_worker_error_message is available (defined at worker.rs:19) and already used throughout
  • Main thread correctly ignores worker-error without queue interaction (lib.rs:51)
  • The simplified return avoids the fallback chain

Apply the suggested diff:

-        Err(err) => {
-            let error_text = format!("Invalid worker message: {err:?}");
-            let response = make_query_result_message(Err(error_text)).unwrap_or_else(|e| {
-                let _ = send_worker_error(e);
-                let o = js_sys::Object::new();
-                let _ = set_js_property(&o, "type", &JsValue::from_str("query-result"));
-                let _ = set_js_property(&o, "result", &JsValue::NULL);
-                let _ =
-                    set_js_property(&o, "error", &JsValue::from_str("Failed to build response"));
-                o
-            });
-            if let Err(err) = post_message(&response) {
-                if let Err(send_err) = send_worker_error(err) {
-                    throw_val(send_err);
-                }
-            }
-        }
+        Err(err) => {
+            let _ = send_worker_error_message(&format!("Invalid worker message: {err:?}"));
+            return;
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4008dd and c7624b5.

⛔ Files ignored due to path filters (1)
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • packages/sqlite-web-core/src/coordination.rs (12 hunks)
  • packages/sqlite-web-core/src/database.rs (11 hunks)
  • packages/sqlite-web-core/src/database_functions/bigint_sum.rs (3 hunks)
  • packages/sqlite-web-core/src/database_functions/float_is_zero.rs (1 hunks)
  • packages/sqlite-web-core/src/database_functions/float_negate.rs (1 hunks)
  • packages/sqlite-web-core/src/database_functions/float_sum.rs (3 hunks)
  • packages/sqlite-web-core/src/database_functions/mod.rs (6 hunks)
  • packages/sqlite-web-core/src/lib.rs (0 hunks)
  • packages/sqlite-web-core/src/messages.rs (8 hunks)
  • packages/sqlite-web-core/src/util.rs (2 hunks)
  • packages/sqlite-web-core/src/worker.rs (4 hunks)
  • packages/sqlite-web/src/lib.rs (6 hunks)
💤 Files with no reviewable changes (1)
  • packages/sqlite-web-core/src/lib.rs
🧰 Additional context used
📓 Path-based instructions (2)
packages/sqlite-web-core/src/worker.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Keep worker.rs as the main worker entry point invoked by worker_main()

Files:

  • packages/sqlite-web-core/src/worker.rs
packages/sqlite-web-core/src/messages.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Use structured message types for worker communication as defined in messages.rs

Files:

  • packages/sqlite-web-core/src/messages.rs
🧠 Learnings (8)
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
  • packages/sqlite-web-core/src/messages.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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()

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
  • packages/sqlite-web-core/src/coordination.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
  • packages/sqlite-web-core/src/coordination.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database_functions/mod.rs
  • packages/sqlite-web-core/src/database_functions/float_negate.rs
  • packages/sqlite-web-core/src/database.rs
  • packages/sqlite-web-core/src/database_functions/float_is_zero.rs
  • packages/sqlite-web-core/src/database_functions/float_sum.rs
📚 Learning: 2025-09-19T07:06:37.309Z
Learnt from: findolor
PR: rainlanguage/sqlite-web#14
File: svelte-test/tests/database-functions/float-is-zero.test.ts:60-90
Timestamp: 2025-09-19T07:06:37.309Z
Learning: For floating-point database functions in the rainlanguage/sqlite-web codebase, negative zero handling is not required and doesn't need to be tested.

Applied to files:

  • packages/sqlite-web-core/src/database_functions/float_negate.rs
  • packages/sqlite-web-core/src/database_functions/float_is_zero.rs
📚 Learning: 2025-10-20T12:24:07.874Z
Learnt from: findolor
PR: rainlanguage/sqlite-web#19
File: packages/sqlite-web-core/src/database.rs:122-157
Timestamp: 2025-10-20T12:24:07.874Z
Learning: In packages/sqlite-web-core/src/database.rs, named parameters (:name, name, $name) are intentionally not supported for SQL parameter binding. Only plain '?' and numbered '?N' placeholders are supported.

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
  • packages/sqlite-web-core/src/coordination.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-08-27T05:55:40.481Z
Learnt from: findolor
PR: rainlanguage/sqlite-web#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.

Applied to files:

  • packages/sqlite-web-core/src/database_functions/bigint_sum.rs
  • packages/sqlite-web-core/src/database_functions/float_sum.rs
🧬 Code graph analysis (3)
packages/sqlite-web-core/src/worker.rs (2)
packages/sqlite-web-core/src/util.rs (2)
  • js_value_to_string (39-50)
  • set_js_property (29-37)
packages/sqlite-web-core/src/coordination.rs (2)
  • new (44-78)
  • setup_channel_listener (80-97)
packages/sqlite-web-core/src/database_functions/float_sum.rs (1)
packages/sqlite-web-core/src/database_functions/bigint_sum.rs (5)
  • new (15-17)
  • std (81-81)
  • std (99-99)
  • std (128-131)
  • std (153-156)
packages/sqlite-web-core/src/coordination.rs (2)
packages/sqlite-web-core/src/util.rs (4)
  • js_value_to_string (39-50)
  • sanitize_identifier (5-19)
  • set_js_property (29-37)
  • value (43-43)
packages/sqlite-web/src/lib.rs (5)
  • new (226-244)
  • from (202-204)
  • from (208-210)
  • from (214-219)
  • query (276-317)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (33)
packages/sqlite-web-core/src/database_functions/float_negate.rs (1)

97-97: LGTM! Test configuration correctly aligned with wasm-bindgen-test framework.

The change to #[cfg(all(test, target_family = "wasm"))] appropriately restricts test compilation to WebAssembly targets, which matches the test framework being used (wasm_bindgen_test). This prevents build errors on non-WASM targets where wasm-bindgen-test dependencies wouldn't be available.

packages/sqlite-web/src/lib.rs (4)

120-151: LGTM: Comprehensive parameter type handling.

The function correctly handles all JavaScript parameter types including null/undefined, BigInt, binary data (Uint8Array/ArrayBuffer), primitives (number/boolean/string), with proper validation for non-finite numbers and clear error messages.


90-118: LGTM: Clean encoding helpers with proper error handling.

Both encode_bigint_to_obj and encode_binary_to_obj correctly serialize JavaScript values with appropriate error propagation using map_err.


488-652: LGTM: Comprehensive test coverage for parameter binding.

The test suite thoroughly covers:

  • All parameter types (primitives, BigInt, binary data)
  • Edge cases (sparse arrays with holes, NaN/Infinity rejection)
  • Message format validation (params presence/absence)
  • Proper assertions on normalized output

The post-message spy mechanism is a clever approach to validate worker message construction without requiring a full worker implementation.


15-44: LGTM: Clean worker creation and message handler setup.

The create_worker_from_code function properly creates a Worker from embedded code using the Blob API and correctly revokes the object URL to prevent memory leaks. The install_onmessage_handler appropriately delegates to control and result handlers.

packages/sqlite-web-core/src/util.rs (1)

29-37: LGTM: robust wrapper over Reflect::set.

Clear Ok(false) path and propagated JsValue error are good for debugging interop failures.

packages/sqlite-web-core/src/database_functions/mod.rs (1)

129-156: wasm-only tests are fine.

Gating tests to wasm matches where these modules run.

packages/sqlite-web-core/src/database_functions/float_is_zero.rs (1)

58-91: LGTM: wasm-targeted tests.

Scope is narrowed appropriately for wasm builds.

packages/sqlite-web-core/src/database_functions/bigint_sum.rs (2)

7-7: Nice: zero-result avoids allocation.

Static "0" bytes with correct length and TRANSIENT use are sound.

Also applies to: 123-133


94-111: Context init check matches project style.

Zeroed-memory scan aligns with prior preference. (Based on learnings)

packages/sqlite-web-core/src/database_functions/float_sum.rs (1)

6-6: Good defensive handling for zero-rows case.

Creating zero hex and surfacing a clear error on CString failure is robust.

Also applies to: 106-129

packages/sqlite-web-core/src/messages.rs (1)

18-21: Params shape is backward-compatible and correct.

serde(default) + skip_serializing_if ensure omission on None and resilience on missing fields. (As per coding guidelines)

Also applies to: 36-41

packages/sqlite-web-core/src/worker.rs (2)

48-62: Structured result builder is clean.

make_query_result_message centralizes shape; fallback path also reports worker-error.


124-133: Startup error reporting is solid.

Propagating leadership errors via worker-error and throwing on failure is appropriate.

packages/sqlite-web-core/src/database.rs (12)

18-46: LGTM: RAII guard and buffer lifetime management.

The BoundBuffers and StmtGuard implementations correctly handle lifetime management for bound text/blob data and ensure proper statement finalization. The StmtGuard::take() method properly prevents double-finalization by nulling the pointer.


114-164: LGTM: Column name and value extraction.

The column handling methods correctly extract names and values with appropriate fallbacks for edge cases (missing names, invalid floats).


166-256: LGTM: Placeholder detection and validation.

The placeholder mode detection and validation correctly enforces the constraint that only plain '?' and numbered '?N' placeholders are supported (mixing is disallowed, named parameters are rejected). Continuity validation for numbered placeholders ensures proper parameter mapping.

Based on learnings.


258-295: LGTM: Parameter mapping logic.

The build_param_map method correctly maps SQLite parameter indices to user-provided parameter indices for both plain and numbered placeholder modes. The unreachable!() at lines 271-272 is justified since numbered placeholders never return null names.


297-359: LGTM: JSON parameter parsing.

The parameter parsing methods correctly handle various JSON types including extended types (blob via base64, bigint as string). Error messages include 1-based parameter indices for clarity.


361-454: LGTM: Parameter binding with proper lifetime management.

The binding methods correctly bind various parameter types to SQLite statements. Text and blob bindings store buffers in BoundBuffers and pass pointers to SQLite with None destructor, relying on BoundBuffers lifetime to keep the data alive during statement execution.


455-502: LGTM: Tail trivia validation.

The is_trivia_tail_only function correctly identifies whether the tail pointer contains only whitespace and comments (line/block). The implementation appropriately assumes the tail begins after a complete SQL statement, so string/quote handling is not required.


504-579: LGTM: Parameterized statement execution.

The bind_params_for_stmt and exec_single_statement_with_params methods correctly orchestrate parameter binding and execution. The use of StmtGuard ensures proper statement finalization even on error paths. The single-statement constraint is appropriately enforced for parameterized queries.


640-687: LGTM: Updated statement execution with lazy column names.

The modifications to exec_prepared_statement correctly use StmtGuard for automatic cleanup and lazily collect column names only when the first row is encountered (optimization for non-SELECT statements).


690-797: LGTM: Transaction state management improvements.

The updates to exec and exec_single_statement correctly integrate transaction state tracking and automatic rollback on error, preventing orphaned transactions.


799-817: LGTM: New parameterized query API.

The exec_with_params public method provides a clean interface for parameterized queries, properly delegating to the internal implementation and handling transaction state refresh.


1062-1218: LGTM: Comprehensive test coverage for parameter binding.

The integration tests thoroughly cover parameterized queries including:

  • Various data types (null, bool, int, float, text)
  • Plain and numbered placeholder modes
  • Duplicate numbered indices
  • Gap detection in numbered placeholders
  • Extended types (blob via base64, bigint as string)
packages/sqlite-web-core/src/coordination.rs (7)

25-41: LGTM: Helper functions for cleaner code.

The reflect_get and send_channel_message helpers provide clean abstractions that centralize error handling and improve readability throughout the file.


80-162: LGTM: Improved error propagation in setup methods.

The updated setup_channel_listener and attempt_leadership methods now return Result<(), JsValue> for proper error propagation. The use of helper functions (reflect_get, set_js_property, send_channel_message) improves code clarity and error handling.


164-200: LGTM: Parameter support in execute_query.

The updated execute_query method correctly threads the optional params through both leader (direct execution) and follower (channel message) code paths. The extracted timeout logic improves readability.


203-240: LGTM: Centralized message handler.

The handle_channel_message function provides clean centralized routing for channel messages with proper error handling and fallback responses.


242-294: LGTM: Clean helper functions for query execution.

The build_query_response, handle_query_response, and exec_on_db functions provide clear, focused abstractions that improve code organization. The exec_on_db function correctly routes to exec_with_params or exec based on whether parameters are provided.


296-340: LGTM: Query request and timeout handling.

The post_query_request and schedule_timeout_promise functions correctly handle message posting and timeout management. The timeout promise properly cleans up pending queries.

Note: Past reviews mentioned "a lot of levels of nesting" in this file. While the current nesting in these functions is moderate, consider flattening nested closures if you refactor for readability.


466-546: LGTM: Test updates for new signatures.

The test updates correctly handle the new method signatures, passing None for parameters where appropriate and checking Result return types for setup methods.

Comment on lines +22 to 24
let bigint_sum_name = CString::new("BIGINT_SUM")
.map_err(|_| "Function name BIGINT_SUM contains interior NUL bytes".to_string())?;
let ret = unsafe {
Copy link

@coderabbitai coderabbitai bot Oct 23, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid allocation for static function names.

Use CStr::from_bytes_with_nul on b"...\0" to skip CString allocation and impossible interior‑NUL error on string literals. Keeps intent and reduces overhead.

-    let bigint_sum_name = CString::new("BIGINT_SUM")
-        .map_err(|_| "Function name BIGINT_SUM contains interior NUL bytes".to_string())?;
+    let bigint_sum_name = std::ffi::CStr::from_bytes_with_nul(b"BIGINT_SUM\0").unwrap();
...
-            bigint_sum_name.as_ptr(),
+            bigint_sum_name.as_ptr(),

Apply similarly to FLOAT_SUM, FLOAT_ZERO_HEX, FLOAT_NEGATE, FLOAT_IS_ZERO. (Based on learnings)

Also applies to: 43-45, 64-66, 85-87, 106-108

🤖 Prompt for AI Agents
In packages/sqlite-web-core/src/database_functions/mod.rs around lines 22-24,
the code creates CString allocations for static function names (e.g.
"BIGINT_SUM"); change these to use CStr::from_bytes_with_nul(b"BIGINT_SUM\0") to
avoid allocation and eliminate the possibility of interior NUL errors for string
literals, converting to a pointer as needed for the unsafe call; apply the same
change to the other static names listed at lines 43-45, 64-66, 85-87, and
106-108 (FLOAT_SUM, FLOAT_ZERO_HEX, FLOAT_NEGATE, FLOAT_IS_ZERO).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not needed

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! I'll skip this optimization. The current approach with CString::new and proper error handling works well.


🧠 Learnings used
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Comment on lines +46 to +58
fn handle_worker_control_message(data: &JsValue) -> bool {
if let Ok(obj) = js_sys::Reflect::get(data, &JsValue::from_str("type")) {
if let Some(msg_type) = obj.as_string() {
if msg_type == "worker-ready" {
return true;
} else if msg_type == "worker-error" {
let _ = js_sys::Reflect::get(data, &JsValue::from_str("error"));
return true;
}
}
}
false
}
Copy link

@coderabbitai coderabbitai bot Oct 23, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider logging worker errors.

Line 52 fetches the error value from the worker message but doesn't use it. While the function correctly returns true to indicate a control message was handled, logging the error would help with debugging.

             } else if msg_type == "worker-error" {
-                let _ = js_sys::Reflect::get(data, &JsValue::from_str("error"));
+                if let Ok(error) = js_sys::Reflect::get(data, &JsValue::from_str("error")) {
+                    web_sys::console::error_1(&error);
+                }
                 return true;
🤖 Prompt for AI Agents
In packages/sqlite-web/src/lib.rs around lines 46 to 58, the code reads the
"error" field from the worker message but drops it; modify the branch that
handles "worker-error" to log the error value before returning true. Retrieve
the error JsValue, convert it to a readable string (e.g., try error.as_string()
and fall back to js_sys::JSON::stringify(&error).ok().and_then(|s|
s.as_string()).unwrap_or_else(|| format!("{:?}", error))), then call
web_sys::console::error_1(&JsValue::from_str(&error_string)) so the error is
visible in the browser console.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not needed

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! I'll skip this suggestion.


🧠 Learnings used
Learnt from: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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

Comment on lines 60 to 88
fn handle_query_result_message(
data: &JsValue,
pending_queries: &Rc<RefCell<Vec<(js_sys::Function, js_sys::Function)>>>,
) {
if let Ok(obj) = js_sys::Reflect::get(data, &JsValue::from_str("type")) {
if let Some(msg_type) = obj.as_string() {
if msg_type == "query-result" {
if let Some((resolve, reject)) = pending_queries.borrow_mut().pop() {
if let Ok(error) = js_sys::Reflect::get(data, &JsValue::from_str("error")) {
if !error.is_null() && !error.is_undefined() {
let error_str =
error.as_string().unwrap_or_else(|| format!("{error:?}"));
let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&error_str));
return;
}
}

if let Ok(result) = js_sys::Reflect::get(data, &JsValue::from_str("result")) {
if !result.is_null() && !result.is_undefined() {
let result_str =
result.as_string().unwrap_or_else(|| format!("{result:?}"));
let _ = resolve.call1(&JsValue::NULL, &JsValue::from_str(&result_str));
}
}
}
}
}
}
}
Copy link

@coderabbitai coderabbitai bot Oct 23, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle failures when calling resolve/reject callbacks.

Lines 72 and 81 ignore the result of call1() on the resolve and reject functions. If these JavaScript function calls fail, the promise will never settle, causing the query to hang indefinitely without any indication of what went wrong.

Apply this diff to log failures:

-                            let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&error_str));
+                            if let Err(e) = reject.call1(&JsValue::NULL, &JsValue::from_str(&error_str)) {
+                                web_sys::console::error_1(&JsValue::from_str(&format!("Failed to reject promise: {e:?}")));
+                            }
                             return;
-                            let _ = resolve.call1(&JsValue::NULL, &JsValue::from_str(&result_str));
+                            if let Err(e) = resolve.call1(&JsValue::NULL, &JsValue::from_str(&result_str)) {
+                                web_sys::console::error_1(&JsValue::from_str(&format!("Failed to resolve promise: {e:?}")));
+                            }
🤖 Prompt for AI Agents
In packages/sqlite-web/src/lib.rs around lines 60 to 88, the code currently
ignores the Result returned by resolve.call1 and reject.call1 which can cause
promises to hang silently; update both call1 invocations to handle their Result
(e.g., match or if let Err(err) = ...) and log any failure using a JS console
logger such as web_sys::console::error_1(&err) or similar so failures are
visible; ensure both the reject and resolve branches handle and log errors from
call1 rather than discarding them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not needed

Copy link

Choose a reason for hiding this comment

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

@findolor, understood. I've noted your decision on this suggestion.


🧠 Learnings used
Learnt from: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sqlite-web-core/src/database.rs (1)

1368-1381: Fix test assertion to match actual prepare error message.

prepare_one returns the SQLite error string (e.g., “near ...: syntax error”). Adjust the test to assert on that instead of a hardcoded prefix.

     let result = db.exec("INVALID SQL SYNTAX HERE").await;
     assert!(result.is_err());
     let error = result.unwrap_err();
-    assert!(
-        error.contains("Failed to prepare statement"),
-        "Invalid SQL should produce prepare statement error"
-    );
+    assert!(
+        error.contains("syntax error") || error.to_lowercase().contains("sqlite error"),
+        "Invalid SQL should surface SQLite's prepare error"
+    );
♻️ Duplicate comments (2)
packages/sqlite-web-core/src/util.rs (1)

49-52: LGTM: JSON.stringify fallback added as previously suggested.
This addresses the earlier nit on non-Error JsValues.

packages/sqlite-web/src/lib.rs (1)

46-58: Optional: log worker-error payload to console for visibility.
Already suggested previously; repeating for completeness.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7624b5 and 89dc7e6.

⛔ Files ignored due to path filters (1)
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • packages/sqlite-web-core/src/database.rs (11 hunks)
  • packages/sqlite-web-core/src/util.rs (2 hunks)
  • packages/sqlite-web/src/lib.rs (6 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-10-20T12:24:07.874Z
Learnt from: findolor
PR: rainlanguage/sqlite-web#19
File: packages/sqlite-web-core/src/database.rs:122-157
Timestamp: 2025-10-20T12:24:07.874Z
Learning: In packages/sqlite-web-core/src/database.rs, named parameters (:name, name, $name) are intentionally not supported for SQL parameter binding. Only plain '?' and numbered '?N' placeholders are supported.

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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()

Applied to files:

  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (1)
packages/sqlite-web-core/src/database.rs (1)

48-57: Overall binding/validation flow looks solid.

  • Placeholder detection and validation cover plain and numbered cases, reject named/mixed, and ensure continuity.
  • RAII finalization via StmtGuard prevents leaks on early returns.
  • BoundBuffers lifetime management for text/blob is correct.

Please run the wasm tests for exec_with_params on your CI target to confirm no regressions in environments where OPFS is unavailable (the tests gracefully early-return today).

Also applies to: 161-216, 253-290, 499-531, 533-574, 794-812

Comment on lines +29 to +37
pub fn set_js_property(target: &JsValue, key: &str, value: &JsValue) -> Result<(), JsValue> {
match Reflect::set(target, &JsValue::from_str(key), value) {
Ok(true) => Ok(()),
Ok(false) => Err(JsValue::from_str(&format!(
"Reflect::set returned false for key {key}"
))),
Err(err) => Err(err),
}
}
Copy link

@coderabbitai coderabbitai bot Oct 27, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a real JS TypeError for Reflect::set returning false.

Return a TypeError with the property name; improves debuggability and consistency with JS semantics.

 pub fn set_js_property(target: &JsValue, key: &str, value: &JsValue) -> Result<(), JsValue> {
     match Reflect::set(target, &JsValue::from_str(key), value) {
         Ok(true) => Ok(()),
-        Ok(false) => Err(JsValue::from_str(&format!(
-            "Reflect::set returned false for key {key}"
-        ))),
+        Ok(false) => Err(js_sys::TypeError::new(
+            &format!("Reflect.set returned false for property '{key}'"),
+        )
+        .into()),
         Err(err) => Err(err),
     }
 }
🤖 Prompt for AI Agents
In packages/sqlite-web-core/src/util.rs around lines 29 to 37, when Reflect::set
returns Ok(false) the code currently returns a JsValue string; change this to
return a real JavaScript TypeError containing the property name (e.g.
Err(js_sys::TypeError::new(&format!("Reflect::set returned false for key
{key}")).into())). Replace the Err(JsValue::from_str(...)) path with creating a
js_sys::TypeError and converting it into JsValue so callers receive a proper JS
TypeError.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not needed

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! I've noted your decision.

Comment on lines +112 to +124
fn encode_binary_to_obj(bytes: Vec<u8>) -> Result<JsValue, SQLiteWasmDatabaseError> {
let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
let obj = js_sys::Object::new();
js_sys::Reflect::set(
&obj,
&JsValue::from_str("__type"),
&JsValue::from_str("blob"),
)
.map_err(SQLiteWasmDatabaseError::from)?;
js_sys::Reflect::set(&obj, &JsValue::from_str("base64"), &JsValue::from_str(&b64))
.map_err(SQLiteWasmDatabaseError::from)?;
Ok(obj.into())
}
Copy link

@coderabbitai coderabbitai bot Oct 27, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid extra allocation when base64‑encoding.

Use encode_string to write into a preallocated String.

 fn encode_binary_to_obj(bytes: Vec<u8>) -> Result<JsValue, SQLiteWasmDatabaseError> {
-    let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
+    let mut b64 = String::new();
+    base64::engine::general_purpose::STANDARD.encode_string(&bytes, &mut b64);
     let obj = js_sys::Object::new();
🤖 Prompt for AI Agents
In packages/sqlite-web/src/lib.rs around lines 112 to 124, the base64 encoding
currently creates an owned Vec->String via encode which allocates twice; replace
it with a preallocated String and call STANDARD.encode_string to write into it.
Allocate the String with capacity ((bytes.len()+2)/3)*4 (or use
bytes.len()*4/3+4) to avoid reallocations, call encode_string(&bytes, &mut s) to
fill it, then use JsValue::from_str(&s) when setting the "base64" property; keep
the same error handling and return obj.into() as before.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not needed

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! I've noted your decision on this optimization. The current implementation is clear and straightforward. 👍


🧠 Learnings used
Learnt from: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/sqlite-web-core/src/util.rs (1)

29-37: Consider returning a real JS TypeError on Reflect.set false.

Using TypeError improves parity with JS semantics and debuggability. If you keep the current string error by choice, ignore this.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89dc7e6 and 9b5926a.

⛔ Files ignored due to path filters (1)
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • packages/sqlite-web-core/src/database.rs (11 hunks)
  • packages/sqlite-web-core/src/util.rs (3 hunks)
  • packages/sqlite-web/src/lib.rs (6 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-10-20T12:24:07.874Z
Learnt from: findolor
PR: rainlanguage/sqlite-web#19
File: packages/sqlite-web-core/src/database.rs:122-157
Timestamp: 2025-10-20T12:24:07.874Z
Learning: In packages/sqlite-web-core/src/database.rs, named parameters (:name, name, $name) are intentionally not supported for SQL parameter binding. Only plain '?' and numbered '?N' placeholders are supported.

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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()

Applied to files:

  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web/src/lib.rs
🧬 Code graph analysis (2)
packages/sqlite-web-core/src/util.rs (2)
packages/sqlite-web/src/lib.rs (7)
  • new (247-265)
  • v (131-131)
  • v (134-134)
  • v (137-137)
  • from (212-214)
  • from (218-220)
  • from (224-229)
packages/sqlite-web-core/src/coordination.rs (1)
  • new (44-78)
packages/sqlite-web-core/src/database.rs (2)
packages/sqlite-web-core/src/database_functions/bigint_sum.rs (5)
  • std (81-81)
  • std (99-99)
  • std (128-131)
  • std (153-156)
  • new (15-17)
packages/sqlite-web-core/src/database_functions/float_sum.rs (5)
  • std (70-70)
  • std (87-87)
  • std (115-118)
  • std (157-160)
  • new (13-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (9)
packages/sqlite-web-core/src/util.rs (1)

39-60: BigInt handled correctly; note nested BigInt caveat.

Top-level BigInt is safe; JSON.stringify on objects containing BigInt will still throw and hit the Debug fallback. Optional: detect BigInt properties (via JS helper) before stringify to produce a stable string.

packages/sqlite-web-core/src/database.rs (3)

535-576: Early-return paths finalize stmt via RAII — good.

Using StmtGuard ensures finalize on both success and error, preventing leaks on bind/execute failures.


126-161: JSON float handling: good guard against NaN/Infinity.

Returning null for non-finite values avoids silent corruption.


163-218: Placeholder mode detection/validation/map are consistent and reject named params.

Continuity checks and mode exclusivity look solid; matches the decision to not support named parameters. Based on learnings

Also applies to: 220-253, 255-292

packages/sqlite-web/src/lib.rs (5)

16-28: Good: always revoke Blob URL even if Worker::new fails.

Prevents object URL leaks on constructor errors.


316-326: Avoid orphaned promises on postMessage failure — good.

Post then enqueue callbacks; reject on error. Prevents hangs.


97-125: Param normalization covers BigInt and binary types; consistent error text.

BigInt→string object and Uint8Array/ArrayBuffer→base64 object look correct; non‑finite numbers rejected with position info.

Also applies to: 127-161


267-279: Message construction with optional params and error propagation — solid.

Only set “params” when non-empty; Reflect::set errors are propagated; Promise awaited via JsFuture.

Also applies to: 281-331


30-45: Code already correctly implements FIFO ordering — no changes needed.

The pending_queries VecDeque already uses FIFO via push_back() and pop_front() (lines 321 and 68). There is no LIFO mismatch to fix. The review comment incorrectly implies this pattern needed to be implemented or corrected, when it is already correct in the current code.

The optional suggestion about requestId mapping is valid only if the worker can respond out-of-order; however, the codebase contains no such mechanism, and responses are processed sequentially in order received. This remains a future enhancement suggestion if needed, but is not a current defect.

Likely an incorrect or invalid review comment.

Comment on lines 390 to 407
fn bind_text(
&self,
stmt: *mut sqlite3_stmt,
i: i32,
c: &CString,
buffers: &mut BoundBuffers,
) -> Result<(), String> {
buffers._texts.push(c.clone());
let last = buffers._texts.last().unwrap();
let ptr = last.as_ptr();
let len = last.as_bytes().len() as i32;
let rc = unsafe {
sqlite3_bind_text(stmt, i, ptr, len, None::<unsafe extern "C" fn(*mut c_void)>)
};
if rc != SQLITE_OK {
return Err(format!("Failed to bind text at {i}"));
}
Ok(())
Copy link

@coderabbitai coderabbitai bot Oct 27, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Text/blob binding relies on buffers staying alive; add rationale or use SQLITE_TRANSIENT.

You pass NULL destructor and retain backing storage in BoundBuffers. This is correct but easy to break in refactors. Add a brief comment stating the lifetime guarantee, or switch to SQLITE_TRANSIENT to let SQLite copy.

-        let rc = unsafe {
-            sqlite3_bind_text(stmt, i, ptr, len, None::<unsafe extern "C" fn(*mut c_void)>)
-        };
+        // Safety: BoundBuffers keeps `last` alive until after execution; if this changes,
+        // use SQLITE_TRANSIENT to force SQLite to copy.
+        let rc = unsafe {
+            sqlite3_bind_text(stmt, i, ptr, len, None::<unsafe extern "C" fn(*mut c_void)>)
+        };

Also applies to: 410-434


🏁 Script executed:

#!/bin/bash
# Locate and inspect BoundBuffers struct definition
rg -n "struct BoundBuffers" packages/sqlite-web-core/src/database.rs -A 10

Length of output: 223


🏁 Script executed:

#!/bin/bash
# Check bind_blob function (lines 410-434) to see if same pattern applies
rg -n "fn bind_blob" packages/sqlite-web-core/src/database.rs -A 20

Length of output: 694


🏁 Script executed:

#!/bin/bash
# Inspect exec_prepared_statement to verify buffers lifetime
rg -n "fn exec_prepared_statement" packages/sqlite-web-core/src/database.rs -A 40

Length of output: 1801


🏁 Script executed:

#!/bin/bash
# Check where BoundBuffers is instantiated and passed to binding functions
rg -n "BoundBuffers" packages/sqlite-web-core/src/database.rs -B 2 -A 2 | head -100

Length of output: 1103


🏁 Script executed:

#!/bin/bash
# Get full bind_prepared_statement implementation (line 505+)
sed -n '505,560p' packages/sqlite-web-core/src/database.rs

Length of output: 2165


🏁 Script executed:

#!/bin/bash
# Find where bind_prepared_statement is called
rg -n "bind_prepared_statement" packages/sqlite-web-core/src/database.rs -B 2 -A 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the execute/query flow to see if buffers are held for statement lifetime
rg -n "fn execute\|fn query" packages/sqlite-web-core/src/database.rs -A 15 | head -80

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get lines 537-600 to see complete exec_single_statement_with_params flow
sed -n '537,600p' packages/sqlite-web-core/src/database.rs

Length of output: 2468


🏁 Script executed:

#!/bin/bash
# Search for where buffers are assigned from function call (not just parameter passing)
rg -n "let.*buffers.*=" packages/sqlite-web-core/src/database.rs -B 1 -A 3

Length of output: 462


🏁 Script executed:

#!/bin/bash
# Look at the complete exec_single_statement_with_params to understand buffer ownership
sed -n '537,650p' packages/sqlite-web-core/src/database.rs

Length of output: 4148


🏁 Script executed:

#!/bin/bash
# Search for other buffer-related unsafe blocks to check if pattern is consistent
rg -n "sqlite3_bind_text|sqlite3_bind_blob" packages/sqlite-web-core/src/database.rs -B 2 -A 2

Length of output: 383


🏁 Script executed:

#!/bin/bash
# Check if there are any async variants that might handle buffers differently
rg -n "async fn.*exec\|async fn.*query\|async fn.*execute" packages/sqlite-web-core/src/database.rs

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get the function name signature that wraps bind_prepared_statement
sed -n '502,505p' packages/sqlite-web-core/src/database.rs

Length of output: 125


Add clarity to buffer lifetime safety with a comment.

The code correctly keeps BoundBuffers in scope (_buffers_guard) through both binding and execution, ensuring pointers remain valid with the NULL destructor. However, this pattern is fragile under refactoring. Adding an explicit comment explaining the lifetime contract is good practice, and it applies to both bind_text and bind_blob.

        let rc = unsafe {
+           // Safety: BoundBuffers keeps `last` alive until after execution; if this changes,
+           // use SQLITE_TRANSIENT to force SQLite to copy.
            sqlite3_bind_text(stmt, i, ptr, len, None::<unsafe extern "C" fn(*mut c_void)>)
        };

The same rationale applies to bind_blob (lines 410–434).

🤖 Prompt for AI Agents
packages/sqlite-web-core/src/database.rs around lines 390–407 (and similarly
410–434 for bind_blob): add an explicit comment above the bind_text (and
bind_blob) implementations that documents the lifetime safety contract — explain
that BoundBuffers' _texts/_blobs vectors are pushed to and kept alive via a
surrounding _buffers_guard for the duration of statement execution, that the
pointers passed to sqlite3_bind_text/sqlite3_bind_blob are only valid because we
use a NULL destructor and maintain the buffers in scope, and warn that
refactoring which removes or shortens the guard's scope would break this
invariant; keep the comment concise, reference the NULL destructor usage, and
mirror the same comment in both bind_text and bind_blob locations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not needed

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! I'll skip this suggestion.


🧠 Learnings used
Learnt from: findolor
PR: rainlanguage/sqlite-web#19
File: packages/sqlite-web-core/src/database.rs:122-157
Timestamp: 2025-10-20T12:24:07.874Z
Learning: In packages/sqlite-web-core/src/database.rs, named parameters (:name, name, $name) are intentionally not supported for SQL parameter binding. Only plain '?' and numbered '?N' placeholders are supported.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sqlite-web-core/src/database.rs (1)

919-932: Tests: avoid exact float equality; use approx.

Exact equality with f64 is brittle. Use a tolerance like elsewhere in this file.

-        assert_eq!(
-            first["price"].as_f64().unwrap(),
-            999.99,
-            "First product price should be 999.99"
-        );
+        assert!(
+            (first["price"].as_f64().unwrap() - 999.99).abs() < 1e-9,
+            "First product price should be ~999.99"
+        );
 ...
-        assert_eq!(
-            second["price"].as_f64().unwrap(),
-            25.50,
-            "Second product price should be 25.50"
-        );
+        assert!(
+            (second["price"].as_f64().unwrap() - 25.50).abs() < 1e-9,
+            "Second product price should be ~25.50"
+        );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a93d09 and 8e096d2.

⛔ Files ignored due to path filters (1)
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • packages/sqlite-web-core/src/database.rs (11 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-20T12:24:07.874Z
Learnt from: findolor
PR: rainlanguage/sqlite-web#19
File: packages/sqlite-web-core/src/database.rs:122-157
Timestamp: 2025-10-20T12:24:07.874Z
Learning: In packages/sqlite-web-core/src/database.rs, named parameters (:name, name, $name) are intentionally not supported for SQL parameter binding. Only plain '?' and numbered '?N' placeholders are supported.

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
🔇 Additional comments (4)
packages/sqlite-web-core/src/database.rs (4)

80-97: Prepare errors now consistently prefixed.

Good: prepare_one returns “Failed to prepare statement: …”, aligning with tests and improving debuggability.


127-162: Non‑finite floats → null; blob marker is fine.

Returning JSON null for NaN/Infinity avoids silent coercion. The "" sentinel is acceptable for preview queries.


164-219: Placeholder detection/validation is sound; named params explicitly rejected.

Mixing '?' and '?N' is blocked; numbered continuity enforced; named placeholders rejected per design. Consider documenting this limitation in the public API if not already.

Based on learnings

Also applies to: 221-254, 256-294


642-675: Stmt RAII is correct; finalize on all paths.

Wrapping stmt with StmtGuard ensures finalization even on error. Good.

Comment on lines 15 to 16
unsafe impl Send for SQLiteDatabase {}
unsafe impl Sync for SQLiteDatabase {}
Copy link

@coderabbitai coderabbitai bot Oct 27, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Gate unsafe Send/Sync to wasm (avoid unsound cross-thread use).

Unconditionally marking SQLiteDatabase as Send + Sync can be unsound off‑wasm and invites accidental multi‑threaded use. Gate these impls to wasm or drop them.

-unsafe impl Send for SQLiteDatabase {}
-unsafe impl Sync for SQLiteDatabase {}
+#[cfg(target_family = "wasm")]
+unsafe impl Send for SQLiteDatabase {}
+#[cfg(target_family = "wasm")]
+unsafe impl Sync for SQLiteDatabase {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
unsafe impl Send for SQLiteDatabase {}
unsafe impl Sync for SQLiteDatabase {}
#[cfg(target_family = "wasm")]
unsafe impl Send for SQLiteDatabase {}
#[cfg(target_family = "wasm")]
unsafe impl Sync for SQLiteDatabase {}
🤖 Prompt for AI Agents
In packages/sqlite-web-core/src/database.rs around lines 15 to 16, the code
unconditionally implements unsafe Send and Sync for SQLiteDatabase which is
unsound off‑wasm; gate these impls to wasm by wrapping them in conditional
compilation (e.g. #[cfg(target_arch = "wasm32")] on each unsafe impl) or remove
them entirely if cross‑thread use is not supported, and add a short comment
explaining why they are only enabled for wasm targets.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is not an issue

Copy link

Choose a reason for hiding this comment

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

@findolor, understood—I've noted your decision. Thank you for the clarification!


🧠 Learnings used
Learnt from: CR
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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: findolor
PR: rainlanguage/sqlite-web#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
PR: rainlanguage/sqlite-web#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: CR
PR: rainlanguage/sqlite-web#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

#[cfg(all(test, target_family = "wasm"))]
mod tests {
use super::*;
use serde_json::json;
Copy link

@coderabbitai coderabbitai bot Oct 27, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Tests: bring base64::Engine into scope for encode().

Method calls on STANDARD require the Engine trait in the tests module scope; otherwise, encode won’t resolve.

 mod tests {
     use super::*;
-    use serde_json::json;
+    use serde_json::json;
+    use base64::Engine as _;

Also applies to: 1171-1183

🤖 Prompt for AI Agents
In packages/sqlite-web-core/src/database.rs around line 831 (and similarly for
the tests around lines 1171-1183), the tests call STANDARD.encode(...) but the
base64::Engine trait is not in scope so encode() doesn't resolve; fix by
importing the trait into the tests module (e.g., add a use base64::Engine;
inside the tests mod or the specific test functions where STANDARD is used) so
STANDARD.encode(...) compiles.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is not an issue

Copy link

Choose a reason for hiding this comment

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

@findolor, you're absolutely right! The use super::*; statement in the tests module already brings the base64::Engine trait into scope from the parent module (where it's imported at the top of the file), so STANDARD.encode() works without any additional imports. Thanks for the clarification!


🧠 Learnings used
Learnt from: CR
PR: rainlanguage/sqlite-web#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
PR: rainlanguage/sqlite-web#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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/sqlite-web-core/src/database.rs (2)

15-16: Gate unsafe Send/Sync to wasm targets (or remove).

Unconditional Send/Sync can be unsound off‑wasm. Consider cfg-gating to wasm.

-unsafe impl Send for SQLiteDatabase {}
-unsafe impl Sync for SQLiteDatabase {}
+#[cfg(target_family = "wasm")]
+unsafe impl Send for SQLiteDatabase {}
+#[cfg(target_family = "wasm")]
+unsafe impl Sync for SQLiteDatabase {}

459-506: Add a brief note about scanner scope (quotes/strings intentionally out-of-scope).

The tail comes after a complete statement; this scanner only permits whitespace/comments. A short comment helps prevent reuse on arbitrary fragments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e096d2 and 8a1e9af.

⛔ Files ignored due to path filters (1)
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • packages/sqlite-web-core/src/database.rs (11 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-20T12:24:07.874Z
Learnt from: findolor
PR: rainlanguage/sqlite-web#19
File: packages/sqlite-web-core/src/database.rs:122-157
Timestamp: 2025-10-20T12:24:07.874Z
Learning: In packages/sqlite-web-core/src/database.rs, named parameters (:name, name, $name) are intentionally not supported for SQL parameter binding. Only plain '?' and numbered '?N' placeholders are supported.

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: test
🔇 Additional comments (7)
packages/sqlite-web-core/src/database.rs (7)

164-219: Placeholder detection and validation: solid and consistent.

Clear separation of Plain vs Numbered, named rejected by design; continuity and count checks are correct.

Also applies to: 221-254


256-294: Param map build is correct; unreachable branch is justified.

Mapping '?N'→0-based indices and treating anonymous '?' via null name is appropriate here.


295-358: Parameter JSON normalization is well-scoped.

i64/f64 split, CString NUL check, blob/base64 and bigint object forms are sensible.


359-441: Binding helpers: good error context and safe lifetimes.

Appending sqlite_errmsg aids diagnostics; buffer-ownership via BoundBuffers is correct for SQLITE_STATIC.


508-541: End-to-end binding pipeline reads cleanly.

Mode detection → validation → map → bind with buffers guard is cohesive.


542-583: Single-statement with params: correct tail and arity handling.

Enforces single statement, guards no-param cases, and uses StmtGuard for cleanup.


803-821: Public exec_with_params API shape is good.

Returns pretty JSON for selects and affected-row text otherwise; updates transaction state.

Comment on lines 173 to 209
let mut has_plain = false;
let mut has_numbered = false;
let mut has_named = false;
let mut numbered_max: usize = 0;
let mut numbered_present = std::collections::BTreeSet::new();

for i in 1..=param_count as i32 {
let name_ptr = unsafe { sqlite3_bind_parameter_name(stmt, i) };
if name_ptr.is_null() {
has_plain = true;
} else {
let name = unsafe { CStr::from_ptr(name_ptr) }.to_string_lossy();
let s = name.as_ref();
if let Some(digits) = s.strip_prefix('?') {
if digits.is_empty() {
has_plain = true;
} else if let Ok(n) = digits.parse::<isize>() {
if n <= 0 {
return Err(
"Invalid parameter index: ?0 or negative indices are not allowed."
.to_string(),
);
}
has_numbered = true;
let n_u = n as usize;
numbered_present.insert(n_u);
if n_u > numbered_max {
numbered_max = n_u;
}
} else {
return Err(format!("Invalid parameter index: {}", s));
}
} else {
has_named = true; // :name/@name/$name not supported
}
}
}
Copy link

Choose a reason for hiding this comment

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

consider something like

Suggested change
let mut has_plain = false;
let mut has_numbered = false;
let mut has_named = false;
let mut numbered_max: usize = 0;
let mut numbered_present = std::collections::BTreeSet::new();
for i in 1..=param_count as i32 {
let name_ptr = unsafe { sqlite3_bind_parameter_name(stmt, i) };
if name_ptr.is_null() {
has_plain = true;
} else {
let name = unsafe { CStr::from_ptr(name_ptr) }.to_string_lossy();
let s = name.as_ref();
if let Some(digits) = s.strip_prefix('?') {
if digits.is_empty() {
has_plain = true;
} else if let Ok(n) = digits.parse::<isize>() {
if n <= 0 {
return Err(
"Invalid parameter index: ?0 or negative indices are not allowed."
.to_string(),
);
}
has_numbered = true;
let n_u = n as usize;
numbered_present.insert(n_u);
if n_u > numbered_max {
numbered_max = n_u;
}
} else {
return Err(format!("Invalid parameter index: {}", s));
}
} else {
has_named = true; // :name/@name/$name not supported
}
}
}
#[derive(Default)]
struct ParamAnalysis {
has_plain: bool,
has_numbered: bool,
has_named: bool,
numbered_max: usize,
numbered_present: std::collections::BTreeSet<usize>,
}
let ParamAnalysis { has_plain, has_numbered, has_named, numbered_max, numbered_present } =
(1..=param_count as i32)
.try_fold(ParamAnalysis::default(), |mut acc, i| {
let name_ptr = unsafe { sqlite3_bind_parameter_name(stmt, i) };
if name_ptr.is_null() {
acc.has_plain = true;
return Ok(acc);
}
let name = unsafe { CStr::from_ptr(name_ptr) }.to_string_lossy();
let s = name.as_ref();
match s.strip_prefix('?') {
Some(digits) if digits.is_empty() => {
acc.has_plain = true;
}
Some(digits) => {
let n = digits.parse::<isize>()
.map_err(|_| format!("Invalid parameter index: {}", s))?;
if n <= 0 {
return Err("Invalid parameter index: ?0 or negative indices are not allowed.".to_string());
}
acc.has_numbered = true;
let n_u = n as usize;
acc.numbered_present.insert(n_u);
acc.numbered_max = acc.numbered_max.max(n_u);
}
None => {
acc.has_named = true;
}
}
Ok(acc)
})?;

Comment on lines 233 to 260
match mode {
PlaceholderMode::Plain { count } => {
if params_len != *count {
return Err(format!(
"Expected {} parameters but got {}.",
count, params_len
));
}
Ok(())
}
PlaceholderMode::Numbered { max, present } => {
for need in 1..=*max {
if !present.contains(&need) {
return Err(format!(
"Missing parameter index ?{} in statement (indices must be continuous).",
need
));
}
}
if params_len != *max {
return Err(format!(
"Expected {} parameters but got {}.",
max, params_len
));
}
Ok(())
}
}
Copy link

Choose a reason for hiding this comment

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

Suggested change
match mode {
PlaceholderMode::Plain { count } => {
if params_len != *count {
return Err(format!(
"Expected {} parameters but got {}.",
count, params_len
));
}
Ok(())
}
PlaceholderMode::Numbered { max, present } => {
for need in 1..=*max {
if !present.contains(&need) {
return Err(format!(
"Missing parameter index ?{} in statement (indices must be continuous).",
need
));
}
}
if params_len != *max {
return Err(format!(
"Expected {} parameters but got {}.",
max, params_len
));
}
Ok(())
}
}
match mode {
PlaceholderMode::Plain { count } if params_len != *count => {
Err(format!(
"Expected {count} parameters but got {params_len}."
))
}
PlaceholderMode::Plain { .. } => Ok(()),
PlaceholderMode::Numbered { max, present } => {
(1..=*max)
.find(|need| !present.contains(need))
.map(|need| {
Err(format!(
"Missing parameter index ?{need} in statement (indices must be continuous)."
))
})
.unwrap_or_else(|| {
if params_len != *max {
Err(format!(
"Expected {max} parameters but got {params_len}."
))
} else {
Ok(())
}
})
}
}

Comment on lines 269 to 298
let mut param_map: Vec<usize> = Vec::with_capacity(param_count);
for i in 1..=param_count as i32 {
let name_ptr = unsafe { sqlite3_bind_parameter_name(stmt, i) };
let target_index = if name_ptr.is_null() {
match mode {
PlaceholderMode::Plain { .. } => (i as usize) - 1,
PlaceholderMode::Numbered { .. } => {
unreachable!("numbered placeholders never return null names")
}
}
} else {
let name = unsafe { CStr::from_ptr(name_ptr) }.to_string_lossy();
let s = name.as_ref();
if let Some(stripped) = s.strip_prefix('?') {
let n: usize = stripped
.parse()
.map_err(|_| format!("Invalid parameter index: {}", s))?;
if n == 0 {
return Err(
"Invalid parameter index: ?0 or negative indices are not allowed."
.to_string(),
);
}
n - 1
} else {
return Err("Named parameters not supported.".to_string());
}
};
param_map.push(target_index);
}
Copy link

Choose a reason for hiding this comment

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

Suggested change
let mut param_map: Vec<usize> = Vec::with_capacity(param_count);
for i in 1..=param_count as i32 {
let name_ptr = unsafe { sqlite3_bind_parameter_name(stmt, i) };
let target_index = if name_ptr.is_null() {
match mode {
PlaceholderMode::Plain { .. } => (i as usize) - 1,
PlaceholderMode::Numbered { .. } => {
unreachable!("numbered placeholders never return null names")
}
}
} else {
let name = unsafe { CStr::from_ptr(name_ptr) }.to_string_lossy();
let s = name.as_ref();
if let Some(stripped) = s.strip_prefix('?') {
let n: usize = stripped
.parse()
.map_err(|_| format!("Invalid parameter index: {}", s))?;
if n == 0 {
return Err(
"Invalid parameter index: ?0 or negative indices are not allowed."
.to_string(),
);
}
n - 1
} else {
return Err("Named parameters not supported.".to_string());
}
};
param_map.push(target_index);
}
let param_map: Vec<usize> = (1..=param_count as i32)
.map(|i| {
let name_ptr = unsafe { sqlite3_bind_parameter_name(stmt, i) };
if name_ptr.is_null() {
match mode {
PlaceholderMode::Plain { .. } => Ok((i as usize) - 1),
PlaceholderMode::Numbered { .. } => {
unreachable!("numbered placeholders never return null names")
}
}
} else {
let name = unsafe { CStr::from_ptr(name_ptr) }.to_string_lossy();
let s = name.as_ref();
match s.strip_prefix('?') {
Some(stripped) => {
let n: usize = stripped
.parse()
.map_err(|_| format!("Invalid parameter index: {s}"))?;
if n == 0 {
Err("Invalid parameter index: ?0 or negative indices are not allowed.".to_string())
} else {
Ok(n - 1)
}
}
None => Err("Named parameters not supported.".to_string())
}
}
})
.collect::<Result<Vec<_>, _>>()?;

Comment on lines 475 to 512
let mut i = 0usize;
while i < rest.len() {
match rest[i] {
b' ' | b'\t' | b'\r' | b'\n' => {
i += 1;
}
b'-' => {
if i + 1 < rest.len() && rest[i + 1] == b'-' {
// line comment -- ... until newline
i += 2;
while i < rest.len() && rest[i] != b'\n' {
i += 1;
}
} else {
return false;
}
}
b'/' => {
if i + 1 < rest.len() && rest[i + 1] == b'*' {
// block comment /* ... */
i += 2;
while i + 1 < rest.len() && !(rest[i] == b'*' && rest[i + 1] == b'/') {
i += 1;
}
if i + 1 < rest.len() {
i += 2; // consume */
} else {
// unterminated comment: treat as non-trivia to be safe
return false;
}
} else {
return false;
}
}
_ => return false,
}
}
true
Copy link

Choose a reason for hiding this comment

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

consider

        fn is_all_trivia(remaining: &[u8]) -> bool {
            match remaining {
                [] => true,
                [b' ' | b'\t' | b'\r' | b'\n', tail @ ..] => is_all_trivia(tail),
                [b'-', b'-', tail @ ..] => {
                    let after_comment = tail.iter()
                        .position(|&byte| byte == b'\n')
                        .map_or(tail.len(), |pos| pos);
                    is_all_trivia(&tail[after_comment..])
                }
                [b'/', b'*', tail @ ..] => {
                    tail.windows(2)
                        .position(|window| window == b"*/")
                        .map(|pos| is_all_trivia(&tail[pos + 2..]))
                        .unwrap_or(false)
                }
                _ => false,
            }
        }

or

        let mut remaining = rest;
        loop {
            match remaining {
                [] => return true,
                [b' ' | b'\t' | b'\r' | b'\n', tail @ ..] => {
                    remaining = tail;
                }
                [b'-', b'-', tail @ ..] => {
                    // line comment -- ... until newline
                    match tail.iter().position(|&byte| byte == b'\n') {
                        Some(pos) => remaining = &tail[pos..],
                        None => return true, // comment to end of input
                    }
                }
                [b'/', b'*', tail @ ..] => {
                    // block comment /* ... */
                    match tail.windows(2).position(|window| window == b"*/") {
                        Some(pos) => remaining = &tail[pos + 2..],
                        None => return false, // unterminated comment
                    }
                }
                _ => return false,
            }
        }

Comment on lines 526 to 546
let mut buffers = BoundBuffers {
_texts: Vec::new(),
_blobs: Vec::new(),
};

let param_count = unsafe { sqlite3_bind_parameter_count(stmt) } as usize;
for i in 1..=param_count as i32 {
let target_index = param_map[(i - 1) as usize];
let val = params.get(target_index).ok_or_else(|| {
format!(
"Missing parameter value at index {} (0-based {})",
target_index + 1,
target_index
)
})?;

let kind = self.parse_json_param(target_index, val)?;
self.bind_param(stmt, i, &kind, &mut buffers)?;
}

Ok(buffers)
Copy link

Choose a reason for hiding this comment

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

Suggested change
let mut buffers = BoundBuffers {
_texts: Vec::new(),
_blobs: Vec::new(),
};
let param_count = unsafe { sqlite3_bind_parameter_count(stmt) } as usize;
for i in 1..=param_count as i32 {
let target_index = param_map[(i - 1) as usize];
let val = params.get(target_index).ok_or_else(|| {
format!(
"Missing parameter value at index {} (0-based {})",
target_index + 1,
target_index
)
})?;
let kind = self.parse_json_param(target_index, val)?;
self.bind_param(stmt, i, &kind, &mut buffers)?;
}
Ok(buffers)
let param_count = unsafe { sqlite3_bind_parameter_count(stmt) } as usize;
(1..=param_count as i32)
.try_fold(
BoundBuffers {
_texts: Vec::new(),
_blobs: Vec::new(),
},
|mut buffers, param_index| {
let target_index = param_map[(param_index - 1) as usize];
let val = params.get(target_index).ok_or_else(|| {
format!(
"Missing parameter value at index {} (0-based {target_index})",
target_index + 1
)
})?;
let kind = self.parse_json_param(target_index, val)?;
self.bind_param(stmt, param_index, &kind, &mut buffers)?;
Ok(buffers)
},
)

Comment on lines 554 to 589
let sql_cstr = CString::new(sql).map_err(|e| format!("Invalid SQL string: {e}"))?;
let ptr = sql_cstr.as_ptr();

let (stmt_opt, tail) = self.prepare_one(ptr)?;

if let Some(stmt) = stmt_opt {
let mut stmt_guard = StmtGuard::new(stmt);

if !Self::is_trivia_tail_only(tail) {
return Err("Parameterized queries must contain a single statement.".to_string());
}

let param_count = unsafe { sqlite3_bind_parameter_count(stmt) } as usize;
if param_count == 0 {
if !params.is_empty() {
return Err(format!(
"No parameters expected but {} provided.",
params.len()
));
}
return self.exec_prepared_statement(stmt_guard.take());
}

let _buffers_guard = self.bind_params_for_stmt(stmt, &params)?;
self.exec_prepared_statement(stmt_guard.take())
} else if Self::is_trivia_tail_only(tail) {
if !params.is_empty() {
return Err(format!(
"No parameters expected but {} provided.",
params.len()
));
}
Ok((None, 0))
} else {
Err("Parameterized queries must contain a single statement.".to_string())
}
Copy link

Choose a reason for hiding this comment

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

Suggested change
let sql_cstr = CString::new(sql).map_err(|e| format!("Invalid SQL string: {e}"))?;
let ptr = sql_cstr.as_ptr();
let (stmt_opt, tail) = self.prepare_one(ptr)?;
if let Some(stmt) = stmt_opt {
let mut stmt_guard = StmtGuard::new(stmt);
if !Self::is_trivia_tail_only(tail) {
return Err("Parameterized queries must contain a single statement.".to_string());
}
let param_count = unsafe { sqlite3_bind_parameter_count(stmt) } as usize;
if param_count == 0 {
if !params.is_empty() {
return Err(format!(
"No parameters expected but {} provided.",
params.len()
));
}
return self.exec_prepared_statement(stmt_guard.take());
}
let _buffers_guard = self.bind_params_for_stmt(stmt, &params)?;
self.exec_prepared_statement(stmt_guard.take())
} else if Self::is_trivia_tail_only(tail) {
if !params.is_empty() {
return Err(format!(
"No parameters expected but {} provided.",
params.len()
));
}
Ok((None, 0))
} else {
Err("Parameterized queries must contain a single statement.".to_string())
}
let sql_cstr = CString::new(sql).map_err(|e| format!("Invalid SQL string: {e}"))?;
let ptr = sql_cstr.as_ptr();
let (stmt_opt, tail) = self.prepare_one(ptr)?;
let Some(stmt) = stmt_opt else {
if !Self::is_trivia_tail_only(tail) {
return Err("Parameterized queries must contain a single statement.".to_string());
}
if !params.is_empty() {
return Err(format!(
"No parameters expected but {params_len} provided.",
params_len = params.len()
));
}
return Ok((None, 0));
};
let mut stmt_guard = StmtGuard::new(stmt);
if !Self::is_trivia_tail_only(tail) {
return Err("Parameterized queries must contain a single statement.".to_string());
}
let param_count = unsafe { sqlite3_bind_parameter_count(stmt) } as usize;
if param_count == 0 {
if !params.is_empty() {
return Err(format!(
"No parameters expected but {params_len} provided.",
params_len = params.len()
));
}
return self.exec_prepared_statement(stmt_guard.take());
}
let _buffers_guard = self.bind_params_for_stmt(stmt, &params)?;
self.exec_prepared_statement(stmt_guard.take())

Comment on lines 64 to 116
fn handle_incoming_value(data: JsValue) {
match serde_wasm_bindgen::from_value::<WorkerMessage>(data) {
Ok(WorkerMessage::ExecuteQuery { sql, params }) => {
WORKER_STATE.with(|s| {
if let Some(state) = s.borrow().as_ref() {
let state = Rc::clone(state);
spawn_local(async move {
let response =
make_query_result_message(state.execute_query(sql, params).await)
.unwrap_or_else(|err| {
let _ = send_worker_error(err);
let o = js_sys::Object::new();
let _ = set_js_property(
&o,
"type",
&JsValue::from_str("query-result"),
);
let _ = set_js_property(&o, "result", &JsValue::NULL);
let _ = set_js_property(
&o,
"error",
&JsValue::from_str("Failed to build response"),
);
o
});
if let Err(err) = post_message(&response) {
if let Err(send_err) = send_worker_error(err) {
throw_val(send_err);
}
}
});
}
});
}
Err(err) => {
let error_text = format!("Invalid worker message: {err:?}");
let response = make_query_result_message(Err(error_text)).unwrap_or_else(|e| {
let _ = send_worker_error(e);
let o = js_sys::Object::new();
let _ = set_js_property(&o, "type", &JsValue::from_str("query-result"));
let _ = set_js_property(&o, "result", &JsValue::NULL);
let _ =
set_js_property(&o, "error", &JsValue::from_str("Failed to build response"));
o
});
if let Err(err) = post_message(&response) {
if let Err(send_err) = send_worker_error(err) {
throw_val(send_err);
}
}
}
}
}
Copy link

Choose a reason for hiding this comment

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

Suggested change
fn handle_incoming_value(data: JsValue) {
match serde_wasm_bindgen::from_value::<WorkerMessage>(data) {
Ok(WorkerMessage::ExecuteQuery { sql, params }) => {
WORKER_STATE.with(|s| {
if let Some(state) = s.borrow().as_ref() {
let state = Rc::clone(state);
spawn_local(async move {
let response =
make_query_result_message(state.execute_query(sql, params).await)
.unwrap_or_else(|err| {
let _ = send_worker_error(err);
let o = js_sys::Object::new();
let _ = set_js_property(
&o,
"type",
&JsValue::from_str("query-result"),
);
let _ = set_js_property(&o, "result", &JsValue::NULL);
let _ = set_js_property(
&o,
"error",
&JsValue::from_str("Failed to build response"),
);
o
});
if let Err(err) = post_message(&response) {
if let Err(send_err) = send_worker_error(err) {
throw_val(send_err);
}
}
});
}
});
}
Err(err) => {
let error_text = format!("Invalid worker message: {err:?}");
let response = make_query_result_message(Err(error_text)).unwrap_or_else(|e| {
let _ = send_worker_error(e);
let o = js_sys::Object::new();
let _ = set_js_property(&o, "type", &JsValue::from_str("query-result"));
let _ = set_js_property(&o, "result", &JsValue::NULL);
let _ =
set_js_property(&o, "error", &JsValue::from_str("Failed to build response"));
o
});
if let Err(err) = post_message(&response) {
if let Err(send_err) = send_worker_error(err) {
throw_val(send_err);
}
}
}
}
}
fn handle_incoming_value(data: JsValue) {
let make_error_response = || {
let o = js_sys::Object::new();
let _ = set_js_property(&o, "type", &JsValue::from_str("query-result"));
let _ = set_js_property(&o, "result", &JsValue::NULL);
let _ = set_js_property(&o, "error", &JsValue::from_str("Failed to build response"));
o
};
let send_response = |response: &js_sys::Object| {
post_message(response)
.or_else(|err| send_worker_error(err).map_err(throw_val))
.ok();
};
match serde_wasm_bindgen::from_value::<WorkerMessage>(data) {
Ok(WorkerMessage::ExecuteQuery { sql, params }) => {
WORKER_STATE.with(|s| {
let Some(state) = s.borrow().as_ref().map(Rc::clone) else {
return;
};
spawn_local(async move {
let result = state.execute_query(sql, params).await;
let response = make_query_result_message(result).unwrap_or_else(|err| {
let _ = send_worker_error(err);
make_error_response()
});
send_response(&response);
});
});
}
Err(err) => {
let error_text = format!("Invalid worker message: {err:?}");
let response = make_query_result_message(Err(error_text)).unwrap_or_else(|e| {
let _ = send_worker_error(e);
make_error_response()
});
send_response(&response);
}
}
}

Comment on lines 61 to 95
fn handle_query_result_message(
data: &JsValue,
pending_queries: &Rc<RefCell<VecDeque<(js_sys::Function, js_sys::Function)>>>,
) {
if let Ok(obj) = js_sys::Reflect::get(data, &JsValue::from_str("type")) {
if let Some(msg_type) = obj.as_string() {
if msg_type == "query-result" {
if let Some((resolve, reject)) = pending_queries.borrow_mut().pop_front() {
if let Ok(error) = js_sys::Reflect::get(data, &JsValue::from_str("error")) {
if !error.is_null() && !error.is_undefined() {
let error_str =
error.as_string().unwrap_or_else(|| format!("{error:?}"));
if let Err(_err) =
reject.call1(&JsValue::NULL, &JsValue::from_str(&error_str))
{
}
return;
}
}

if let Ok(result) = js_sys::Reflect::get(data, &JsValue::from_str("result")) {
if !result.is_null() && !result.is_undefined() {
let result_str =
result.as_string().unwrap_or_else(|| format!("{result:?}"));
if let Err(_err) =
resolve.call1(&JsValue::NULL, &JsValue::from_str(&result_str))
{
}
}
}
}
}
}
}
}
Copy link

Choose a reason for hiding this comment

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

Suggested change
fn handle_query_result_message(
data: &JsValue,
pending_queries: &Rc<RefCell<VecDeque<(js_sys::Function, js_sys::Function)>>>,
) {
if let Ok(obj) = js_sys::Reflect::get(data, &JsValue::from_str("type")) {
if let Some(msg_type) = obj.as_string() {
if msg_type == "query-result" {
if let Some((resolve, reject)) = pending_queries.borrow_mut().pop_front() {
if let Ok(error) = js_sys::Reflect::get(data, &JsValue::from_str("error")) {
if !error.is_null() && !error.is_undefined() {
let error_str =
error.as_string().unwrap_or_else(|| format!("{error:?}"));
if let Err(_err) =
reject.call1(&JsValue::NULL, &JsValue::from_str(&error_str))
{
}
return;
}
}
if let Ok(result) = js_sys::Reflect::get(data, &JsValue::from_str("result")) {
if !result.is_null() && !result.is_undefined() {
let result_str =
result.as_string().unwrap_or_else(|| format!("{result:?}"));
if let Err(_err) =
resolve.call1(&JsValue::NULL, &JsValue::from_str(&result_str))
{
}
}
}
}
}
}
}
}
fn handle_query_result_message(
data: &JsValue,
pending_queries: &Rc<RefCell<VecDeque<(js_sys::Function, js_sys::Function)>>>,
) {
let msg_type = js_sys::Reflect::get(data, &JsValue::from_str("type"))
.ok()
.and_then(|obj| obj.as_string());
let Some(msg_type) = msg_type else { return };
if msg_type != "query-result" {
return;
}
let Some((resolve, reject)) = pending_queries.borrow_mut().pop_front() else {
return;
};
let error = js_sys::Reflect::get(data, &JsValue::from_str("error"))
.ok()
.filter(|e| !e.is_null() && !e.is_undefined());
if let Some(error) = error {
let error_str = error.as_string().unwrap_or_else(|| format!("{error:?}"));
let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&error_str));
return;
}
if let Some(result) = js_sys::Reflect::get(data, &JsValue::from_str("result"))
.ok()
.filter(|r| !r.is_null() && !r.is_undefined())
{
let result_str = result.as_string().unwrap_or_else(|| format!("{result:?}"));
let _ = resolve.call1(&JsValue::NULL, &JsValue::from_str(&result_str));
}
}

Comment on lines 268 to 278
let arr = Self::ensure_array(params)?;

let normalized = js_sys::Array::new();
let len = arr.length();
for i in 0..len {
let v = arr.get(i);
let nv = normalize_one_param(&v, i)?;
normalized.push(&nv);
}

Ok(normalized)
Copy link

Choose a reason for hiding this comment

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

Suggested change
let arr = Self::ensure_array(params)?;
let normalized = js_sys::Array::new();
let len = arr.length();
for i in 0..len {
let v = arr.get(i);
let nv = normalize_one_param(&v, i)?;
normalized.push(&nv);
}
Ok(normalized)
let arr = Self::ensure_array(params)?;
(0..arr.length())
.try_fold(js_sys::Array::new(), |normalized, i| {
let nv = normalize_one_param(&arr.get(i), i)?;
normalized.push(&nv);
Ok(normalized)
})

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
packages/sqlite-web-core/src/database.rs (1)

1175-1204: Tests call STANDARD.encode without importing the Engine trait in this module.

Trait methods require the trait in scope per module; use base64::Engine in the tests mod.

 #[cfg(all(test, target_family = "wasm"))]
 mod tests {
     use super::*;
     use serde_json::json;
+    use base64::Engine as _;

If you believe this compiles via use super::*, please point to a pub use base64::Engine in the parent module; otherwise the trait isn’t in scope for this module.

packages/sqlite-web/src/lib.rs (1)

163-167: Track a monotonically increasing requestId on the main thread.

Add a counter and use a HashMap for pending queries.

 #[wasm_bindgen]
 pub struct SQLiteWasmDatabase {
     worker: Worker,
-    pending_queries: Rc<RefCell<VecDeque<(js_sys::Function, js_sys::Function)>>>,
+    pending_queries: Rc<RefCell<HashMap<u32, (js_sys::Function, js_sys::Function)>>>,
+    next_request_id: Rc<RefCell<u32>>,
 }
-        let pending_queries: Rc<RefCell<VecDeque<(js_sys::Function, js_sys::Function)>>> =
-            Rc::new(RefCell::new(VecDeque::new()));
-        install_onmessage_handler(&worker, Rc::clone(&pending_queries));
+        let pending_queries: Rc<RefCell<HashMap<u32, (js_sys::Function, js_sys::Function)>>> =
+            Rc::new(RefCell::new(HashMap::new()));
+        let next_request_id = Rc::new(RefCell::new(1u32));
+        install_onmessage_handler(&worker, Rc::clone(&pending_queries));
@@
         Ok(SQLiteWasmDatabase {
             worker,
-            pending_queries,
+            pending_queries,
+            next_request_id,
         })

Also applies to: 245-265

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7075651 and eafbbd2.

⛔ Files ignored due to path filters (1)
  • svelte-test/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • packages/sqlite-web-core/src/database.rs (11 hunks)
  • packages/sqlite-web-core/src/worker.rs (4 hunks)
  • packages/sqlite-web/src/lib.rs (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
packages/sqlite-web-core/src/worker.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Keep worker.rs as the main worker entry point invoked by worker_main()

Files:

  • packages/sqlite-web-core/src/worker.rs
🧠 Learnings (6)
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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()

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/worker.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-10-20T12:24:07.874Z
Learnt from: findolor
PR: rainlanguage/sqlite-web#19
File: packages/sqlite-web-core/src/database.rs:122-157
Timestamp: 2025-10-20T12:24:07.874Z
Learning: In packages/sqlite-web-core/src/database.rs, named parameters (:name, name, $name) are intentionally not supported for SQL parameter binding. Only plain '?' and numbered '?N' placeholders are supported.

Applied to files:

  • packages/sqlite-web-core/src/database.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
  • packages/sqlite-web/src/lib.rs
📚 Learning: 2025-09-17T06:55:20.178Z
Learnt from: CR
PR: rainlanguage/sqlite-web#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

Applied to files:

  • packages/sqlite-web-core/src/database.rs
🧬 Code graph analysis (1)
packages/sqlite-web-core/src/worker.rs (2)
packages/sqlite-web-core/src/util.rs (2)
  • js_value_to_string (39-60)
  • set_js_property (29-37)
packages/sqlite-web-core/src/coordination.rs (2)
  • new (44-78)
  • setup_channel_listener (80-97)
🔇 Additional comments (3)
packages/sqlite-web-core/src/database.rs (2)

80-104: Good: finalize stmt on prepare error.

Avoids leaking prepared statements/locks on prepare failure.


544-580: exec_single_statement_with_params flow is correct and leak-safe.

  • Single-statement enforcement via tail scan.
  • Proper guards (StmtGuard + BoundBuffers lifetime).
  • Clear errors when params provided but no placeholders.
packages/sqlite-web/src/lib.rs (1)

16-28: LGTM: Blob URL is revoked on both success and failure paths.

Avoids object URL leaks during worker creation.

Comment on lines +171 to +240
fn detect_placeholder_mode(&self, stmt: *mut sqlite3_stmt) -> Result<PlaceholderMode, String> {
let param_count = unsafe { sqlite3_bind_parameter_count(stmt) } as usize;

#[derive(Default)]
struct ParamAnalysis {
has_plain: bool,
has_numbered: bool,
has_named: bool,
numbered_max: usize,
numbered_present: std::collections::BTreeSet<usize>,
}

let ParamAnalysis {
has_plain,
has_numbered,
has_named,
numbered_max,
numbered_present,
} = (1..=param_count as i32).try_fold(ParamAnalysis::default(), |mut acc, i| {
let name_ptr = unsafe { sqlite3_bind_parameter_name(stmt, i) };
if name_ptr.is_null() {
acc.has_plain = true;
return Ok(acc);
}

let name = unsafe { CStr::from_ptr(name_ptr) }.to_string_lossy();
let s = name.as_ref();

match s.strip_prefix('?') {
Some("") => {
acc.has_plain = true;
}
Some(digits) => {
let n = digits
.parse::<isize>()
.map_err(|_| format!("Invalid parameter index: {}", s))?;
if n <= 0 {
return Err(
"Invalid parameter index: ?0 or negative indices are not allowed."
.to_string(),
);
}
acc.has_numbered = true;
let n_u = n as usize;
acc.numbered_present.insert(n_u);
acc.numbered_max = acc.numbered_max.max(n_u);
}
None => {
acc.has_named = true;
}
}
Ok(acc)
})?;

if has_named {
return Err("Named parameters not supported.".to_string());
}
if has_plain && has_numbered {
return Err("Mixing '?' and '?N' placeholders is not supported.".to_string());
}

if has_plain {
Ok(PlaceholderMode::Plain { count: param_count })
} else {
Ok(PlaceholderMode::Numbered {
max: numbered_max,
present: numbered_present,
})
}
}
Copy link

@coderabbitai coderabbitai bot Oct 28, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Placeholder detection/validation/mapping are consistent and safe.

  • Disallows mixing '?' and '?N'.
  • Rejects named params (as intended).
  • Builds a stable param_map for binding.

Add a brief doc comment on detect_placeholder_mode clarifying named params are intentionally unsupported per API.

Also applies to: 242-273, 275-312

🤖 Prompt for AI Agents
In packages/sqlite-web-core/src/database.rs around lines 171 to 240 (also apply
same change to 242-273 and 275-312), add a brief doc comment above
detect_placeholder_mode explaining that the function detects placeholder modes,
rejects named parameters intentionally (named params are not supported by this
API), disallows mixing plain '?' and numbered '?N' placeholders, and returns
either Plain { count } or Numbered { max, present }; update the comment to state
this behavior and the rationale that named params are intentionally unsupported
so callers understand the validation is deliberate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not needed

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! I'll skip the doc comment addition.


🧠 Learnings used
Learnt from: findolor
PR: rainlanguage/sqlite-web#19
File: packages/sqlite-web-core/src/database.rs:122-157
Timestamp: 2025-10-20T12:24:07.874Z
Learning: In packages/sqlite-web-core/src/database.rs, named parameters (:name, name, $name) are intentionally not supported for SQL parameter binding. Only plain '?' and numbered '?N' placeholders are supported.

@findolor findolor requested a review from 0xgleb October 28, 2025 06:38
@findolor findolor merged commit 6a3c150 into main Oct 29, 2025
3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Nov 10, 2025
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants