Skip to content

Conversation

@Tango992
Copy link
Member

Previously, running this code causes segfault:

import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync(':memory:');
const stmt = db.prepare('SELECT 1 AS value');
db.close();
stmt.get(); // segmentation fault (core dumped)

Changes in this PR also allows the https://github.com/nodejs/node/blob/v24.2.0/test/parallel/test-sqlite-statement-sync-columns.js test to pass.

Copilot AI review requested due to automatic review settings November 18, 2025 16:09
@coderabbitai
Copy link

coderabbitai bot commented Nov 18, 2025

Walkthrough

Refactors SQLite prepared statement management by replacing raw C-style pointers with an InnerStatementPtr wrapper (Rc<Cell<Option<*mut sqlite3_stmt>>>) to safely handle statement finalization and prevent use-after-finalize errors. Introduces StatementFinalized error variant and updates all statement operations to check finalization state.

Changes

Cohort / File(s) Change Summary
Statement pointer wrapper refactoring
ext/node/ops/sqlite/statement.rs, ext/node/ops/sqlite/database.rs
Introduces InnerStatementPtr type alias and replaces direct raw pointer management with shared reference semantics. StatementSync and DatabaseSync updated to store wrapped pointers. New methods stmt_ptr() and assert_statement_finalized() provide safe pointer access and finalization checks. Drop implementation reworked to finalize via Rc pointer comparison. All FFI calls updated to operate through the new abstraction with proper error propagation.
Error handling
ext/node/ops/sqlite/mod.rs
Adds StatementFinalized error variant to SqliteError enum and includes it in invalid state error-code mapping alongside AlreadyOpen.
Test coverage
tests/node_compat/config.toml
Adds test entry for parallel/test-sqlite-statement-sync-columns.js.
Finalization validation test
tests/unit_node/sqlite_test.ts
Introduces new test case validating that StatementSync methods and properties uniformly raise StatementFinalized error after database closure.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Stmt as StatementSync
    participant DB as DatabaseSync
    participant FFI as SQLite FFI

    rect rgb(240, 248, 255)
    Note over Stmt,FFI: Normal Operation Flow
    App->>Stmt: Call operation (e.g., step())
    Stmt->>Stmt: stmt_ptr() → get raw ptr from Rc<Cell<>>
    alt Pointer Valid
        Stmt->>FFI: Call sqlite3_step(ptr)
        FFI-->>Stmt: Result
        Stmt-->>App: Success
    else Pointer is None
        Stmt-->>App: Error: StatementFinalized
    end
    end

    rect rgb(255, 240, 240)
    Note over App,FFI: Finalization Flow (on DB close)
    App->>DB: close()
    DB->>DB: Iterate statements vector
    DB->>Stmt: Access InnerStatementPtr
    Stmt->>FFI: sqlite3_finalize(ptr)
    Stmt->>Stmt: set(None) in Rc<Cell<>>
    Note over Stmt: Future access returns StatementFinalized
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • statement.rs requires careful review of the new stmt_ptr() abstraction, Drop implementation changes, and the systematic replacement of all direct pointer accesses with error-handling paths
  • Rc<Cell<>> pattern needs verification for thread-safety implications and correct pointer comparison logic in Drop
  • FFI safety comments and error propagation paths throughout multiple methods should be validated
  • Test coverage in sqlite_test.ts confirms observable behavior changes for use-after-finalize scenarios

Suggested reviewers

  • bartlomieju

Poem

🐰 A statement now wrapped in its cozy Rc,
No more raw pointers running wild and free,
When databases close, we gently decline—
"Statement finalized!" is our kind reply line!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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
Title check ✅ Passed The title accurately describes the main change: fixing a segfault when calling StatementSync methods after connection closure.
Description check ✅ Passed The description is directly related to the changeset, providing a clear example of the segfault problem and explaining how the PR addresses it.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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

Copilot finished reviewing on behalf of Tango992 November 18, 2025 16:11
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes a segmentation fault that occurs when calling StatementSync methods after the database connection has been closed. The fix wraps the raw SQLite statement pointer in Rc<Cell<Option<*mut ffi::sqlite3_stmt>>> to track finalization state.

