Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

[Miniflare 2] Fix binding of ?N parameters in D1 prepared statements #544

Merged
merged 1 commit into from
Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions packages/d1/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import path from "path";
import { performance } from "perf_hooks";
import { Request, RequestInfo, RequestInit, Response } from "@miniflare/core";
import type { SqliteDB } from "@miniflare/shared";
import type { Statement as SqliteStatement } from "better-sqlite3";
import splitSqlQuery from "./splitter";

// query
Expand Down Expand Up @@ -98,19 +97,37 @@ const EXECUTE_RETURNS_DATA_MESSAGE =
export class D1DatabaseAPI {
constructor(private readonly db: SqliteDB) {}

#query: QueryRunner = (query) => {
const meta: OkMeta = { start: performance.now() };
#prepareAndBind(query: SingleQuery) {
// D1 only respects the first statement
const sql = splitSqlQuery(query.sql)[0];
const stmt = this.db.prepare(sql);
const params = normaliseParams(query.params);
if (params.length === 0) return stmt;

try {
return stmt.bind(params);
} catch (e) {
// For statements using ?1, ?2, etc, we want to pass them as an array but
// `better-sqlite3` expects an object with the shape:
// `{ 1: params[0], 2: params[1], ... }`. Try bind like that instead.
try {
return stmt.bind(Object.fromEntries(params.map((v, i) => [i + 1, v])));
} catch {}
// If that still failed, re-throw the original error
throw e;
}
}

#query: QueryRunner = (query) => {
const meta: OkMeta = { start: performance.now() };
const stmt = this.#prepareAndBind(query);
let results: any[];
if (stmt.reader) {
results = stmt.all(params);
results = stmt.all();
} else {
// `/query` does support queries that don't return data,
// returning `[]` instead of `null`
const result = stmt.run(params);
const result = stmt.run();
results = [];
meta.last_row_id = Number(result.lastInsertRowid);
meta.changes = result.changes;
Expand All @@ -120,13 +137,10 @@ export class D1DatabaseAPI {

#execute: QueryRunner = (query) => {
const meta: OkMeta = { start: performance.now() };
// D1 only respects the first statement
const sql = splitSqlQuery(query.sql)[0];
const stmt = this.db.prepare(sql);
const stmt = this.#prepareAndBind(query);
// `/execute` only supports queries that don't return data
if (stmt.reader) throw new Error(EXECUTE_RETURNS_DATA_MESSAGE);
const params = normaliseParams(query.params);
const result = stmt.run(params);
const result = stmt.run();
meta.last_row_id = Number(result.lastInsertRowid);
meta.changes = result.changes;
return ok(null, meta);
Expand Down
12 changes: 12 additions & 0 deletions packages/d1/test/d1js.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,18 @@ test("D1PreparedStatement: bind", async (t) => {
.bind("green")
.all<ColourRow>();
t.is(colourResults.results?.length, 1);

// Check with numbered parameters (execute and query)
// https://github.com/cloudflare/miniflare/issues/504
await db
.prepare("INSERT INTO colours (id, name, rgb) VALUES (?3, ?1, ?2)")
.bind("yellow", 0xffff00, 4)
.run();
const colourResult = await db
.prepare("SELECT * FROM colours WHERE id = ?1")
.bind(4)
.first<ColourRow>();
t.deepEqual(colourResult, { id: 4, name: "yellow", rgb: 0xffff00 });
});

// Lots of strange edge cases here...
Expand Down