bun-query-builder 0.2.30 → 0.2.32

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
@@ -12641,6 +12641,61 @@ function renderInPredicate(column, values, negated, placeholders) {
12641
12641
  return negated ? TRUE_PREDICATE : FALSE_PREDICATE;
12642
12642
  return `${column} ${negated ? "NOT IN" : "IN"} (${placeholders})`;
12643
12643
  }
12644
+ function toDialectPlaceholders(sql, dialect) {
12645
+ if (dialect !== "postgres")
12646
+ return sql;
12647
+ let out = "";
12648
+ let index = 0;
12649
+ let position = 0;
12650
+ while (position < sql.length) {
12651
+ const char = sql[position];
12652
+ if (char === "'") {
12653
+ const end = closingQuote(sql, position, "'");
12654
+ out += sql.slice(position, end);
12655
+ position = end;
12656
+ continue;
12657
+ }
12658
+ if (char === '"') {
12659
+ const end = closingQuote(sql, position, '"');
12660
+ out += sql.slice(position, end);
12661
+ position = end;
12662
+ continue;
12663
+ }
12664
+ if (char === "$") {
12665
+ const tag = sql.slice(position).match(/^\$[A-Z_a-z]\w*\$|^\$\$/)?.[0];
12666
+ if (tag) {
12667
+ const close = sql.indexOf(tag, position + tag.length);
12668
+ const end = close === -1 ? sql.length : close + tag.length;
12669
+ out += sql.slice(position, end);
12670
+ position = end;
12671
+ continue;
12672
+ }
12673
+ }
12674
+ if (char === "?") {
12675
+ index++;
12676
+ out += `$${index}`;
12677
+ position++;
12678
+ continue;
12679
+ }
12680
+ out += char;
12681
+ position++;
12682
+ }
12683
+ return out;
12684
+ }
12685
+ function closingQuote(sql, start, quote) {
12686
+ let position = start + 1;
12687
+ while (position < sql.length) {
12688
+ if (sql[position] === quote) {
12689
+ if (sql[position + 1] === quote) {
12690
+ position += 2;
12691
+ continue;
12692
+ }
12693
+ return position + 1;
12694
+ }
12695
+ position++;
12696
+ }
12697
+ return sql.length;
12698
+ }
12644
12699
  var FALSE_PREDICATE = "1 = 0", TRUE_PREDICATE = "1 = 1";
12645
12700
 
12646
12701
  // src/client.ts
@@ -18518,6 +18573,28 @@ ${sql};` : `${sql};`;
18518
18573
  recordMigrationQuery() {
18519
18574
  return "INSERT INTO migrations (migration) VALUES (?)";
18520
18575
  }
18576
+ jsonExtract(column, key) {
18577
+ return `JSON_UNQUOTE(JSON_EXTRACT(${column}, CONCAT('$.', ${key})))`;
18578
+ }
18579
+ dateBucket(column, grain, offsetHours = 0) {
18580
+ const shifted = offsetHours === 0 ? column : `DATE_ADD(${column}, INTERVAL ${offsetHours} HOUR)`;
18581
+ switch (grain) {
18582
+ case "hour":
18583
+ return `DATE_FORMAT(${shifted}, '%Y-%m-%dT%H:00:00.000Z')`;
18584
+ case "week":
18585
+ return `DATE_FORMAT(DATE_SUB(${shifted}, INTERVAL WEEKDAY(${shifted}) DAY), '%Y-%m-%dT00:00:00.000Z')`;
18586
+ case "month":
18587
+ return `DATE_FORMAT(${shifted}, '%Y-%m-01T00:00:00.000Z')`;
18588
+ case "year":
18589
+ return `DATE_FORMAT(${shifted}, '%Y-01-01T00:00:00.000Z')`;
18590
+ case "day":
18591
+ default:
18592
+ return `DATE_FORMAT(${shifted}, '%Y-%m-%dT00:00:00.000Z')`;
18593
+ }
18594
+ }
18595
+ booleanLiteral(value) {
18596
+ return value ? "TRUE" : "FALSE";
18597
+ }
18521
18598
  renderColumn(column) {
18522
18599
  const typeSql = this.getColumnType(column);
18523
18600
  const parts = [this.quoteIdentifier(column.name), typeSql];
@@ -18715,6 +18792,28 @@ ${sql};` : `${sql};`;
18715
18792
  recordMigrationQuery() {
18716
18793
  return "INSERT INTO migrations (migration) VALUES ($1)";
18717
18794
  }
