Skip to content

Add support for bind params #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
38 changes: 37 additions & 1 deletion src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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([
Expand Down
32 changes: 30 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* A param that's expected to be bound later of included in `values`.
*/
export const BIND_PARAM = Symbol("BIND_PARAM");
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name options:

  • BIND_PARAMETER
  • BIND_VALUE
  • BIND
  • BIND_KEY
  • PARAM


/**
* Values supported by SQL engine.
*/
Expand All @@ -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;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only question to myself is whether values should now become a getter that throws when bindParams > 0? That would ensure the instance isn't incorrectly used without binding params.

readonly values: Value[];
readonly strings: string[];

Expand Down Expand Up @@ -53,15 +59,20 @@ 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.
this.strings[pos] += rawString;
} else {
this.values[pos++] = child;
this.strings[pos] = rawString;
if (child === BIND_PARAM) this.bindParams++;
}
}
}
Expand Down Expand Up @@ -90,6 +101,23 @@ export class Sql {
return value;
}

bind(...params: Value[]) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't technically need this method if you know what you're doing, but the array length safety and merging with any non-params seems like a good pattern to enforce.

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,
Expand Down
Loading