bun-query-builder 0.2.29 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.js CHANGED
@@ -11862,6 +11862,68 @@ import { Database } from "bun:sqlite";
11862
11862
  import { mkdirSync as mkdirSync5 } from "fs";
11863
11863
  import { dirname as dirname5 } from "path";
11864
11864
  import process21 from "process";
11865
+ function bindNumberedPlaceholders(sql, params) {
11866
+ if (params.length === 0 || !/\$\d/.test(sql))
11867
+ return { sql, params };
11868
+ const out = [];
11869
+ let rewritten = "";
11870
+ let highest = 0;
11871
+ for (let i = 0;i < sql.length; i++) {
11872
+ const char = sql[i];
11873
+ if (char === "'" || char === '"' || char === "`") {
11874
+ const end = skipQuoted(sql, i, char);
11875
+ rewritten += sql.slice(i, end);
11876
+ i = end - 1;
11877
+ continue;
11878
+ }
11879
+ if (char === "-" && sql[i + 1] === "-") {
11880
+ const end = sql.indexOf(`
11881
+ `, i);
11882
+ const stop = end === -1 ? sql.length : end;
11883
+ rewritten += sql.slice(i, stop);
11884
+ i = stop - 1;
11885
+ continue;
11886
+ }
11887
+ if (char === "/" && sql[i + 1] === "*") {
11888
+ const end = sql.indexOf("*/", i + 2);
11889
+ const stop = end === -1 ? sql.length : end + 2;
11890
+ rewritten += sql.slice(i, stop);
11891
+ i = stop - 1;
11892
+ continue;
11893
+ }
11894
+ if (char === "$") {
11895
+ const digits = /^\d+/.exec(sql.slice(i + 1))?.[0];
11896
+ if (digits) {
11897
+ const index = Number(digits);
11898
+ highest = Math.max(highest, index);
11899
+ if (index >= 1 && index <= params.length) {
11900
+ out.push(params[index - 1]);
11901
+ rewritten += "?";
11902
+ i += digits.length;
11903
+ continue;
11904
+ }
11905
+ }
11906
+ }
11907
+ rewritten += char;
11908
+ }
11909
+ if (highest > params.length)
11910
+ throw new Error(`SQL references $${highest} but only ${params.length} value${params.length === 1 ? " was" : "s were"} provided`);
11911
+ if (out.length === 0)
11912
+ return { sql, params };
11913
+ return { sql: rewritten, params: out };
11914
+ }
11915
+ function skipQuoted(sql, start, quote) {
11916
+ for (let i = start + 1;i < sql.length; i++) {
11917
+ if (sql[i] !== quote)
11918
+ continue;
11919
+ if (sql[i + 1] === quote) {
11920
+ i++;
11921
+ continue;
11922
+ }
11923
+ return i + 1;
11924
+ }
11925
+ return sql.length;
11926
+ }
11865
11927
 