Key changes:

  • Introduction of InnerStatementPtr type alias to wrap statement pointers with finalization tracking
  • Addition of StatementFinalized error variant for proper error handling when statements are used after finalization
  • Updated all StatementSync methods to check finalization state before accessing the underlying pointer

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
ext/node/ops/sqlite/statement.rs Core logic changes: wraps statement pointer, adds finalization checks to all methods
ext/node/ops/sqlite/database.rs Updates prepare and close methods to work with wrapped statement pointers
ext/node/ops/sqlite/mod.rs Adds StatementFinalized error variant
tests/unit_node/sqlite_test.ts Adds comprehensive test for post-close method calls
tests/node_compat/config.toml Enables Node.js compatibility test for statement columns

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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: 0

🧹 Nitpick comments (4)
ext/node/ops/sqlite/statement.rs (3)

80-101: Drop logic safely avoids double finalization; minor simplification possible

The Drop implementation correctly:

  • Removes this statement’s InnerStatementPtr from the shared statements vec.
  • Finalizes only if the cell still contains Some(ptr).
  • Clears the cell to None afterward so future uses see StatementFinalized.

You could simplify slightly and avoid the temporary finalized_stmt by using Cell::take/replace:

-    let mut finalized_stmt = None;
-
-    if let Some(pos) = statements
-      .iter()
-      .position(|stmt| Rc::ptr_eq(stmt, &self.inner))
-    {
-      let stmt = statements.remove(pos);
-      finalized_stmt = stmt.get();
-      stmt.set(None);
-    }
-
-    if let Some(ptr) = finalized_stmt {
+    if let Some(pos) = statements
+      .iter()
+      .position(|stmt| Rc::ptr_eq(stmt, &self.inner))
+    {
+      let stmt = statements.remove(pos);
+      if let Some(ptr) = stmt.replace(None) {
         // SAFETY: `ptr` is a valid pointer to a sqlite3_stmt.
         unsafe {
           ffi::sqlite3_finalize(ptr);
         }
       }
     }

Purely a readability/ergonomics win; behavior is equivalent.


147-185: Column iterator error handling is functionally correct but slightly lossy

ColumnIterator::new and the iterator implementation now propagate SqliteError via Result, which lets read_row cleanly surface StatementFinalized when the underlying statement is gone. In next, though, any error from column_name is coerced to StatementFinalized:

let name = match self.stmt.column_name(index) {
  Ok(name) => name,
  Err(_) => return Some(Err(SqliteError::StatementFinalized)),
};

Given that column_name currently only fails due to stmt_ptr (i.e. finalized statements), this is correct, but if new error cases are added later you might want to preserve the original error instead of hard‑coding StatementFinalized.


196-211: assert_statement_finalized naming is inverted relative to its behavior

The helper:

fn assert_statement_finalized(&self) -> Result<(), SqliteError> {
  if self.inner.get().is_none() {
    return Err(SqliteError::StatementFinalized);
  }
  Ok(())
}

actually asserts that the statement has not been finalized and is used as such in the setters. The behavior is correct, but the name is misleading and could cause confusion for future changes.

Consider renaming to something like assert_not_finalized or ensure_not_finalized for clarity, and updating call sites accordingly.

Also applies to: 793-824

ext/node/ops/sqlite/database.rs (1)

533-581: prepare() wiring to InnerStatementPtr looks correct; consider future GC behavior

The new prepare() implementation:

  • Prepares raw_stmt as before.
  • Wraps it in Rc<Cell<Option<*mut _>>> and pushes a clone into the shared statements vec.
  • Returns a StatementSync whose inner and statements fields point at the same cell/vec as the database.

This ensures the DB and statements share finalization state and that both close() and StatementSync::Drop can coordinate safely; no issues with the immediate design.

One broader (pre‑existing) lifetime consideration: if the DatabaseSync object is garbage‑collected without an explicit close(), the underlying rusqlite::Connection will be dropped while StatementSync instances may still hold InnerStatementPtr cells containing Some(ptr). That scenario is outside the explicit close() path this PR addresses, but might still permit use‑after‑close if callers keep statements alive longer than the database.

If you want to harden this in the future, a separate follow‑up could:

  • Either implement Drop for DatabaseSync that finalizes any remaining statements and clears their cells, or
  • Make StatementSync hold a strong reference to the connection so the DB outlives all statements.

Not a blocker for this change set, but worth tracking.

Also applies to: 569-571

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c61416 and 0df0f5e.

📒 Files selected for processing (5)
  • ext/node/ops/sqlite/database.rs (4 hunks)
  • ext/node/ops/sqlite/mod.rs (2 hunks)
  • ext/node/ops/sqlite/statement.rs (24 hunks)
  • tests/node_compat/config.toml (1 hunks)
  • tests/unit_node/sqlite_test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
ext/node/ops/sqlite/mod.rs (2)
ext/node/ops/sqlite/validators.rs (1)
  • code (17-22)
ext/node/polyfills/internal/errors.ts (1)
  • ERR_INVALID_STATE (2692-2696)
ext/node/ops/sqlite/statement.rs (1)
ext/node/ops/sqlite/database.rs (1)
  • new (404-442)
tests/unit_node/sqlite_test.ts (1)
tests/unit/test_util.ts (1)
  • assertThrows (19-19)
⏰ 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). (12)
  • GitHub Check: CodeQL analysis (javascript-typescript)
  • GitHub Check: Agent
  • GitHub Check: test debug windows-x86_64
  • GitHub Check: test release linux-x86_64
  • GitHub Check: test debug linux-x86_64
  • GitHub Check: test debug linux-aarch64
  • GitHub Check: test release linux-aarch64
  • GitHub Check: test debug macos-aarch64
  • GitHub Check: bench release linux-x86_64
  • GitHub Check: lint debug linux-x86_64
  • GitHub Check: build libs
  • GitHub Check: lint debug windows-x86_64
