Skip to content

Conversation

@savannahar68
Copy link

Summary

  • Fixed Bun.SQL's unsafe() method to support named parameters with objects
  • Previously only array-based positional parameters worked
  • Now supports named parameters with :, $, or @ prefixes in both strict and non-strict modes

Details

The issue was in src/js/internal/sql/sqlite.ts where SQLiteQueryHandle was using $apply() to pass parameters, which incorrectly wrapped objects in arrays instead of passing them directly to the underlying bun:sqlite statement methods.

Changes:

  1. Updated type signatures to accept Record<string, unknown> in addition to arrays
  2. Added type checks to detect objects vs arrays
  3. Pass objects directly without using $apply() wrapper
  4. Pass arrays using $apply() as before (maintains backward compatibility)

Testing:

  • All 232 existing tests pass
  • Added 3 new tests covering:
    • Named parameters with strict: true mode (standard behavior)
    • Named parameters with different prefixes (:, $, @)
    • Named parameters with strict: false mode (requires prefix in keys)

Test plan

bun bd test test/js/sql/sqlite-sql.test.ts

Fixes #22799

Fixes issue where named parameters with :, $, or @ prefixes were not
working in Bun.SQL's unsafe() method. The method was using $apply()
which wrapped objects in arrays instead of passing them directly to
the underlying bun:sqlite statement methods.

Changes:
- Updated SQLiteQueryHandle to accept objects in addition to arrays
- Fixed parameter passing to use direct calls for objects
- Added tests for named parameters with strict and non-strict modes

Fixes oven-sh#22799
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 8, 2025

Walkthrough

Adds support for object-based named-parameter binding for SQLite by widening internal value types to accept arrays or maps, updating the SQLiteQueryHandle and createQueryHandle signatures, and branching execution paths for SELECT vs non-SELECT queries. Adds tests covering named-parameter binding across strict/non-strict modes and different prefixes.

Changes

