diff --git a/src/index.spec.ts b/src/index.spec.ts index f8f2b83..3f0e0db 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,6 +1,6 @@ import { inspect } from "util"; import { describe, it, expect } from "vitest"; -import sql, { empty, join, bulk, raw, Sql } from "./index.js"; +import sql, { empty, join, bulk, raw, Sql, BIND_PARAM } from "./index.js"; describe("sql template tag", () => { it("should generate sql", () => { @@ -163,6 +163,42 @@ describe("sql template tag", () => { }); }); + describe("bind parameters", () => { + it("should bind parameters", () => { + const query = sql`SELECT * FROM books WHERE author = ${BIND_PARAM}`; + const values = query.bind("Blake"); + + expect(query.text).toEqual("SELECT * FROM books WHERE author = $1"); + expect(query.values).toEqual([BIND_PARAM]); + expect(values).toEqual(["Blake"]); + }); + + it("should merge with other values", () => { + const query = sql`SELECT * FROM books WHERE author = ${BIND_PARAM} OR author_id = ${"Taylor"}`; + const values = query.bind("Blake"); + + expect(query.text).toEqual( + "SELECT * FROM books WHERE author = $1 OR author_id = $2", + ); + expect(query.values).toEqual([BIND_PARAM, "Taylor"]); + expect(values).toEqual(["Blake", "Taylor"]); + }); + + it("should error when binding too many parameters", () => { + const query = sql`SELECT * FROM books WHERE author = ${BIND_PARAM}`; + expect(() => query.bind("Blake", "Taylor")).toThrowError( + "Expected 1 parameters to be bound, but got 2", + ); + }); + + it("should error when binding too few parameters", () => { + const query = sql`SELECT * FROM books WHERE author = ${BIND_PARAM}`; + expect(() => query.bind()).toThrowError( + "Expected 1 parameters to be bound, but got 0", + ); + }); + }); + describe("bulk", () => { it("should join nested list", () => { const query = bulk([ diff --git a/src/index.ts b/src/index.ts index 6f46eed..858e458 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,8 @@ +/** + * A param that's expected to be bound later of included in `values`. + */ +export const BIND_PARAM = Symbol("BIND_PARAM"); + /** * Values supported by SQL engine. */ @@ -12,6 +17,7 @@ export type RawValue = Value | Sql; * A SQL instance can be nested within each other to build SQL strings. */ export class Sql { + readonly bindParams = 0; readonly values: Value[]; readonly strings: string[]; @@ -53,8 +59,12 @@ export class Sql { let childIndex = 0; while (childIndex < child.values.length) { - this.values[pos++] = child.values[childIndex++]; - this.strings[pos] = child.strings[childIndex]; + const value = child.values[childIndex++]; + const str = child.strings[childIndex]; + + this.values[pos++] = value; + this.strings[pos] = str; + if (value === BIND_PARAM) this.bindParams++; } // Append raw string to current string. @@ -62,6 +72,7 @@ export class Sql { } else { this.values[pos++] = child; this.strings[pos] = rawString; + if (child === BIND_PARAM) this.bindParams++; } } } @@ -90,6 +101,23 @@ export class Sql { return value; } + bind(...params: Value[]) { + if (params.length !== this.bindParams) { + throw new TypeError( + `Expected ${this.bindParams} parameters to be bound, but got ${params.length}`, + ); + } + + const values = new Array(this.values.length); + + for (let i = 0, j = 0; i < this.values.length; i++) { + const value = this.values[i]; + values[i] = value === BIND_PARAM ? params[j++] : value; + } + + return values; + } + inspect() { return { sql: this.sql,