🔇 Additional comments (7)
ext/node/ops/sqlite/statement.rs (2)

65-72: Shared InnerStatementPtr design correctly centralizes statement lifetime

Using InnerStatementPtr = Rc<Cell<Option<*mut sqlite3_stmt>>> for both StatementSync.inner and entries in DatabaseSync.statements nicely guarantees a single source of truth for the statement handle and its finalized state. This is exactly the right foundation to prevent use‑after‑finalize across connection close and statement drop paths.


212-246: All statement operations now defensively gate on stmt_ptr()

reset, step, column_count, column_name, column_value, bind_value, bind_params, source_sql, expanded_sql, and columns are all correctly routed through stmt_ptr() so that any use after the connection has closed (and the underlying InnerStatementPtr has been cleared to None) surfaces SqliteError::StatementFinalized instead of touching freed memory. This directly addresses the segfault scenario described in the PR and looks solid.

Also applies to: 248-313, 349-425, 443-559, 826-987

tests/node_compat/config.toml (1)

978-979: Node compat test inclusion looks correct

Adding "parallel/test-sqlite-statement-sync-columns.js" = {} under [tests] is consistent with the existing configuration and ensures the Node 24 columns test runs against the new statement‑finalization logic.

ext/node/ops/sqlite/mod.rs (1)

126-130: New StatementFinalized error integrates cleanly with existing error mapping

The SqliteError::StatementFinalized variant and its code() mapping:

  • Provide a clear, dedicated error for finalized statements with message "statement has been finalized", matching the TS test expectations.
  • Correctly classify it as ERR_INVALID_STATE alongside AlreadyOpen/AlreadyClosed/InUse, which is consistent with Node’s error taxonomy.

No issues from the API or error‑surface perspective.

Also applies to: 178-193

tests/unit_node/sqlite_test.ts (1)

442-463: Post‑close StatementSync test thoroughly exercises the new error path

The new test:

  • Mirrors the reported repro (calling StatementSync methods after db.close()).
  • Verifies a consistent "statement has been finalized" message across read APIs (all, get, iterate, expandedSQL, sourceSQL) and configuration setters.

This is a solid regression test for the segfault fix and should guard future changes to statement lifecycle handling.

ext/node/ops/sqlite/database.rs (2)

33-35: Database tracks statement cells instead of raw pointers in a consistent way

Switching DatabaseSync.statements to Rc<RefCell<Vec<InnerStatementPtr>>> and importing InnerStatementPtr aligns the database’s tracking structure with the new shared pointer abstraction used by StatementSync. This keeps ownership/lifetime information centralized and consistent.

Also applies to: 232-237


488-505: close() correctly finalizes all tracked statements and clears their cells

The revised close():

  • Guards on AlreadyClosed as before.
  • Drains self.statements, finalizing each underlying sqlite3_stmt only if the cell currently holds Some(ptr), then sets the cell to None.

This ensures:

  • No double‑finalization, even if StatementSync::Drop runs later.
  • All live StatementSync instances observe StatementFinalized via stmt_ptr() after db.close().

Behavior matches the PR’s goal and looks safe.

Copy link
Member

@littledivy littledivy left a comment

Choose a reason for hiding this comment

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

LGTM

@Tango992 Tango992 merged commit 3c0f289 into denoland:main Nov 19, 2025
41 of 43 checks passed
@Tango992 Tango992 deleted the fix-sqlite-operation-after-close branch November 19, 2025 01:56
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.

2 participants