Skip to content

Commit

Permalink
feat: support named bind parameters (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
DjDeveloperr authored Jan 4, 2022
1 parent 1231f5b commit 228b18e
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 7 deletions.
64 changes: 58 additions & 6 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export class Database {
* db.execute("create table users (id integer not null, username varchar(20) not null)");
* // Inserts
* db.execute("insert into users (id, username) values(?, ?)", id, username);
* // Insert with named parameters
* db.execute("insert into users (id, username) values(:id, :username)", { id, username });
* // Pragma statements
* db.execute("pragma journal_mode = WAL");
* db.execute("pragma synchronous = normal");
Expand All @@ -137,10 +139,16 @@ export class Database {
* Under the hood, it uses `sqlite3_exec` if no parameters are given to bind
* with the SQL statement, a prepared statement otherwise.
*/
execute(sql: string, ...args: unknown[]): void;
execute(sql: string, args: Record<string, unknown>): void;
execute(sql: string, ...args: unknown[]) {
if (args.length) {
const prep = this.prepare(sql);
prep.bindAll(...args);
if (typeof args[0] === "object" && args[0] !== null) {
prep.bindAllNamed(args[0] as Record<string, unknown>);
} else {
prep.bindAll(...args);
}
prep.step();
prep.finalize();
} else sqlite3_exec(this.#handle, sql);
Expand Down Expand Up @@ -177,16 +185,30 @@ export class Database {
* const users = db.queryArray<[number, string]>("select id, username from users");
* // Using bind parameters
* const [user] = db.queryArray<[number, string]>("select id, username from users where email = ?", email);
* // Using named bind parameters
* const [user] = db.queryArray<[number, string]>("select id, username from users where email = :email", { email });
* ```
*
* @param sql SQL query to execute.
* @param args Parameters to bind to the query.
*
* @returns Array of rows (where rows are containing array of columns).
*/
queryArray<T extends unknown[] = any[]>(sql: string, ...args: unknown[]) {
queryArray<T extends unknown[] = any[]>(sql: string, ...args: unknown[]): T[];
queryArray<T extends unknown[] = any[]>(
sql: string,
args: Record<string, unknown>,
): T[];
queryArray<T extends unknown[] = any[]>(
sql: string,
...args: unknown[]
): T[] {
const stmt = this.prepare(sql);
stmt.bindAll(...args);
if (typeof args[0] === "object" && args[0] !== null) {
stmt.bindAllNamed(args[0] as Record<string, unknown>);
} else {
stmt.bindAll(...args);
}
const rows = [];
for (const row of stmt) {
rows.push(row.asArray());
Expand All @@ -212,19 +234,36 @@ export class Database {
* id: number,
* username: string,
* }>("select id, username from users where email = ?", email);
* // Using named bind parameters
* const [user] = db.queryObject<{
* id: number,
* username: string,
* }>("select id, username from users where email = :email", { email });
* ```
*
* @param sql SQL query to execute.
* @param args Parameters to bind to the query.
*
* @returns Array of rows, where rows are objects mapping column names to values.
*/
queryObject<T extends Record<string, unknown> = Record<string, any>>(
sql: string,
...args: unknown[]
): T[];
queryObject<T extends Record<string, unknown> = Record<string, any>>(
sql: string,
args: Record<string, unknown>,
): T[];
queryObject<T extends Record<string, unknown> = Record<string, any>>(
sql: string,
...args: unknown[]
) {
const stmt = this.prepare(sql);
stmt.bindAll(...args);
if (typeof args[0] === "object" && args[0] !== null) {
stmt.bindAllNamed(args[0] as Record<string, unknown>);
} else {
stmt.bindAll(...args);
}
const rows = [];
for (const row of stmt) {
rows.push(row.asObject());
Expand Down Expand Up @@ -342,7 +381,11 @@ export class PreparedStatement {

/** Get index of a binding parameter by its name. */
bindParameterIndex(name: string) {
return sqlite3_bind_parameter_index(this.#handle, name);
const index = sqlite3_bind_parameter_index(this.#handle, name);
if (index === 0) {
throw new Error(`Couldn't find index for '${name}'`);
}
return index;
}

#cstrCache = new Map<string, Uint8Array>();
Expand Down Expand Up @@ -473,6 +516,13 @@ export class PreparedStatement {
}
}

bindAllNamed(values: Record<string, unknown>) {
for (const name in values) {
const index = this.bindParameterIndex(":" + name);
this.bind(index, values[name]);
}
}

#cachedColCount?: number;

/** Column count in current row. */
Expand Down Expand Up @@ -555,7 +605,9 @@ export class PreparedStatement {
}

/** Adds another step to prepared statement to be executed. Don't forget to call `finalize`. */
execute(...args: unknown[]) {
execute(...args: unknown[]): void;
execute(args: Record<string, unknown>): void;
execute(...args: unknown[]): void {
this.bindAll(...args);
this.step();
this.reset();
Expand Down
5 changes: 4 additions & 1 deletion test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ Deno.test("sqlite", async (t) => {
double: number;
blob: Uint8Array;
nullable: null;
}>("select * from test where integer != ? and text != ?", 1, "hello world");
}>("select * from test where integer != :integer and text != :text", {
integer: 1,
text: "hello world",
});

assertEquals(rows.length, 9);
for (const row of rows) {
Expand Down

0 comments on commit 228b18e

Please sign in to comment.