11866
11928
  class SQLiteWrapper {
11867
11929
  db;
@@ -11876,12 +11938,14 @@ class SQLiteWrapper {
11876
11938
  applySqliteBootstrapPragmas(this.db);
11877
11939
  }
11878
11940
  query(sql, params = []) {
11879
- const stmt = this.db.prepare(sql);
11880
- return stmt.all(...params);
11941
+ const bound = bindNumberedPlaceholders(sql, params);
11942
+ const stmt = this.db.prepare(bound.sql);
11943
+ return stmt.all(...bound.params);
11881
11944
  }
11882
11945
  run(sql, params = []) {
11883
- const stmt = this.db.prepare(sql);
11884
- return stmt.run(...params);
11946
+ const bound = bindNumberedPlaceholders(sql, params);
11947
+ const stmt = this.db.prepare(bound.sql);
11948
+ return stmt.run(...bound.params);
11885
11949
  }
11886
11950
  close() {
11887
11951
  this.db.close();
@@ -12650,6 +12714,21 @@ function renderSelectColumn(col) {
12650
12714
  }
12651
12715
  throw new TypeError(`[query-builder] select(): unsupported column ${String(col)} \u2014 pass a column name, a string[], or a SQL fragment (e.g. sql\`count(*) as c\`)`);
12652
12716
  }
12717
+ function hasTopLevelOrderBy(s) {
12718
+ const re = /\(|\)|\bORDER\s+BY\b/gi;
12719
+ let depth = 0;
12720
+ let m;
12721
+ while (m = re.exec(s)) {
12722
+ if (m[0] === "(") {
12723
+ depth++;
12724
+ } else if (m[0] === ")") {
12725
+ depth = Math.max(0, depth - 1);
12726
+ } else if (depth === 0) {
12727
+ return true;
12728
+ }
12729
+ }
12730
+ return false;
12731
+ }
12653
12732
  function validateIdentifier(name, context) {
12654
12733
  if (!SQL_PATTERNS.IDENTIFIER.test(name)) {
12655
12734
  const contextMsg = context ? ` in ${context}` : "";
@@ -13160,8 +13239,8 @@ function createQueryBuilder(state) {
13160
13239
  }
13161
13240
  return -1;
13162
13241
  };
13163
- const currentSql = () => {
13164
- const body = renderWhereTerms(whereTerms);
13242
+ const currentSql = (extra) => {
13243
+ const body = renderWhereTerms(extra ? [...whereTerms, extra] : whereTerms);
13165
13244
  if (!body)
13166
13245
  return text;
13167
13246
  if (whereTail) {
@@ -14888,25 +14967,33 @@ function createQueryBuilder(state) {
14888
14967
  return { data: hasMore ? data.slice(0, perPage) : data, meta: { perPage, page: p, hasMore } };
14889
14968
  },
14890
14969
  async cursorPaginate(perPage, cursor, column = "id", direction = "asc") {
14891
- let q = ensureBuilt();
14970
+ if (!Number.isInteger(perPage) || perPage <= 0)
14971
+ throw new TypeError(`[query-builder] cursorPaginate(perPage): expected positive integer, got ${perPage}`);
14972
+ const cols = (Array.isArray(column) ? column : [column]).map(String);
14973
+ for (const c of cols)
14974
+ validateIdentifier(c, "cursorPaginate(column)");
14975
+ const cmp = direction === "asc" ? ">" : "<";
14976
+ const dir = direction === "asc" ? "ASC" : "DESC";
14977
+ const params = [...whereParams];
14978
+ let predicate = "";
14892
14979
  if (cursor !== undefined && cursor !== null) {
14893
14980
  if (Array.isArray(column)) {
14894
- const cols = column.map((c) => sql(String(c)));
14895
- const comp = direction === "asc" ? sql`>` : sql`<`;
14896
- const tupleCols = sql`(${sql(cols)})`;
14897
- const tupleVals = sql`(${sql(cursor)})`;
14898
- q = sql`${q} WHERE ${tupleCols} ${comp} ${tupleVals}`;
14981
+ const phs = cursor.map((v) => {
14982
+ params.push(v);
14983
+ return getPlaceholder(params.length);
14984
+ });
14985
+ predicate = `(${cols.join(", ")}) ${cmp} (${phs.join(", ")})`;
14899
14986
  } else {
14900
- q = direction === "asc" ? sql`${q} WHERE ${sql(String(column))} > ${cursor}` : sql`${q} WHERE ${sql(String(column))} < ${cursor}`;
14987
+ params.push(cursor);
14988
+ predicate = `${cols[0]} ${cmp} ${getPlaceholder(params.length)}`;
14901
14989
  }
14902
14990
  }
14903
- if (Array.isArray(column)) {
14904
- const orderParts = column.map((c) => sql`${sql(String(c))} ${direction === "asc" ? sql`ASC` : sql`DESC`}`);
14905
- const orderExpr = orderParts.reduce((acc, p, i) => i === 0 ? p : sql`${acc}, ${p}`);
14906
- q = sql`${q} ORDER BY ${orderExpr} LIMIT ${perPage + 1}`;
14907
- } else {
14908
- q = sql`${q} ORDER BY ${sql(String(column))} ${direction === "asc" ? sql`ASC` : sql`DESC`} LIMIT ${perPage + 1}`;
14991
+ const base2 = reorderSelectClauses(predicate ? currentSql({ conn: "AND", sql: predicate }) : currentSql());
14992
+ const order = cols.map((c) => `${c} ${dir}`).join(", ");
14993
+ if (hasTopLevelOrderBy(base2)) {
14994
+ throw new TypeError(`[query-builder] cursorPaginate() cannot be combined with orderBy() \u2014 it orders by ` + `${order} itself, and a cursor is only meaningful in that order. ` + `Remove the orderBy(), or pass the column and direction to cursorPaginate().`);
14909
14995
  }
14996
+ const q = params.length > 0 ? _sql.unsafe(`${base2} ORDER BY ${order} LIMIT ${perPage + 1}`, params) : _sql.unsafe(`${base2} ORDER BY ${order} LIMIT ${perPage + 1}`);
14910
14997
  const rows = await runWithHooks(q, "select", { signal: abortSignal, timeoutMs });
14911
14998
  const hasMore = rows.length > perPage;
14912
14999
  const data = rows.slice(0, perPage);
@@ -31980,7 +32067,7 @@ function getPrefix() {
31980
32067
  }
31981
32068
  var prefix = getPrefix();
31982
32069
  // package.json
31983
- var version2 = "0.2.29";
32070
+ var version2 = "0.2.30";
31984
32071
 
31985
32072
  // bin/cli.ts
31986
32073
  init_actions();
package/dist/db.d.ts CHANGED
@@ -1,6 +1,35 @@
1
1
  import { Database } from 'bun:sqlite';
2
2
  import { SQL } from 'bun';
3
3
  import type { DatabaseConfig, PoolConfig, SupportedDialect } from './types';
4
+ /**
5
+ * SQLite wrapper that provides a SQL-like tagged template literal interface
6
+ * using bun:sqlite's Database class for better compiled binary support.
7
+ */
8
+ /**
9
+ * Give `$1`-style placeholders their Postgres meaning on SQLite: `$n` is the
10
+ * n-th value, wherever it appears and however often.
11
+ *
12
+ * bun:sqlite treats `$1` as a NAME, not an index. Handed an array it binds the
13
+ * values to those names in ORDER OF FIRST APPEARANCE and ignores the numbers
14
+ * entirely, so a query whose placeholders are not already in ascending order
15
+ * binds the wrong values and says nothing:
16
+ *
17
+ * sql.unsafe('SELECT $2 AS a, $1 AS b', ['ONE', 'TWO'])
18
+ * -> { a: 'ONE', b: 'TWO' } // both backwards, no error
19
+ *
20
+ * Every caller writing `$n` means the Postgres semantics, because the same
21
+ * query text is expected to run on both drivers. The failure is silent, and it
22
+ * lands hardest in exactly the queries that repeat a parameter - authorisation
23
+ * checks of the shape "owner_id = $2 OR EXISTS (... user_id = $2)" - where the
24
+ * result is a permission answer computed from the wrong values.
25
+ *
26
+ * Rewritten to ordinal `?` here, once, at the only place both `query` and `run`
27
+ * pass through. Placeholders inside string literals, quoted identifiers and
28
+ * comments are left alone: `'$1'` is data, not a parameter. SQL with no
29
+ * numbered placeholders is returned untouched, so `?` queries and Bun's own
30
+ * tagged-template output are unaffected.
31
+ */
32
+ export declare function bindNumberedPlaceholders(sql: string, params: any[]): { sql: string, params: any[] };
4
33
  export declare function splitSqlStatements(sql: string): string[];
5
34
  /**
6
35
  * Creates a database connection string based on the configured dialect and database settings.
@@ -98,10 +127,6 @@ export declare interface DriverConnection {
98
127
  }
99
128
  /** An `unsafe(...)` result: a query object that is ALSO directly awaitable. */
100
129
  export type AwaitableDriverQuery = DriverQuery & PromiseLike<any>;
101
- /**
102
- * SQLite wrapper that provides a SQL-like tagged template literal interface
103
- * using bun:sqlite's Database class for better compiled binary support.
104
- */
105
130
  declare class SQLiteWrapper {
106
131
  constructor(filename: string);
107
132
  query(sql: string, params?: any[]): any[];
package/dist/src/index.js CHANGED
@@ -11861,6 +11861,68 @@ import { Database } from "bun:sqlite";
11861
11861
  import { mkdirSync as mkdirSync5 } from "fs";
11862
11862
  import { dirname as dirname5 } from "path";
11863
11863
  import process21 from "process";
11864
+ function bindNumberedPlaceholders(sql, params) {
11865
+ if (params.length === 0 || !/\$\d/.test(sql))
11866
+ return { sql, params };
11867
+ const out = [];
11868
+ let rewritten = "";
11869
+ let highest = 0;
11870
+ for (let i = 0;i < sql.length; i++) {
11871
+ const char = sql[i];
11872
+ if (char === "'" || char === '"' || char === "`") {
11873
+ const end = skipQuoted(sql, i, char);
11874
+ rewritten += sql.slice(i, end);
11875
+ i = end - 1;
11876
+ continue;
11877
+ }
11878
+ if (char === "-" && sql[i + 1] === "-") {
11879
+ const end = sql.indexOf(`
11880
+ `, i);
11881
+ const stop = end === -1 ? sql.length : end;
11882
+ rewritten += sql.slice(i, stop);
11883
+ i = stop - 1;
11884
+ continue;
11885
+ }
11886
+ if (char === "/" && sql[i + 1] === "*") {
11887
+ const end = sql.indexOf("*/", i + 2);
11888
+ const stop = end === -1 ? sql.length : end + 2;
11889
+ rewritten += sql.slice(i, stop);
11890
+ i = stop - 1;
11891
+ continue;
11892
+ }
11893
+ if (char === "$") {
11894
+ const digits = /^\d+/.exec(sql.slice(i + 1))?.[0];
11895
+ if (digits) {
11896
+ const index = Number(digits);
11897
+ highest = Math.max(highest, index);
11898
+ if (index >= 1 && index <= params.length) {
11899
+ out.push(params[index - 1]);
11900
+ rewritten += "?";
11901
+ i += digits.length;
11902
+ continue;
11903
+ }
11904
+ }
11905
+ }
11906
+ rewritten += char;
11907
+ }
11908
+ if (highest > params.length)
11909
+ throw new Error(`SQL references $${highest} but only ${params.length} value${params.length === 1 ? " was" : "s were"} provided`);
11910
+ if (out.length === 0)
11911
+ return { sql, params };
11912
+ return { sql: rewritten, params: out };
11913
+ }
11914
+ function skipQuoted(sql, start, quote) {
11915
+ for (let i = start + 1;i < sql.length; i++) {
11916
+ if (sql[i] !== quote)
11917
+ continue;
11918
+ if (sql[i + 1] === quote) {
11919
+ i++;
11920
+ continue;
11921
+ }
11922
+ return i + 1;
11923
+ }
11924
+ return sql.length;
11925
+ }
11864
11926
 
11865
11927
  class SQLiteWrapper {
11866
11928
  db;
@@ -11875,12 +11937,14 @@ class SQLiteWrapper {
11875
11937
  applySqliteBootstrapPragmas(this.db);
11876
11938
  }
11877
11939
  query(sql, params = []) {
11878
- const stmt = this.db.prepare(sql);
11879
- return stmt.all(...params);
11940
+ const bound = bindNumberedPlaceholders(sql, params);
11941
+ const stmt = this.db.prepare(bound.sql);
11942
+ return stmt.all(...bound.params);
11880
11943
  }
11881
11944
  run(sql, params = []) {
11882
- const stmt = this.db.prepare(sql);
11883
- return stmt.run(...params);
11945
+ const bound = bindNumberedPlaceholders(sql, params);
11946
+ const stmt = this.db.prepare(bound.sql);
11947
+ return stmt.run(...bound.params);
11884
11948
  }
11885
11949
  close() {
11886
11950
  this.db.close();
@@ -12649,6 +12713,21 @@ function renderSelectColumn(col) {
12649
12713
  }
12650
12714
  throw new TypeError(`[query-builder] select(): unsupported column ${String(col)} \u2014 pass a column name, a string[], or a SQL fragment (e.g. sql\`count(*) as c\`)`);
12651
12715
  }
12716
+ function hasTopLevelOrderBy(s) {
12717
+ const re = /\(|\)|\bORDER\s+BY\b/gi;
12718
+ let depth = 0;
12719
+ let m;
12720
+ while (m = re.exec(s)) {
12721
+ if (m[0] === "(") {
12722
+ depth++;
12723
+ } else if (m[0] === ")") {
12724
+ depth = Math.max(0, depth - 1);
12725
+ } else if (depth === 0) {
12726
+ return true;
12727
+ }
12728
+ }
12729
+ return false;
12730
+ }
12652
12731
  function validateIdentifier(name, context) {
12653
12732
  if (!SQL_PATTERNS.IDENTIFIER.test(name)) {
12654
12733
  const contextMsg = context ? ` in ${context}` : "";
@@ -13159,8 +13238,8 @@ function createQueryBuilder(state) {
13159
13238
  }
13160
13239
  return -1;
13161
13240
  };
13162
- const currentSql = () => {
13163
- const body = renderWhereTerms(whereTerms);
13241
+ const currentSql = (extra) => {
13242
+ const body = renderWhereTerms(extra ? [...whereTerms, extra] : whereTerms);
13164
13243
  if (!body)
13165
13244
  return text;
13166
13245
  if (whereTail) {
@@ -14887,25 +14966,33 @@ function createQueryBuilder(state) {
14887
14966
  return { data: hasMore ? data.slice(0, perPage) : data, meta: { perPage, page: p, hasMore } };
14888
14967
  },
14889
14968
  async cursorPaginate(perPage, cursor, column = "id", direction = "asc") {
14890
- let q = ensureBuilt();
14969
+ if (!Number.isInteger(perPage) || perPage <= 0)
14970
+ throw new TypeError(`[query-builder] cursorPaginate(perPage): expected positive integer, got ${perPage}`);
14971
+ const cols = (Array.isArray(column) ? column : [column]).map(String);
14972
+ for (const c of cols)
14973
+ validateIdentifier(c, "cursorPaginate(column)");
14974
+ const cmp = direction === "asc" ? ">" : "<";
14975
+ const dir = direction === "asc" ? "ASC" : "DESC";
14976
+ const params = [...whereParams];
14977
+ let predicate = "";
14891
14978
  if (cursor !== undefined && cursor !== null) {
14892
14979
  if (Array.isArray(column)) {
14893
- const cols = column.map((c) => sql(String(c)));
14894
- const comp = direction === "asc" ? sql`>` : sql`<`;
14895
- const tupleCols = sql`(${sql(cols)})`;
14896
- const tupleVals = sql`(${sql(cursor)})`;
14897
- q = sql`${q} WHERE ${tupleCols} ${comp} ${tupleVals}`;
14980
+ const phs = cursor.map((v) => {
14981
+ params.push(v);
14982
+ return getPlaceholder(params.length);
14983
+ });
14984
+ predicate = `(${cols.join(", ")}) ${cmp} (${phs.join(", ")})`;
14898
14985
  } else {
14899
- q = direction === "asc" ? sql`${q} WHERE ${sql(String(column))} > ${cursor}` : sql`${q} WHERE ${sql(String(column))} < ${cursor}`;
14986
+ params.push(cursor);
14987
+ predicate = `${cols[0]} ${cmp} ${getPlaceholder(params.length)}`;
14900
14988
  }
14901
14989
  }
14902
- if (Array.isArray(column)) {
14903
- const orderParts = column.map((c) => sql`${sql(String(c))} ${direction === "asc" ? sql`ASC` : sql`DESC`}`);
14904
- const orderExpr = orderParts.reduce((acc, p, i) => i === 0 ? p : sql`${acc}, ${p}`);
14905
- q = sql`${q} ORDER BY ${orderExpr} LIMIT ${perPage + 1}`;
14906
- } else {
14907
- q = sql`${q} ORDER BY ${sql(String(column))} ${direction === "asc" ? sql`ASC` : sql`DESC`} LIMIT ${perPage + 1}`;
14990
+ const base2 = reorderSelectClauses(predicate ? currentSql({ conn: "AND", sql: predicate }) : currentSql());
14991
+ const order = cols.map((c) => `${c} ${dir}`).join(", ");
14992
+ if (hasTopLevelOrderBy(base2)) {
14993
+ throw new TypeError(`[query-builder] cursorPaginate() cannot be combined with orderBy() \u2014 it orders by ` + `${order} itself, and a cursor is only meaningful in that order. ` + `Remove the orderBy(), or pass the column and direction to cursorPaginate().`);
14908
14994
  }
14995
+ const q = params.length > 0 ? _sql.unsafe(`${base2} ORDER BY ${order} LIMIT ${perPage + 1}`, params) : _sql.unsafe(`${base2} ORDER BY ${order} LIMIT ${perPage + 1}`);
14909
14996
  const rows = await runWithHooks(q, "select", { signal: abortSignal, timeoutMs });
14910
14997
  const hasMore = rows.length > perPage;
14911
14998
  const data = rows.slice(0, perPage);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-query-builder",
3
3
  "type": "module",
4
- "version": "0.2.29",
4
+ "version": "0.2.30",
5
5
  "description": "A simple yet performant query builder for TypeScript. Built with Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",