18795
+ jsonExtract(column, key) {
18796
+ return `(${column}::jsonb ->> ${key})`;
18797
+ }
18798
+ dateBucket(column, grain, offsetHours = 0) {
18799
+ const shifted = offsetHours === 0 ? `${column}::timestamp` : `(${column}::timestamp ${offsetHours >= 0 ? "+" : "-"} interval '${Math.abs(offsetHours)} hours')`;
18800
+ const truncated = grain === "week" ? `date_trunc('week', ${shifted})` : shifted;
18801
+ switch (grain) {
18802
+ case "hour":
18803
+ return `to_char(${truncated}, 'YYYY-MM-DD"T"HH24:00:00.000"Z"')`;
18804
+ case "month":
18805
+ return `to_char(${truncated}, 'YYYY-MM-01"T"00:00:00.000"Z"')`;
18806
+ case "year":
18807
+ return `to_char(${truncated}, 'YYYY-01-01"T"00:00:00.000"Z"')`;
18808
+ case "week":
18809
+ case "day":
18810
+ default:
18811
+ return `to_char(${truncated}, 'YYYY-MM-DD"T"00:00:00.000"Z"')`;
18812
+ }
18813
+ }
18814
+ booleanLiteral(value) {
18815
+ return value ? "TRUE" : "FALSE";
18816
+ }
18718
18817
  renderColumn(column) {
18719
18818
  const typeSql = column.isPrimaryKey ? this.getPrimaryKeyType(column) : this.getColumnType(column);
18720
18819
  const parts = [this.quoteIdentifier(column.name), typeSql];
@@ -18998,6 +19097,28 @@ class SQLiteDriver {
18998
19097
  recordMigrationQuery() {
18999
19098
  return "INSERT INTO migrations (migration) VALUES (?)";
19000
19099
  }
19100
+ jsonExtract(column, key) {
19101
+ return `json_extract(${column}, '$.' || ${key})`;
19102
+ }
19103
+ dateBucket(column, grain, offsetHours = 0) {
19104
+ const shifted = offsetHours === 0 ? column : `datetime(${column}, '${offsetHours >= 0 ? "+" : "-"}${Math.abs(offsetHours)} hours')`;
19105
+ switch (grain) {
19106
+ case "hour":
19107
+ return `strftime('%Y-%m-%dT%H:00:00.000Z', ${shifted})`;
19108
+ case "week":
19109
+ return `strftime('%Y-%m-%dT00:00:00.000Z', ${shifted}, '-6 days', 'weekday 1')`;
19110
+ case "month":
19111
+ return `strftime('%Y-%m-01T00:00:00.000Z', ${shifted})`;
19112
+ case "year":
19113
+ return `strftime('%Y-01-01T00:00:00.000Z', ${shifted})`;
19114
+ case "day":
19115
+ default:
19116
+ return `strftime('%Y-%m-%dT00:00:00.000Z', ${shifted})`;
19117
+ }
19118
+ }
19119
+ booleanLiteral(value) {
19120
+ return value ? "TRUE" : "FALSE";
19121
+ }
19001
19122
  renderColumn(column) {
19002
19123
  const typeSql = this.getColumnType(column);
19003
19124
  const parts = [this.quoteIdentifier(column.name), typeSql];
@@ -32067,7 +32188,7 @@ function getPrefix() {
32067
32188
  }
32068
32189
  var prefix = getPrefix();
32069
32190
  // package.json
32070
- var version2 = "0.2.30";
32191
+ var version2 = "0.2.32";
32071
32192
 
32072
32193
  // bin/cli.ts
32073
32194
  init_actions();
@@ -1,4 +1,5 @@
1
1
  import type { ColumnPlan, IndexPlan, RebuildTableSpec, TablePlan } from '../migrations';
2
+ import type { DateBucketGrain } from './postgres';
2
3
  export declare interface DialectDriver {
3
4
  createEnumType: (enumTypeName: string, values: string[]) => string
4
5
  createTable: (table: TablePlan) => string
@@ -39,5 +40,8 @@ export declare class MySQLDriver implements DialectDriver {
39
40
  createMigrationsTable(): string;
40
41
  getExecutedMigrationsQuery(): string;
41
42
  recordMigrationQuery(): string;
43
+ jsonExtract(column: string, key: string): string;
44
+ dateBucket(column: string, grain: DateBucketGrain, offsetHours?: number): string;
45
+ booleanLiteral(value: boolean): string;
42
46
  protected renderColumn(column: ColumnPlan): string;
43
47
  }
@@ -1,4 +1,11 @@
1
1
  import type { ColumnPlan, IndexPlan, RebuildTableSpec, TablePlan } from '../migrations';
2
+ /**
3
+ * The ISO shape every driver's {@link DialectDriver.dateBucket} must produce.
4
+ *
5
+ * Exported so a driver's tests can assert against one definition rather than
6
+ * each restating the format it happens to emit.
7
+ */
8
+ export declare const DATE_BUCKET_FORMAT: 'YYYY-MM-DDTHH:MM:SS.000Z';
2
9
  export declare interface DialectDriver {
3
10
  createEnumType: (enumTypeName: string, values: string[]) => string
4
11
  createTable: (table: TablePlan) => string
@@ -16,7 +23,12 @@ export declare interface DialectDriver {
16
23
  createMigrationsTable: () => string
17
24
  getExecutedMigrationsQuery: () => string
18
25
  recordMigrationQuery: () => string
26
+ jsonExtract: (column: string, key: string) => string
27
+ dateBucket: (column: string, grain: DateBucketGrain, offsetHours?: number) => string
28
+ booleanLiteral: (value: boolean) => string
19
29
  }
30
+ /** The buckets {@link DialectDriver.dateBucket} can truncate to. */
31
+ export type DateBucketGrain = 'hour' | 'day' | 'week' | 'month' | 'year';
20
32
  export declare class PostgresDriver implements DialectDriver {
21
33
  createEnumType(enumTypeName: string, values: string[]): string;
22
34
  createTable(table: TablePlan): string;
@@ -34,4 +46,7 @@ export declare class PostgresDriver implements DialectDriver {
34
46
  createMigrationsTable(): string;
35
47
  getExecutedMigrationsQuery(): string;
36
48
  recordMigrationQuery(): string;
49
+ jsonExtract(column: string, key: string): string;
50
+ dateBucket(column: string, grain: DateBucketGrain, offsetHours?: number): string;
51
+ booleanLiteral(value: boolean): string;
37
52
  }
@@ -1,4 +1,5 @@
1
1
  import type { ColumnPlan, IndexPlan, RebuildTableSpec, TablePlan } from '../migrations';
2
+ import type { DateBucketGrain } from './postgres';
2
3
  /**
3
4
  * Whether a plan type belongs to the numeric family. Used by the `_id`
4
5
  * safety net (numeric ids must store as INTEGER on SQLite) — non-numeric
@@ -40,4 +41,7 @@ export declare class SQLiteDriver implements DialectDriver {
40
41
  createMigrationsTable(): string;
41
42
  getExecutedMigrationsQuery(): string;
42
43
  recordMigrationQuery(): string;
44
+ jsonExtract(column: string, key: string): string;
45
+ dateBucket(column: string, grain: DateBucketGrain, offsetHours?: number): string;
46
+ booleanLiteral(value: boolean): string;
43
47
  }
package/dist/index.d.ts CHANGED
@@ -50,3 +50,4 @@ export * from './types';
50
50
  export { type ModelDefinition, defineModel } from './model';
51
51
  // Explicit re-exports for model registry functions
52
52
  export { getModel, getAllModels, getModelRegistry, hasModel, clearModelRegistry, registerModel } from './model';
53
+ export { toDialectPlaceholders } from './sql-fragments';
@@ -1,3 +1,4 @@
1
+ import type { SupportedDialect } from './types';
1
2
  /**
2
3
  * Render a WHERE body (without the `WHERE` keyword) from an ordered term list.
3
4
  *
@@ -32,6 +33,25 @@ export declare function renderWhereTerms(terms: readonly WhereTerm[]): string;
32
33
  * failure as the `IN ()` bug itself.
33
34
  */
34
35
  export declare function renderInPredicate(column: string, values: any[], negated: boolean, placeholders: string): string;
36
+ /**
37
+ * Rewrite `?` placeholders into the numbered form Postgres requires.
38
+ *
39
+ * Raw SQL written with `?` runs on SQLite and MySQL and is a syntax error on
40
+ * Postgres, which reports it as a problem with whatever token follows: a WHERE
41
+ * built as `project_id = ? AND …` is rejected with `syntax error at or near
42
+ * "AND"`, which sends the reader looking at the wrong part of the query. An
43
+ * application that developed against SQLite therefore finds out at the point
44
+ * its raw queries first meet Postgres, and the error does not name the cause.
45
+ *
46
+ * Only unquoted placeholders are rewritten. A `?` inside a string literal, a
47
+ * quoted identifier, or a dollar-quoted block is data and is left exactly as
48
+ * written — the naive global replace is the reason this needs to be a function
49
+ * with tests rather than one line at a call site.
50
+ *
51
+ * Dialects that already take `?` get the string back unchanged, so this is safe
52
+ * to apply unconditionally.
53
+ */
54
+ export declare function toDialectPlaceholders(sql: string, dialect: SupportedDialect): string;
35
55
  /**
36
56
  * Constant predicates, for the cases where a collection is empty.
37
57
  *
package/dist/src/index.js CHANGED
@@ -12640,6 +12640,61 @@ function renderInPredicate(column, values, negated, placeholders) {
12640
12640
  return negated ? TRUE_PREDICATE : FALSE_PREDICATE;
12641
12641
  return `${column} ${negated ? "NOT IN" : "IN"} (${placeholders})`;
12642
12642
  }
12643
+ function toDialectPlaceholders(sql, dialect) {
12644
+ if (dialect !== "postgres")
12645
+ return sql;
12646
+ let out = "";
12647
+ let index = 0;
12648
+ let position = 0;
12649
+ while (position < sql.length) {
12650
+ const char = sql[position];
12651
+ if (char === "'") {
12652
+ const end = closingQuote(sql, position, "'");
12653
+ out += sql.slice(position, end);
12654
+ position = end;
12655
+ continue;
12656
+ }
12657
+ if (char === '"') {
12658
+ const end = closingQuote(sql, position, '"');
12659
+ out += sql.slice(position, end);
12660
+ position = end;
12661
+ continue;
12662
+ }
12663
+ if (char === "$") {
12664
+ const tag = sql.slice(position).match(/^\$[A-Z_a-z]\w*\$|^\$\$/)?.[0];
12665
+ if (tag) {
12666
+ const close = sql.indexOf(tag, position + tag.length);
12667
+ const end = close === -1 ? sql.length : close + tag.length;
12668
+ out += sql.slice(position, end);
12669
+ position = end;
12670
+ continue;
12671
+ }
12672
+ }
12673
+ if (char === "?") {
12674
+ index++;
12675
+ out += `$${index}`;
12676
+ position++;
12677
+ continue;
12678
+ }
12679
+ out += char;
12680
+ position++;
12681
+ }
12682
+ return out;
12683
+ }
12684
+ function closingQuote(sql, start, quote) {
12685
+ let position = start + 1;
12686
+ while (position < sql.length) {
12687
+ if (sql[position] === quote) {
12688
+ if (sql[position + 1] === quote) {
12689
+ position += 2;
12690
+ continue;
12691
+ }
12692
+ return position + 1;
12693
+ }
12694
+ position++;
12695
+ }
12696
+ return sql.length;
12697
+ }
12643
12698
  var FALSE_PREDICATE = "1 = 0", TRUE_PREDICATE = "1 = 1";
12644
12699
 
12645
12700
  // src/client.ts
@@ -18517,6 +18572,28 @@ ${sql};` : `${sql};`;
18517
18572
  recordMigrationQuery() {
18518
18573
  return "INSERT INTO migrations (migration) VALUES (?)";
18519
18574
  }
18575
+ jsonExtract(column, key) {
18576
+ return `JSON_UNQUOTE(JSON_EXTRACT(${column}, CONCAT('$.', ${key})))`;
18577
+ }
18578
+ dateBucket(column, grain, offsetHours = 0) {
18579
+ const shifted = offsetHours === 0 ? column : `DATE_ADD(${column}, INTERVAL ${offsetHours} HOUR)`;
18580
+ switch (grain) {
18581
+ case "hour":
18582
+ return `DATE_FORMAT(${shifted}, '%Y-%m-%dT%H:00:00.000Z')`;
18583
+ case "week":
18584
+ return `DATE_FORMAT(DATE_SUB(${shifted}, INTERVAL WEEKDAY(${shifted}) DAY), '%Y-%m-%dT00:00:00.000Z')`;
18585
+ case "month":
18586
+ return `DATE_FORMAT(${shifted}, '%Y-%m-01T00:00:00.000Z')`;
18587
+ case "year":
18588
+ return `DATE_FORMAT(${shifted}, '%Y-01-01T00:00:00.000Z')`;
18589
+ case "day":
18590
+ default:
18591
+ return `DATE_FORMAT(${shifted}, '%Y-%m-%dT00:00:00.000Z')`;
18592
+ }
18593
+ }
18594
+ booleanLiteral(value) {
18595
+ return value ? "TRUE" : "FALSE";
18596
+ }
18520
18597
  renderColumn(column) {
18521
18598
  const typeSql = this.getColumnType(column);
18522
18599
  const parts = [this.quoteIdentifier(column.name), typeSql];
@@ -18714,6 +18791,28 @@ ${sql};` : `${sql};`;
18714
18791
  recordMigrationQuery() {
18715
18792
  return "INSERT INTO migrations (migration) VALUES ($1)";
18716
18793
  }
18794
+ jsonExtract(column, key) {
18795
+ return `(${column}::jsonb ->> ${key})`;
18796
+ }
18797
+ dateBucket(column, grain, offsetHours = 0) {
18798
+ const shifted = offsetHours === 0 ? `${column}::timestamp` : `(${column}::timestamp ${offsetHours >= 0 ? "+" : "-"} interval '${Math.abs(offsetHours)} hours')`;
18799
+ const truncated = grain === "week" ? `date_trunc('week', ${shifted})` : shifted;
18800
+ switch (grain) {
18801
+ case "hour":
18802
+ return `to_char(${truncated}, 'YYYY-MM-DD"T"HH24:00:00.000"Z"')`;
18803
+ case "month":
18804
+ return `to_char(${truncated}, 'YYYY-MM-01"T"00:00:00.000"Z"')`;
18805
+ case "year":
18806
+ return `to_char(${truncated}, 'YYYY-01-01"T"00:00:00.000"Z"')`;
18807
+ case "week":
18808
+ case "day":
18809
+ default:
18810
+ return `to_char(${truncated}, 'YYYY-MM-DD"T"00:00:00.000"Z"')`;
18811
+ }
18812
+ }
18813
+ booleanLiteral(value) {
18814
+ return value ? "TRUE" : "FALSE";
18815
+ }
18717
18816
  renderColumn(column) {
18718
18817
  const typeSql = column.isPrimaryKey ? this.getPrimaryKeyType(column) : this.getColumnType(column);
18719
18818
  const parts = [this.quoteIdentifier(column.name), typeSql];
@@ -18997,6 +19096,28 @@ class SQLiteDriver {
18997
19096
  recordMigrationQuery() {
18998
19097
  return "INSERT INTO migrations (migration) VALUES (?)";
18999
19098
  }
19099
+ jsonExtract(column, key) {
19100
+ return `json_extract(${column}, '$.' || ${key})`;
19101
+ }
19102
+ dateBucket(column, grain, offsetHours = 0) {
19103
+ const shifted = offsetHours === 0 ? column : `datetime(${column}, '${offsetHours >= 0 ? "+" : "-"}${Math.abs(offsetHours)} hours')`;
19104
+ switch (grain) {
19105
+ case "hour":
19106
+ return `strftime('%Y-%m-%dT%H:00:00.000Z', ${shifted})`;
19107
+ case "week":
19108
+ return `strftime('%Y-%m-%dT00:00:00.000Z', ${shifted}, '-6 days', 'weekday 1')`;
19109
+ case "month":
19110
+ return `strftime('%Y-%m-01T00:00:00.000Z', ${shifted})`;
19111
+ case "year":
19112
+ return `strftime('%Y-01-01T00:00:00.000Z', ${shifted})`;
19113
+ case "day":
19114
+ default:
19115
+ return `strftime('%Y-%m-%dT00:00:00.000Z', ${shifted})`;
19116
+ }
19117
+ }
19118
+ booleanLiteral(value) {
19119
+ return value ? "TRUE" : "FALSE";
19120
+ }
19000
19121
  renderColumn(column) {
19001
19122
  const typeSql = this.getColumnType(column);
19002
19123
  const parts = [this.quoteIdentifier(column.name), typeSql];
@@ -30002,6 +30123,7 @@ export {
30002
30123
  toTableName,
30003
30124
  toModelInstance,
30004
30125
  toDynamoDBItem,
30126
+ toDialectPlaceholders,
30005
30127
  tinker,
30006
30128
  tableNameFor,
30007
30129
  tableInfo,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-query-builder",
3
3
  "type": "module",
4
- "version": "0.2.30",
4
+ "version": "0.2.32",
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",