Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 10 additions & 7 deletions src/js/internal/sql/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,10 @@ class SQLiteQueryHandle implements BaseQueryHandle<BunSQLiteModule.Database> {
private mode = SQLQueryResultMode.objects;

private readonly sql: string;
private readonly values: unknown[];
private readonly values: unknown[] | Record<string, unknown>;
private readonly parsedInfo: SQLParsedInfo;

public constructor(sql: string, values: unknown[]) {
public constructor(sql: string, values: unknown[] | Record<string, unknown>) {
this.sql = sql;
this.values = values;
// Parse the SQL query once when creating the handle
Expand Down Expand Up @@ -243,11 +243,11 @@ class SQLiteQueryHandle implements BaseQueryHandle<BunSQLiteModule.Database> {
let result: unknown[] | undefined;

if (mode === SQLQueryResultMode.values) {
result = stmt.values.$apply(stmt, values);
result = $isArray(values) ? stmt.values.$apply(stmt, values) : stmt.values(values);
} else if (mode === SQLQueryResultMode.raw) {
result = stmt.raw.$apply(stmt, values);
result = $isArray(values) ? stmt.raw.$apply(stmt, values) : stmt.raw(values);
} else {
result = stmt.all.$apply(stmt, values);
result = $isArray(values) ? stmt.all.$apply(stmt, values) : stmt.all(values);
}

const sqlResult = $isArray(result) ? new SQLResultArray(result) : new SQLResultArray([result]);
Expand All @@ -259,7 +259,7 @@ class SQLiteQueryHandle implements BaseQueryHandle<BunSQLiteModule.Database> {
query.resolve(sqlResult);
} else {
// For INSERT/UPDATE/DELETE/CREATE etc., use db.run() which handles multiple statements natively
const changes = db.run.$apply(db, [sql].concat(values));
const changes = $isArray(values) ? db.run.$apply(db, [sql].concat(values)) : db.run(sql, values);
const sqlResult = new SQLResultArray();

sqlResult.command = commandToString(command, parsedInfo.lastToken);
Expand Down Expand Up @@ -346,7 +346,10 @@ class SQLiteAdapter implements DatabaseAdapter<BunSQLiteModule.Database, BunSQLi
}
}

createQueryHandle(sql: string, values: unknown[] | undefined | null = []): SQLiteQueryHandle {
createQueryHandle(
sql: string,
values: unknown[] | Record<string, unknown> | undefined | null = [],
): SQLiteQueryHandle {
return new SQLiteQueryHandle(sql, values ?? []);
}
escapeIdentifier(str: string) {
Expand Down
49 changes: 49 additions & 0 deletions test/js/sql/sqlite-sql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,55 @@ describe("SQL helpers", () => {
expect(results[0].value).toBe("test");
});

test("unsafe with named parameters (strict mode)", async () => {
const strictSql = new SQL({ adapter: "sqlite", filename: ":memory:", strict: true });
await strictSql`CREATE TABLE named_test (id INTEGER, name TEXT, age INTEGER)`;

await strictSql.unsafe("INSERT INTO named_test VALUES (:id, :name, :age)", {
id: 1,
name: "Alice",
age: 30,
});

const results = await strictSql.unsafe("SELECT * FROM named_test WHERE name = :name", { name: "Alice" });
expect(results).toHaveLength(1);
expect(results[0].id).toBe(1);
expect(results[0].name).toBe("Alice");
expect(results[0].age).toBe(30);

await strictSql.close();
});

test("unsafe with named parameters using different prefixes", async () => {
const strictSql = new SQL({ adapter: "sqlite", filename: ":memory:", strict: true });
await strictSql`CREATE TABLE prefix_test (col TEXT)`;
await strictSql.unsafe("INSERT INTO prefix_test VALUES (:value)", { value: "colon" });
await strictSql.unsafe("INSERT INTO prefix_test VALUES ($value)", { value: "dollar" });
await strictSql.unsafe("INSERT INTO prefix_test VALUES (@value)", { value: "at" });

const allResults = await strictSql.unsafe("SELECT * FROM prefix_test");
expect(allResults).toHaveLength(3);
expect(allResults.map(r => r.col).sort()).toEqual(["at", "colon", "dollar"]);

await strictSql.close();
});

test("unsafe with named parameters without strict mode (requires prefix in keys)", async () => {
const defaultSql = new SQL({ adapter: "sqlite", filename: ":memory:" });
await defaultSql`CREATE TABLE default_test (id INTEGER, name TEXT)`;

await defaultSql.unsafe("INSERT INTO default_test VALUES (:id, :name)", { ":id": 1, ":name": "Bob" });

const results = await defaultSql.unsafe("SELECT * FROM default_test WHERE id = :id_param", {
":id_param": 1,
});
expect(results).toHaveLength(1);
expect(results[0].id).toBe(1);
expect(results[0].name).toBe("Bob");

await defaultSql.close();
});

test("insert into with select helper using where IN", async () => {
const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", "");
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`;
Expand Down