Cohort / File(s) Summary
SQLite adapter implementation
src/js/internal/sql/sqlite.ts
Widened parameter type to `unknown[]
SQLite named-parameter tests
test/js/sql/sqlite-sql.test.ts
Added tests for named-parameter binding in strict and non-strict modes, covering :, $, and @ prefixes and multiple insertion/selection paths; duplicated blocks to ensure coverage of different insertion paths.

Suggested reviewers

  • alii

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description uses "## Summary" and "## Details" headings instead of the required "### What does this PR do?" and "### How did you verify your code works?" sections, so it does not adhere to the repository's description template. Please update the description to include the exact template headings "### What does this PR do?" and "### How did you verify your code works?" and ensure each section is populated with the corresponding information.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately summarizes the primary change by stating support for named parameters in Bun.SQL’s unsafe method, which directly aligns with the implemented modifications to SQLiteQueryHandle.
Linked Issues Check ✅ Passed The changes in sqlite.ts and createQueryHandle fully implement support for named parameter objects in Bun.SQL.unsafe() for SQLite, and the added tests verify this behavior under strict and non‐strict modes, fulfilling all objectives of issue #22799.
Out of Scope Changes Check ✅ Passed All modifications and new tests are focused on enabling named parameter support in SQLite and no unrelated code paths or features outside the linked issue’s scope have been altered.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ae70349 and ced0b64.

📒 Files selected for processing (1)
  • src/js/internal/sql/sqlite.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/js/internal/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place internal modules not exposed to users under src/js/internal/

Files:

  • src/js/internal/sql/sqlite.ts
src/js/internal/**/*.{ts,js}

📄 CodeRabbit inference engine (src/js/CLAUDE.md)

Place internal-only modules under internal/

Files:

  • src/js/internal/sql/sqlite.ts
src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js}

📄 CodeRabbit inference engine (src/js/CLAUDE.md)

src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js}: Use require() with string literals only (no dynamic requires)
Do not use ESM import syntax; write modules as CommonJS with export default { ... }
Export via export default {} for modules
Use .$call and .$apply; never use .call or .apply
Prefer JSC intrinsics/private $ APIs for performance (e.g., $Array.from, map.$set, $newArrayWithSize, $debug, $assert)
Validate callbacks with $isCallable and throw $ERR_INVALID_ARG_TYPE with the correct parameter name and expected type
Use process.platform and process.arch for platform detection (rely on inlining/dead-code elimination)

Files:

  • src/js/internal/sql/sqlite.ts
🧬 Code graph analysis (1)
src/js/internal/sql/sqlite.ts (2)
src/js/bun/sqlite.ts (1)
  • values (201-203)
src/js/internal/sql/query.ts (1)
  • values (262-271)
🔇 Additional comments (5)
src/js/internal/sql/sqlite.ts (5)

213-213: LGTM! Type widening enables named parameter support.

The type signature correctly accepts both arrays (for positional parameters) and objects (for named parameters).


216-216: LGTM! Constructor signature aligns with type changes.

The constructor properly accepts both array and object parameter types.


246-250: LGTM! SELECT query handling correctly supports both parameter types.

The conditional logic properly routes:

  • Arrays to .$apply (spreads elements as positional arguments)
  • Objects to .$call (passes object for named binding)

This aligns with bun:sqlite prepared statement conventions and follows the coding guidelines. Based on past review comments, the use of .$call for object branches has been correctly applied.


262-262: LGTM! Non-SELECT query handling correctly supports both parameter types.

The conditional logic properly routes:

  • Arrays to .$apply with SQL prepended (spreads positional arguments)
  • Objects to .$call (passes SQL and named parameters separately)

This follows the coding guidelines and matches bun:sqlite db.run conventions. Based on past review comments, the use of .$call for the object branch has been correctly applied.


349-353: LGTM! Signature and null handling are correct.

The method signature properly accepts both parameter types, and the nullish coalescing (values ?? []) safely converts null/undefined to an empty array for backward compatibility. Empty objects ({}) are correctly handled by the branching logic in the run method.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@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

Caution

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

⚠️ Outside diff range comments (1)
src/js/internal/sql/sqlite.ts (1)

242-260: Always finalize prepared statements (move finalize into a finally block)

If any call throws before Line 258, stmt.finalize() is skipped, leaking a prepared statement. Wrap the SELECT branch in try/finally.

Apply this diff:

-        const stmt = db.prepare(sql);
-        let result: unknown[] | undefined;
-
-        if (mode === SQLQueryResultMode.values) {
-          result = $isArray(values) ? stmt.values.$apply(stmt, values) : stmt.values(values);
-        } else if (mode === SQLQueryResultMode.raw) {
-          result = $isArray(values) ? stmt.raw.$apply(stmt, values) : stmt.raw(values);
-        } else {
-          result = $isArray(values) ? stmt.all.$apply(stmt, values) : stmt.all(values);
-        }
-
-        const sqlResult = $isArray(result) ? new SQLResultArray(result) : new SQLResultArray([result]);
-
-        sqlResult.command = commandToString(command, parsedInfo.lastToken);
-        sqlResult.count = $isArray(result) ? result.length : 1;
-
-        stmt.finalize();
-        query.resolve(sqlResult);
+        const stmt = db.prepare(sql);
+        try {
+          let result: unknown[] | undefined;
+
+          if (mode === SQLQueryResultMode.values) {
+            result = $isArray(values) ? stmt.values.$apply(stmt, values) : stmt.values.$call(stmt, values);
+          } else if (mode === SQLQueryResultMode.raw) {
+            result = $isArray(values) ? stmt.raw.$apply(stmt, values) : stmt.raw.$call(stmt, values);
+          } else {
+            result = $isArray(values) ? stmt.all.$apply(stmt, values) : stmt.all.$call(stmt, values);
+          }
+
+          const sqlResult = $isArray(result) ? new SQLResultArray(result) : new SQLResultArray([result]);
+          sqlResult.command = commandToString(command, parsedInfo.lastToken);
+          sqlResult.count = $isArray(result) ? result.length : 1;
+          query.resolve(sqlResult);
+        } finally {
+          stmt.finalize();
+        }

As per coding guidelines

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 93910f3 and 424cded.

📒 Files selected for processing (2)
  • src/js/internal/sql/sqlite.ts (4 hunks)
  • test/js/sql/sqlite-sql.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/sql/sqlite-sql.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/sql/sqlite-sql.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/sql/sqlite-sql.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Name test files *.test.ts and use bun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; use port: 0 to get a random port
When spawning Bun in tests, use bunExe() and bunEnv from harness
Prefer async/await in tests; for a single callback, use Promise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
Use tempDir/tempDirWithFiles from harness for temporary files and directories in tests
For large/repetitive strings in tests, prefer Buffer.alloc(count, fill).toString() over "A".repeat(count)
Import common test utilities from harness (e.g., bunExe, bunEnv, tempDirWithFiles, tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and use toThrow for synchronous errors
Use describe blocks for grouping, describe.each for parameterized tests, snapshots with toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach); track resources for cleanup in afterEach
Use using/await using with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Files:

  • test/js/sql/sqlite-sql.test.ts
test/js/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests for specific features under test/js/ by module

Files:

  • test/js/sql/sqlite-sql.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must live under test/ and end with .test.ts or .test.tsx
In tests, always use port: 0; do not hardcode ports or roll your own random port
Prefer normalizeBunSnapshot for snapshotting test output instead of asserting raw strings
Do not write tests that assert absence of crashes (e.g., 'no panic' or 'no uncaught exception')
Use Bun’s Jest-compatible runner (import { test, expect } from "bun:test") for tests
Avoid shell commands like find or grep in tests; use Bun’s Glob and built-in tools instead
Prefer running tests via bun bd test and use provided harness utilities (bunEnv, bunExe, tempDir)
Use Bun.spawn with proper stdio handling and await proc.exited in process-spawning tests

Files:

  • test/js/sql/sqlite-sql.test.ts
src/js/internal/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place internal modules not exposed to users under src/js/internal/

Files:

  • src/js/internal/sql/sqlite.ts
src/js/internal/**/*.{ts,js}

📄 CodeRabbit inference engine (src/js/CLAUDE.md)

Place internal-only modules under internal/

Files:

  • src/js/internal/sql/sqlite.ts
src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js}

📄 CodeRabbit inference engine (src/js/CLAUDE.md)

src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js}: Use require() with string literals only (no dynamic requires)
Do not use ESM import syntax; write modules as CommonJS with export default { ... }
Export via export default {} for modules
Use .$call and .$apply; never use .call or .apply
Prefer JSC intrinsics/private $ APIs for performance (e.g., $Array.from, map.$set, $newArrayWithSize, $debug, $assert)
Validate callbacks with $isCallable and throw $ERR_INVALID_ARG_TYPE with the correct parameter name and expected type
Use process.platform and process.arch for platform detection (rely on inlining/dead-code elimination)

Files:

  • src/js/internal/sql/sqlite.ts
🧬 Code graph analysis (2)
test/js/sql/sqlite-sql.test.ts (1)
src/js/internal-for-testing.ts (1)
  • SQL (17-17)
src/js/internal/sql/sqlite.ts (2)
src/js/bun/sqlite.ts (1)
  • values (201-203)
src/js/internal/sql/query.ts (1)
  • values (262-271)
🔇 Additional comments (6)
src/js/internal/sql/sqlite.ts (3)

212-219: Type-widening to support named params looks good

Accepting unknown[] | Record<string, unknown> and threading it through the handle is correct for named bindings. No issues.


349-353: API surface update is sensible; minor nit on default + nullish-coalescing

Signature accepts unknown[] | Record<string, unknown> | undefined | null with default [] and then coalesces to [] again. This is fine (it preserves null→[] behavior), just noting the redundancy.

Please confirm other adapters that call createQueryHandle won’t rely on null vs [] distinction (should be none for SQLite).


246-251: Use .$call for object-binding paths to follow intrinsics guideline and avoid slow paths

Direct calls (stmt.values(obj)/raw(obj)/all(obj)) should use .$call to match how arrays use .$apply and to align with internal calling conventions.

As per coding guidelines

-          result = $isArray(values) ? stmt.values.$apply(stmt, values) : stmt.values(values);
+          result = $isArray(values) ? stmt.values.$apply(stmt, values) : stmt.values.$call(stmt, values);
-          result = $isArray(values) ? stmt.raw.$apply(stmt, values) : stmt.raw(values);
+          result = $isArray(values) ? stmt.raw.$apply(stmt, values) : stmt.raw.$call(stmt, values);
-          result = $isArray(values) ? stmt.all.$apply(stmt, values) : stmt.all(values);
+          result = $isArray(values) ? stmt.all.$apply(stmt, values) : stmt.all.$call(stmt, values);
⛔ Skipped due to learnings
Learnt from: CR
PR: oven-sh/bun#0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-10-04T09:52:49.414Z
Learning: Applies to src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js} : Use .$call and .$apply; never use .call or .apply
test/js/sql/sqlite-sql.test.ts (3)

1359-1376: Good coverage: strict mode named parameters

Validates insert and select with object bindings; closes the DB. Looks solid.


1378-1391: Prefix variations well covered

Verifies :, $, @ prefixes; order-agnostic assertion via sort is good.


1392-1407: Non‑strict mode behavior captured

Requiring prefixes in keys when strict is false is reflected here; assertions are clear.

Consider adding one small test exercising named bindings with .values() and .raw() result modes to cover those branches too (e.g., SELECT with :id returning .values() and .raw()).

Copy link
Contributor

@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 (1)
src/js/internal/sql/sqlite.ts (1)

262-264: Use .$call for object branch to follow fast-call conventions.

For consistency with the array branch and to follow coding guidelines, use db.run.$call(db, sql, values) for object parameter branch in non-SELECT query handling.

As per coding guidelines

Apply this diff:

-        const changes = $isArray(values) 
-          ? db.run.$apply(db, [sql].concat(values)) 
-          : db.run(sql, values);
+        const changes = $isArray(values)
+          ? db.run.$apply(db, [sql].concat(values))
+          : db.run.$call(db, sql, values);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 424cded and ae70349.

📒 Files selected for processing (1)
  • src/js/internal/sql/sqlite.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/js/internal/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place internal modules not exposed to users under src/js/internal/

Files:

  • src/js/internal/sql/sqlite.ts
src/js/internal/**/*.{ts,js}

📄 CodeRabbit inference engine (src/js/CLAUDE.md)

Place internal-only modules under internal/

Files:

  • src/js/internal/sql/sqlite.ts
src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js}

📄 CodeRabbit inference engine (src/js/CLAUDE.md)

src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js}: Use require() with string literals only (no dynamic requires)
Do not use ESM import syntax; write modules as CommonJS with export default { ... }
Export via export default {} for modules
Use .$call and .$apply; never use .call or .apply
Prefer JSC intrinsics/private $ APIs for performance (e.g., $Array.from, map.$set, $newArrayWithSize, $debug, $assert)
Validate callbacks with $isCallable and throw $ERR_INVALID_ARG_TYPE with the correct parameter name and expected type
Use process.platform and process.arch for platform detection (rely on inlining/dead-code elimination)

Files:

  • src/js/internal/sql/sqlite.ts
🧬 Code graph analysis (1)
src/js/internal/sql/sqlite.ts (2)
src/js/bun/sqlite.ts (1)
  • values (201-203)
src/js/internal/sql/query.ts (1)
  • values (262-271)
🔇 Additional comments (2)
src/js/internal/sql/sqlite.ts (2)

213-213: LGTM! Type changes correctly enable object parameter support.

The field and constructor signature changes properly widen the type from unknown[] to unknown[] | Record<string, unknown>, allowing SQLite queries to accept both positional parameters (arrays) and named parameters (objects).

Also applies to: 216-216


351-356: LGTM! Method signature correctly handles object parameters and null values.

The createQueryHandle signature properly:

  1. Accepts unknown[] | Record<string, unknown> | undefined | null to support both parameter types
  2. Defaults to [] when no argument is provided
  3. Uses the nullish coalescing operator (??) to handle explicit undefined or null values

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.

Support named parameters in Bun.SQL

1 participant