bun-query-builder 0.1.31 → 0.1.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
@@ -12344,6 +12344,14 @@ function* iterateAllPivots(meta, options = {}) {
12344
12344
  function isRawExpression(expr) {
12345
12345
  return typeof expr === "object" && expr !== null && "raw" in expr && typeof expr.raw === "string";
12346
12346
  }
12347
+ function raw(strings, ...values) {
12348
+ if (typeof strings === "string")
12349
+ return { raw: strings };
12350
+ let out = strings[0];
12351
+ for (let i = 0;i < values.length; i++)
12352
+ out += formatSubqueryValue(values[i]) + strings[i + 1];
12353
+ return { raw: out };
12354
+ }
12347
12355
  function quoteInsertIdent(id) {
12348
12356
  return config5.dialect === "mysql" ? `\`${id.replace(/`/g, "``")}\`` : `"${id.replace(/"/g, '""')}"`;
12349
12357
  }
@@ -12412,13 +12420,31 @@ function validateQualifiedIdentifier(value, context) {
12412
12420
  throw new TypeError(`[query-builder] ${context}: identifier segment '${part}' contains characters outside [A-Za-z0-9_]`);
12413
12421
  }
12414
12422
  }
12415
- function assertSqlFragment(fragment, context) {
12416
- if (fragment === null || fragment === undefined) {
12417
- throw new TypeError(`[query-builder] ${context}: fragment must be a SqlFragment, got ${fragment}`);
12418
- }
12423
+ function renderRawFragment(fragment, context) {
12419
12424
  if (typeof fragment === "string") {
12420
12425
  warnOnceBareSqlFragment(context);
12426
+ return fragment;
12421
12427
  }
12428
+ if (fragment === null || fragment === undefined)
12429
+ throw new TypeError(`[query-builder] ${context}: fragment must be a SqlFragment, got ${fragment}`);
12430
+ if (isRawExpression(fragment))
12431
+ return fragment.raw;
12432
+ if (typeof fragment === "object") {
12433
+ const f = fragment;
12434
+ if (typeof f.raw === "string")
12435
+ return f.raw;
12436
+ if (typeof f.raw === "function") {
12437
+ const r = f.raw();
12438
+ if (typeof r === "string")
12439
+ return r;
12440
+ }
12441
+ if (typeof f.sql === "string")
12442
+ return f.sql;
12443
+ const s = String(fragment);
12444
+ if (s !== "[object Object]" && s !== "[object Promise]")
12445
+ return s;
12446
+ }
12447
+ throw new TypeError(`[query-builder] ${context}: cannot render this value as a SQL fragment. ` + `A Bun \`sql\`...\`\` query object cannot be converted to SQL text \u2014 pass a ` + `string, or use the exported \`raw\` helper: raw\`count(*) as c\` / raw('age > 18').`);
12422
12448
  }
12423
12449
  function warnOnceBareSqlFragment(context) {
12424
12450
  if (warnedSqlFragmentContexts.has(context))
@@ -13093,12 +13119,12 @@ function createQueryBuilder(state) {
13093
13119
  return this;
13094
13120
  },
13095
13121
  selectRaw(fragment) {
13096
- assertSqlFragment(fragment, "selectRaw(fragment)");
13122
+ const frag = renderRawFragment(fragment, "selectRaw(fragment)");
13097
13123
  const fromIdx = text.indexOf(" FROM ");
13098
13124
  if (fromIdx !== -1) {
13099
- text = `${text.substring(0, fromIdx)}, ${String(fragment)}${text.substring(fromIdx)}`;
13125
+ text = `${text.substring(0, fromIdx)}, ${frag}${text.substring(fromIdx)}`;
13100
13126
  } else {
13101
- text += `, ${String(fragment)}`;
13127
+ text += `, ${frag}`;
13102
13128
  }
13103
13129
  built = null;
13104
13130
  return this;
@@ -13211,12 +13237,15 @@ function createQueryBuilder(state) {
13211
13237
  if (cols.length === 0)
13212
13238
  return this;
13213
13239
  const rendered = cols.map(renderSelectColumn);
13240
+ const distinctMatch = /^SELECT\s+(DISTINCT(?:\s+ON\s+\([^)]*\))?\s+)/i.exec(text);
13241
+ const distinctPrefix = distinctMatch ? distinctMatch[1] : "";
13214
13242
  const fromIndex = text.indexOf(" FROM ");
13215
13243
  if (fromIndex !== -1) {
13216
- text = `SELECT ${rendered.join(", ")}${text.substring(fromIndex)}`;
13244
+ text = `SELECT ${distinctPrefix}${rendered.join(", ")}${text.substring(fromIndex)}`;
13217
13245
  } else {
13218
- text = `SELECT ${rendered.join(", ")} FROM ${table}`;
13246
+ text = `SELECT ${distinctPrefix}${rendered.join(", ")} FROM ${table}`;
13219
13247
  }
13248
+ built = null;
13220
13249
  return this;
13221
13250
  },
13222
13251
  addSelect(...columns2) {
@@ -14105,9 +14134,9 @@ function createQueryBuilder(state) {
14105
14134
  return this;
14106
14135
  },
14107
14136
  whereRaw(fragment) {
14108
- assertSqlFragment(fragment, "whereRaw(fragment)");
14137
+ const frag = renderRawFragment(fragment, "whereRaw(fragment)");
14109
14138
  const keyword = SQL_PATTERNS.WHERE.test(text) ? "AND" : "WHERE";
14110
- text += ` ${keyword} ${String(fragment)}`;
14139
+ text += ` ${keyword} ${frag}`;
14111
14140
  built = null;
14112
14141
  return this;
14113
14142
  },
@@ -14473,8 +14502,8 @@ function createQueryBuilder(state) {
14473
14502
  return this;
14474
14503
  },
14475
14504
  groupByRaw(fragment) {
14476
- assertSqlFragment(fragment, "groupByRaw(fragment)");
14477
- text = SQL_PATTERNS.GROUP_BY.test(text) ? `${text}, ${String(fragment)}` : `${text} GROUP BY ${String(fragment)}`;
14505
+ const frag = renderRawFragment(fragment, "groupByRaw(fragment)");
14506
+ text = SQL_PATTERNS.GROUP_BY.test(text) ? `${text}, ${frag}` : `${text} GROUP BY ${frag}`;
14478
14507
  built = null;
14479
14508
  return this;
14480
14509
  },
@@ -14506,15 +14535,15 @@ function createQueryBuilder(state) {
14506
14535
  return this;
14507
14536
  },
14508
14537
  havingRaw(fragment) {
14509
- assertSqlFragment(fragment, "havingRaw(fragment)");
14538
+ const frag = renderRawFragment(fragment, "havingRaw(fragment)");
14510
14539
  const kw = /\bHAVING\b/i.test(text) ? "AND" : "HAVING";
14511
- text += ` ${kw} ${String(fragment)}`;
14540
+ text += ` ${kw} ${frag}`;
14512
14541
  built = null;
14513
14542
  return this;
14514
14543
  },
14515
14544
  orderByRaw(fragment) {
14516
- assertSqlFragment(fragment, "orderByRaw(fragment)");
14517
- text = SQL_PATTERNS.ORDER_BY.test(text) ? `${text}, ${String(fragment)}` : `${text} ORDER BY ${String(fragment)}`;
14545
+ const frag = renderRawFragment(fragment, "orderByRaw(fragment)");
14546
+ text = SQL_PATTERNS.ORDER_BY.test(text) ? `${text}, ${frag}` : `${text} ORDER BY ${frag}`;
14518
14547
  built = null;
14519
14548
  return this;
14520
14549
  },
@@ -14697,20 +14726,20 @@ function createQueryBuilder(state) {
14697
14726
  includeTrashed = true;
14698
14727
  onlyTrashed = true;
14699
14728
  const softDeleteColumn = config5.softDeletes?.column || "deleted_at";
14700
- const splice = (raw, predicate2) => {
14701
- const upper = raw.toUpperCase();
14729
+ const splice = (raw2, predicate2) => {
14730
+ const upper = raw2.toUpperCase();
14702
14731
  let depth = 0;
14703
- for (let i = 0;i < raw.length; i++) {
14704
- const c = raw[i];
14732
+ for (let i = 0;i < raw2.length; i++) {
14733
+ const c = raw2[i];
14705
14734
  if (c === "(")
14706
14735
  depth++;
14707
14736
  else if (c === ")")
14708
14737
  depth--;
14709
- else if (depth === 0 && upper.substring(i, i + 5) === "WHERE" && (i === 0 || /\s/.test(raw[i - 1] ?? "")) && /\s/.test(raw[i + 5] ?? "")) {
14710
- return `${raw.substring(0, i)}WHERE ${predicate2} AND ${raw.substring(i + 6)}`;
14738
+ else if (depth === 0 && upper.substring(i, i + 5) === "WHERE" && (i === 0 || /\s/.test(raw2[i - 1] ?? "")) && /\s/.test(raw2[i + 5] ?? "")) {
14739
+ return `${raw2.substring(0, i)}WHERE ${predicate2} AND ${raw2.substring(i + 6)}`;
14711
14740
  }
14712
14741
  }
14713
- return `${raw} WHERE ${predicate2}`;
14742
+ return `${raw2} WHERE ${predicate2}`;
14714
14743
  };
14715
14744
  const predicate = `${table}.${softDeleteColumn} IS NOT NULL`;
14716
14745
  text = splice(text, predicate);
@@ -15014,13 +15043,13 @@ function createQueryBuilder(state) {
15014
15043
  const cacheKey = `${String(table)}|${prop}`;
15015
15044
  let chosen = dynamicWhereColumnCache.get(cacheKey);
15016
15045
  if (chosen === undefined) {
15017
- const raw = prop.replace(/^(?:or|and)?where/i, "");
15018
- if (!raw) {
15046
+ const raw2 = prop.replace(/^(?:or|and)?where/i, "");
15047
+ if (!raw2) {
15019
15048
  dynamicWhereColumnCache.set(cacheKey, "");
15020
15049
  chosen = "";
15021
15050
  } else {
15022
- const lowerFirst = raw.charAt(0).toLowerCase() + raw.slice(1);
15023
- const snake = raw.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
15051
+ const lowerFirst = raw2.charAt(0).toLowerCase() + raw2.slice(1);
15052
+ const snake = raw2.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
15024
15053
  const available = schema ? Object.keys(schema[String(table)]?.columns ?? {}) : [];
15025
15054
  chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
15026
15055
  dynamicWhereColumnCache.set(cacheKey, chosen);
@@ -15410,6 +15439,10 @@ function createQueryBuilder(state) {
15410
15439
  returning(...cols) {
15411
15440
  const returningSql = `${sqlText} RETURNING ${cols.join(", ")}`;
15412
15441
  const q = _sql.unsafe(returningSql, params);
15442
+ const runFirst = async () => {
15443
+ const rows = await runWithHooks(q, "insert");
15444
+ return Array.isArray(rows) ? rows[0] : rows;
15445
+ };
15413
15446
  return {
15414
15447
  where: () => this,
15415
15448
  andWhere: () => this,
@@ -15418,7 +15451,22 @@ function createQueryBuilder(state) {
15418
15451
  limit: () => this,
15419
15452
  offset: () => this,
15420
15453
  toSQL: () => makeExecutableQuery(q, returningSql),
15421
- execute: () => runWithHooks(q, "insert")
15454
+ execute: () => runWithHooks(q, "insert"),
15455
+ get: () => runWithHooks(q, "insert"),
15456
+ first: runFirst,
15457
+ executeTakeFirst: runFirst,
15458
+ async firstOrFail() {
15459
+ const row = await runFirst();
15460
+ if (!row)
15461
+ throw new Error("Insert with RETURNING returned no rows");
15462
+ return row;
15463
+ },
15464
+ async executeTakeFirstOrThrow() {
15465
+ const row = await runFirst();
15466
+ if (!row)
15467
+ throw new Error("Insert with RETURNING returned no rows");
15468
+ return row;
15469
+ }
15422
15470
  };
15423
15471
  },
15424
15472
  toSQL() {
@@ -15457,19 +15505,27 @@ function createQueryBuilder(state) {
15457
15505
  returningAll() {
15458
15506
  const returningSql = `${sqlText} RETURNING *`;
15459
15507
  const q = _sql.unsafe(returningSql, params);
15508
+ const runFirst = async () => {
15509
+ const result = await runWithHooks(q, "insert");
15510
+ return Array.isArray(result) ? result[0] : result;
15511
+ };
15460
15512
  return {
15461
15513
  toSQL: () => makeExecutableQuery(q, returningSql),
15462
15514
  execute: () => runWithHooks(q, "insert"),
15463
- async executeTakeFirst() {
15464
- const result = await runWithHooks(q, "insert");
15465
- return Array.isArray(result) ? result[0] : result;
15515
+ get: () => runWithHooks(q, "insert"),
15516
+ first: runFirst,
15517
+ executeTakeFirst: runFirst,
15518
+ async firstOrFail() {
15519
+ const row = await runFirst();
15520
+ if (!row)
15521
+ throw new Error("Insert with RETURNING returned no rows");
15522
+ return row;
15466
15523
  },
15467
15524
  async executeTakeFirstOrThrow() {
15468
- const result = await runWithHooks(q, "insert");
15469
- const first = Array.isArray(result) ? result[0] : result;
15470
- if (!first)
15471
- throw new Error("Insert with RETURNING failed");
15472
- return first;
15525
+ const row = await runFirst();
15526
+ if (!row)
15527
+ throw new Error("Insert with RETURNING returned no rows");
15528
+ return row;
15473
15529
  }
15474
15530
  };
15475
15531
  }
@@ -15533,6 +15589,10 @@ function createQueryBuilder(state) {
15533
15589
  returning(...cols) {
15534
15590
  const retText = `${sqlText} RETURNING ${cols.join(", ")}`;
15535
15591
  const q = params.length > 0 ? _sql.unsafe(retText, params) : _sql.unsafe(retText);
15592
+ const runFirst = async () => {
15593
+ const rows = await runWithHooks(q, "update");
15594
+ return Array.isArray(rows) ? rows[0] : rows;
15595
+ };
15536
15596
  const obj = {
15537
15597
  where: () => obj,
15538
15598
  andWhere: () => obj,
@@ -15541,7 +15601,22 @@ function createQueryBuilder(state) {
15541
15601
  limit: () => obj,
15542
15602
  offset: () => obj,
15543
15603
  toSQL: () => makeExecutableQuery(q, retText),
15544
- execute: () => runWithHooks(q, "update")
15604
+ execute: () => runWithHooks(q, "update"),
15605
+ get: () => runWithHooks(q, "update"),
15606
+ first: runFirst,
15607
+ executeTakeFirst: runFirst,
15608
+ async firstOrFail() {
15609
+ const row = await runFirst();
15610
+ if (!row)
15611
+ throw new Error("Update with RETURNING returned no rows");
15612
+ return row;
15613
+ },
15614
+ async executeTakeFirstOrThrow() {
15615
+ const row = await runFirst();
15616
+ if (!row)
15617
+ throw new Error("Update with RETURNING returned no rows");
15618
+ return row;
15619
+ }
15545
15620
  };
15546
15621
  return obj;
15547
15622
  },
@@ -15640,6 +15715,10 @@ function createQueryBuilder(state) {
15640
15715
  returning(...cols) {
15641
15716
  const retText = `${sqlText} RETURNING ${cols.join(", ")}`;
15642
15717
  const q = delParams.length > 0 ? _sql.unsafe(retText, delParams) : _sql.unsafe(retText);
15718
+ const runFirst = async () => {
15719
+ const rows = await runWithHooks(q, "delete");
15720
+ return Array.isArray(rows) ? rows[0] : rows;
15721
+ };
15643
15722
  const obj = {
15644
15723
  where: () => obj,
15645
15724
  andWhere: () => obj,
@@ -15648,7 +15727,22 @@ function createQueryBuilder(state) {
15648
15727
  limit: () => obj,
15649
15728
  offset: () => obj,
15650
15729
  toSQL: () => makeExecutableQuery(q, retText),
15651
- execute: () => runWithHooks(q, "delete")
15730
+ execute: () => runWithHooks(q, "delete"),
15731
+ get: () => runWithHooks(q, "delete"),
15732
+ first: runFirst,
15733
+ executeTakeFirst: runFirst,
15734
+ async firstOrFail() {
15735
+ const row = await runFirst();
15736
+ if (!row)
15737
+ throw new Error("Delete with RETURNING returned no rows");
15738
+ return row;
15739
+ },
15740
+ async executeTakeFirstOrThrow() {
15741
+ const row = await runFirst();
15742
+ if (!row)
15743
+ throw new Error("Delete with RETURNING returned no rows");
15744
+ return row;
15745
+ }
15652
15746
  };
15653
15747
  return obj;
15654
15748
  },
@@ -18000,8 +18094,8 @@ function loadPlanSnapshot(workspaceRoot, dialect) {
18000
18094
  return;
18001
18095
  }
18002
18096
  try {
18003
- const raw = readFileSync3(snapshotPath, "utf8");
18004
- const parsed = JSON.parse(raw);
18097
+ const raw2 = readFileSync3(snapshotPath, "utf8");
18098
+ const parsed = JSON.parse(raw2);
18005
18099
  if (parsed?.plan && Array.isArray(parsed.plan.tables) && parsed.plan.dialect) {
18006
18100
  return parsed.plan;
18007
18101
  }
@@ -18060,8 +18154,8 @@ async function generateMigration(dir, opts = {}) {
18060
18154
  const statePath = String(opts.state || defaultStatePath);
18061
18155
  if (existsSync16(statePath)) {
18062
18156
  try {
18063
- const raw = readFileSync3(statePath, "utf8");
18064
- const parsed = JSON.parse(raw);
18157
+ const raw2 = readFileSync3(statePath, "utf8");
18158
+ const parsed = JSON.parse(raw2);
18065
18159
  previous = parsed?.plan && parsed.plan.tables ? parsed.plan : parsed?.tables ? parsed : undefined;
18066
18160
  if (previous) {
18067
18161
  info("-- Comparing with legacy state file (will migrate to new snapshot format)");
@@ -24651,10 +24745,10 @@ class BelongsToManyRelationBuilder {
24651
24745
  hydrateRows(rows) {
24652
24746
  const fkParent = this.fkParent;
24653
24747
  const fkRelated = this.fkRelated;
24654
- return rows.map((raw) => {
24748
+ return rows.map((raw2) => {
24655
24749
  const relatedRow = {};
24656
24750
  const pivotExtras = {};
24657
- for (const [k2, v2] of Object.entries(raw)) {
24751
+ for (const [k2, v2] of Object.entries(raw2)) {
24658
24752
  if (k2.startsWith(BTM_RELATED_ALIAS))
24659
24753
  relatedRow[k2.slice(BTM_RELATED_ALIAS.length)] = v2;
24660
24754
  else if (k2 !== fkParent && k2 !== fkRelated)
@@ -29120,11 +29214,11 @@ Received ${signal}, cleaning up...`);
29120
29214
  argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
29121
29215
  argv = argv.slice(0, doubleDashesIndex);
29122
29216
  }
29123
- const raw = parseArgv(argv, mriOptions);
29124
- const parsed = { _: raw._ };
29125
- for (const name of Object.keys(raw)) {
29217
+ const raw2 = parseArgv(argv, mriOptions);
29218
+ const parsed = { _: raw2._ };
29219
+ for (const name of Object.keys(raw2)) {
29126
29220
  if (name !== "_") {
29127
- parsed[camelcaseOptionName(name)] = raw[name];
29221
+ parsed[camelcaseOptionName(name)] = raw2[name];
29128
29222
  }
29129
29223
  }
29130
29224
  const args = parsed._;
@@ -29169,11 +29263,11 @@ Received ${signal}, cleaning up...`);
29169
29263
  if (!isUsage)
29170
29264
  return;
29171
29265
  const e2 = err;
29172
- const raw = e2.message ?? "command-line error";
29266
+ const raw2 = e2.message ?? "command-line error";
29173
29267
  const label = this.name ? `${this.name}: ` : "";
29174
- const suffix = /--help/.test(raw) ? "" : `
29268
+ const suffix = /--help/.test(raw2) ? "" : `
29175
29269
  Run \`${this.name ?? "cli"} --help\` for usage.`;
29176
- process52.stderr.write(`${label}${raw}${suffix}
29270
+ process52.stderr.write(`${label}${raw2}${suffix}
29177
29271
  `);
29178
29272
  process52.exit(e2.exitCode ?? 2);
29179
29273
  }
@@ -30031,7 +30125,7 @@ function getPrefix() {
30031
30125
  }
30032
30126
  var prefix = getPrefix();
30033
30127
  // package.json
30034
- var version2 = "0.1.31";
30128
+ var version2 = "0.1.32";
30035
30129
 
30036
30130
  // bin/cli.ts
30037
30131
  init_actions();
package/dist/client.d.ts CHANGED
@@ -2,6 +2,33 @@ import { config } from './config';
2
2
  import { resetConnection } from './db';
3
3
  import type { DatabaseSchema } from './schema';
4
4
  import type { SchemaMeta } from './meta';
5
+ /**
6
+ * # `raw`
7
+ *
8
+ * Build a raw SQL fragment for the `*Raw` builder methods (`whereRaw`,
9
+ * `selectRaw`, `orderByRaw`, `groupByRaw`, `havingRaw`) and `select()`.
10
+ *
11
+ * Use this INSTEAD of a Bun `sql\`...\`` tag: a Bun query object cannot be
12
+ * converted back to SQL text (it stringifies to "[object Promise]"), so it
13
+ * silently corrupts the generated SQL. `raw` returns a `{ raw: string }`
14
+ * fragment that the builder renders correctly and that satisfies the
15
+ * `SqlFragment` type (so it passes the bare-string injection guard).
16
+ *
17
+ * Interpolated values in the tagged-template form are SQL-escaped (strings
18
+ * single-quote-doubled, dates → ISO, numbers/booleans/null inlined) — the
19
+ * same escaping the relation-subquery builders use. For user input that must
20
+ * be parameterised, prefer the typed `where(...)` methods over `raw`.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { raw } from 'bun-query-builder'
25
+ * db.selectFrom('users').selectRaw(raw`count(*) as c`)
26
+ * db.selectFrom('users').whereRaw(raw('age > 18'))
27
+ * db.selectFrom('users').orderByRaw(raw`created_at desc`)
28
+ * db.selectFrom('orders').whereRaw(raw`status = ${userStatus}`) // value escaped
29
+ * ```
30
+ */
31
+ export declare function raw(strings: TemplateStringsArray | string, ...values: unknown[]): RawExpression;
5
32
  // eslint-disable-next-line pickier/no-unused-vars
6
33
  export declare function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Partial<InternalState>): QueryBuilder<DB>;
7
34
  /**
@@ -26,6 +53,10 @@ export declare function clearQueryCache(): void;
26
53
  * ```
27
54
  */
28
55
  export declare function setQueryCacheMaxSize(size: number): void;
56
+ // Type guard for raw SQL expressions
57
+ declare interface RawExpression {
58
+ raw: string
59
+ }
29
60
  export declare interface WhereRaw {
30
61
  raw: any
31
62
  }
package/dist/src/index.js CHANGED
@@ -12344,6 +12344,14 @@ function* iterateAllPivots(meta, options = {}) {
12344
12344
  function isRawExpression(expr) {
12345
12345
  return typeof expr === "object" && expr !== null && "raw" in expr && typeof expr.raw === "string";
12346
12346
  }
12347
+ function raw(strings, ...values) {
12348
+ if (typeof strings === "string")
12349
+ return { raw: strings };
12350
+ let out = strings[0];
12351
+ for (let i = 0;i < values.length; i++)
12352
+ out += formatSubqueryValue(values[i]) + strings[i + 1];
12353
+ return { raw: out };
12354
+ }
12347
12355
  function quoteInsertIdent(id) {
12348
12356
  return config5.dialect === "mysql" ? `\`${id.replace(/`/g, "``")}\`` : `"${id.replace(/"/g, '""')}"`;
12349
12357
  }
@@ -12412,13 +12420,31 @@ function validateQualifiedIdentifier(value, context) {
12412
12420
  throw new TypeError(`[query-builder] ${context}: identifier segment '${part}' contains characters outside [A-Za-z0-9_]`);
12413
12421
  }
12414
12422
  }
12415
- function assertSqlFragment(fragment, context) {
12416
- if (fragment === null || fragment === undefined) {
12417
- throw new TypeError(`[query-builder] ${context}: fragment must be a SqlFragment, got ${fragment}`);
12418
- }
12423
+ function renderRawFragment(fragment, context) {
12419
12424
  if (typeof fragment === "string") {
12420
12425
  warnOnceBareSqlFragment(context);
12426
+ return fragment;
12421
12427
  }
12428
+ if (fragment === null || fragment === undefined)
12429
+ throw new TypeError(`[query-builder] ${context}: fragment must be a SqlFragment, got ${fragment}`);
12430
+ if (isRawExpression(fragment))
12431
+ return fragment.raw;
12432
+ if (typeof fragment === "object") {
12433
+ const f = fragment;
12434
+ if (typeof f.raw === "string")
12435
+ return f.raw;
12436
+ if (typeof f.raw === "function") {
12437
+ const r = f.raw();
12438
+ if (typeof r === "string")
12439
+ return r;
12440
+ }
12441
+ if (typeof f.sql === "string")
12442
+ return f.sql;
12443
+ const s = String(fragment);
12444
+ if (s !== "[object Object]" && s !== "[object Promise]")
12445
+ return s;
12446
+ }
12447
+ throw new TypeError(`[query-builder] ${context}: cannot render this value as a SQL fragment. ` + `A Bun \`sql\`...\`\` query object cannot be converted to SQL text \u2014 pass a ` + `string, or use the exported \`raw\` helper: raw\`count(*) as c\` / raw('age > 18').`);
12422
12448
  }
12423
12449
  function warnOnceBareSqlFragment(context) {
12424
12450
  if (warnedSqlFragmentContexts.has(context))
@@ -13093,12 +13119,12 @@ function createQueryBuilder(state) {
13093
13119
  return this;
13094
13120
  },
13095
13121
  selectRaw(fragment) {
13096
- assertSqlFragment(fragment, "selectRaw(fragment)");
13122
+ const frag = renderRawFragment(fragment, "selectRaw(fragment)");
13097
13123
  const fromIdx = text.indexOf(" FROM ");
13098
13124
  if (fromIdx !== -1) {
13099
- text = `${text.substring(0, fromIdx)}, ${String(fragment)}${text.substring(fromIdx)}`;
13125
+ text = `${text.substring(0, fromIdx)}, ${frag}${text.substring(fromIdx)}`;
13100
13126
  } else {
13101
- text += `, ${String(fragment)}`;
13127
+ text += `, ${frag}`;
13102
13128
  }
13103
13129
  built = null;
13104
13130
  return this;
@@ -13211,12 +13237,15 @@ function createQueryBuilder(state) {
13211
13237
  if (cols.length === 0)
13212
13238
  return this;
13213
13239
  const rendered = cols.map(renderSelectColumn);
13240
+ const distinctMatch = /^SELECT\s+(DISTINCT(?:\s+ON\s+\([^)]*\))?\s+)/i.exec(text);
13241
+ const distinctPrefix = distinctMatch ? distinctMatch[1] : "";
13214
13242
  const fromIndex = text.indexOf(" FROM ");
13215
13243
  if (fromIndex !== -1) {
13216
- text = `SELECT ${rendered.join(", ")}${text.substring(fromIndex)}`;
13244
+ text = `SELECT ${distinctPrefix}${rendered.join(", ")}${text.substring(fromIndex)}`;
13217
13245
  } else {
13218
- text = `SELECT ${rendered.join(", ")} FROM ${table}`;
13246
+ text = `SELECT ${distinctPrefix}${rendered.join(", ")} FROM ${table}`;
13219
13247
  }
13248
+ built = null;
13220
13249
  return this;
13221
13250
  },
13222
13251
  addSelect(...columns2) {
@@ -14105,9 +14134,9 @@ function createQueryBuilder(state) {
14105
14134
  return this;
14106
14135
  },
14107
14136
  whereRaw(fragment) {
14108
- assertSqlFragment(fragment, "whereRaw(fragment)");
14137
+ const frag = renderRawFragment(fragment, "whereRaw(fragment)");
14109
14138
  const keyword = SQL_PATTERNS.WHERE.test(text) ? "AND" : "WHERE";
14110
- text += ` ${keyword} ${String(fragment)}`;
14139
+ text += ` ${keyword} ${frag}`;
14111
14140
  built = null;
14112
14141
  return this;
14113
14142
  },
@@ -14473,8 +14502,8 @@ function createQueryBuilder(state) {
14473
14502
  return this;
14474
14503
  },
14475
14504
  groupByRaw(fragment) {
14476
- assertSqlFragment(fragment, "groupByRaw(fragment)");
14477
- text = SQL_PATTERNS.GROUP_BY.test(text) ? `${text}, ${String(fragment)}` : `${text} GROUP BY ${String(fragment)}`;
14505
+ const frag = renderRawFragment(fragment, "groupByRaw(fragment)");
14506
+ text = SQL_PATTERNS.GROUP_BY.test(text) ? `${text}, ${frag}` : `${text} GROUP BY ${frag}`;
14478
14507
  built = null;
14479
14508
  return this;
14480
14509
  },
@@ -14506,15 +14535,15 @@ function createQueryBuilder(state) {
14506
14535
  return this;
14507
14536
  },
14508
14537
  havingRaw(fragment) {
14509
- assertSqlFragment(fragment, "havingRaw(fragment)");
14538
+ const frag = renderRawFragment(fragment, "havingRaw(fragment)");
14510
14539
  const kw = /\bHAVING\b/i.test(text) ? "AND" : "HAVING";
14511
- text += ` ${kw} ${String(fragment)}`;
14540
+ text += ` ${kw} ${frag}`;
14512
14541
  built = null;
14513
14542
  return this;
14514
14543
  },
14515
14544
  orderByRaw(fragment) {
14516
- assertSqlFragment(fragment, "orderByRaw(fragment)");
14517
- text = SQL_PATTERNS.ORDER_BY.test(text) ? `${text}, ${String(fragment)}` : `${text} ORDER BY ${String(fragment)}`;
14545
+ const frag = renderRawFragment(fragment, "orderByRaw(fragment)");
14546
+ text = SQL_PATTERNS.ORDER_BY.test(text) ? `${text}, ${frag}` : `${text} ORDER BY ${frag}`;
14518
14547
  built = null;
14519
14548
  return this;
14520
14549
  },
@@ -14697,20 +14726,20 @@ function createQueryBuilder(state) {
14697
14726
  includeTrashed = true;
14698
14727
  onlyTrashed = true;
14699
14728
  const softDeleteColumn = config5.softDeletes?.column || "deleted_at";
14700
- const splice = (raw, predicate2) => {
14701
- const upper = raw.toUpperCase();
14729
+ const splice = (raw2, predicate2) => {
14730
+ const upper = raw2.toUpperCase();
14702
14731
  let depth = 0;
14703
- for (let i = 0;i < raw.length; i++) {
14704
- const c = raw[i];
14732
+ for (let i = 0;i < raw2.length; i++) {
14733
+ const c = raw2[i];
14705
14734
  if (c === "(")
14706
14735
  depth++;
14707
14736
  else if (c === ")")
14708
14737
  depth--;
14709
- else if (depth === 0 && upper.substring(i, i + 5) === "WHERE" && (i === 0 || /\s/.test(raw[i - 1] ?? "")) && /\s/.test(raw[i + 5] ?? "")) {
14710
- return `${raw.substring(0, i)}WHERE ${predicate2} AND ${raw.substring(i + 6)}`;
14738
+ else if (depth === 0 && upper.substring(i, i + 5) === "WHERE" && (i === 0 || /\s/.test(raw2[i - 1] ?? "")) && /\s/.test(raw2[i + 5] ?? "")) {
14739
+ return `${raw2.substring(0, i)}WHERE ${predicate2} AND ${raw2.substring(i + 6)}`;
14711
14740
  }
14712
14741
  }
14713
- return `${raw} WHERE ${predicate2}`;
14742
+ return `${raw2} WHERE ${predicate2}`;
14714
14743
  };
14715
14744
  const predicate = `${table}.${softDeleteColumn} IS NOT NULL`;
14716
14745
  text = splice(text, predicate);
@@ -15014,13 +15043,13 @@ function createQueryBuilder(state) {
15014
15043
  const cacheKey = `${String(table)}|${prop}`;
15015
15044
  let chosen = dynamicWhereColumnCache.get(cacheKey);
15016
15045
  if (chosen === undefined) {
15017
- const raw = prop.replace(/^(?:or|and)?where/i, "");
15018
- if (!raw) {
15046
+ const raw2 = prop.replace(/^(?:or|and)?where/i, "");
15047
+ if (!raw2) {
15019
15048
  dynamicWhereColumnCache.set(cacheKey, "");
15020
15049
  chosen = "";
15021
15050
  } else {
15022
- const lowerFirst = raw.charAt(0).toLowerCase() + raw.slice(1);
15023
- const snake = raw.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
15051
+ const lowerFirst = raw2.charAt(0).toLowerCase() + raw2.slice(1);
15052
+ const snake = raw2.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
15024
15053
  const available = schema ? Object.keys(schema[String(table)]?.columns ?? {}) : [];
15025
15054
  chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
15026
15055
  dynamicWhereColumnCache.set(cacheKey, chosen);
@@ -15410,6 +15439,10 @@ function createQueryBuilder(state) {
15410
15439
  returning(...cols) {
15411
15440
  const returningSql = `${sqlText} RETURNING ${cols.join(", ")}`;
15412
15441
  const q = _sql.unsafe(returningSql, params);
15442
+ const runFirst = async () => {
15443
+ const rows = await runWithHooks(q, "insert");
15444
+ return Array.isArray(rows) ? rows[0] : rows;
15445
+ };
15413
15446
  return {
15414
15447
  where: () => this,
15415
15448
  andWhere: () => this,
@@ -15418,7 +15451,22 @@ function createQueryBuilder(state) {
15418
15451
  limit: () => this,
15419
15452
  offset: () => this,
15420
15453
  toSQL: () => makeExecutableQuery(q, returningSql),
15421
- execute: () => runWithHooks(q, "insert")
15454
+ execute: () => runWithHooks(q, "insert"),
15455
+ get: () => runWithHooks(q, "insert"),
15456
+ first: runFirst,
15457
+ executeTakeFirst: runFirst,
15458
+ async firstOrFail() {
15459
+ const row = await runFirst();
15460
+ if (!row)
15461
+ throw new Error("Insert with RETURNING returned no rows");
15462
+ return row;
15463
+ },
15464
+ async executeTakeFirstOrThrow() {
15465
+ const row = await runFirst();
15466
+ if (!row)
15467
+ throw new Error("Insert with RETURNING returned no rows");
15468
+ return row;
15469
+ }
15422
15470
  };
15423
15471
  },
15424
15472
  toSQL() {
@@ -15457,19 +15505,27 @@ function createQueryBuilder(state) {
15457
15505
  returningAll() {
15458
15506
  const returningSql = `${sqlText} RETURNING *`;
15459
15507
  const q = _sql.unsafe(returningSql, params);
15508
+ const runFirst = async () => {
15509
+ const result = await runWithHooks(q, "insert");
15510
+ return Array.isArray(result) ? result[0] : result;
15511
+ };
15460
15512
  return {
15461
15513
  toSQL: () => makeExecutableQuery(q, returningSql),
15462
15514
  execute: () => runWithHooks(q, "insert"),
15463
- async executeTakeFirst() {
15464
- const result = await runWithHooks(q, "insert");
15465
- return Array.isArray(result) ? result[0] : result;
15515
+ get: () => runWithHooks(q, "insert"),
15516
+ first: runFirst,
15517
+ executeTakeFirst: runFirst,
15518
+ async firstOrFail() {
15519
+ const row = await runFirst();
15520
+ if (!row)
15521
+ throw new Error("Insert with RETURNING returned no rows");
15522
+ return row;
15466
15523
  },
15467
15524
  async executeTakeFirstOrThrow() {
15468
- const result = await runWithHooks(q, "insert");
15469
- const first = Array.isArray(result) ? result[0] : result;
15470
- if (!first)
15471
- throw new Error("Insert with RETURNING failed");
15472
- return first;
15525
+ const row = await runFirst();
15526
+ if (!row)
15527
+ throw new Error("Insert with RETURNING returned no rows");
15528
+ return row;
15473
15529
  }
15474
15530
  };
15475
15531
  }
@@ -15533,6 +15589,10 @@ function createQueryBuilder(state) {
15533
15589
  returning(...cols) {
15534
15590
  const retText = `${sqlText} RETURNING ${cols.join(", ")}`;
15535
15591
  const q = params.length > 0 ? _sql.unsafe(retText, params) : _sql.unsafe(retText);
15592
+ const runFirst = async () => {
15593
+ const rows = await runWithHooks(q, "update");
15594
+ return Array.isArray(rows) ? rows[0] : rows;
15595
+ };
15536
15596
  const obj = {
15537
15597
  where: () => obj,
15538
15598
  andWhere: () => obj,
@@ -15541,7 +15601,22 @@ function createQueryBuilder(state) {
15541
15601
  limit: () => obj,
15542
15602
  offset: () => obj,
15543
15603
  toSQL: () => makeExecutableQuery(q, retText),
15544
- execute: () => runWithHooks(q, "update")
15604
+ execute: () => runWithHooks(q, "update"),
15605
+ get: () => runWithHooks(q, "update"),
15606
+ first: runFirst,
15607
+ executeTakeFirst: runFirst,
15608
+ async firstOrFail() {
15609
+ const row = await runFirst();
15610
+ if (!row)
15611
+ throw new Error("Update with RETURNING returned no rows");
15612
+ return row;
15613
+ },
15614
+ async executeTakeFirstOrThrow() {
15615
+ const row = await runFirst();
15616
+ if (!row)
15617
+ throw new Error("Update with RETURNING returned no rows");
15618
+ return row;
15619
+ }
15545
15620
  };
15546
15621
  return obj;
15547
15622
  },
@@ -15640,6 +15715,10 @@ function createQueryBuilder(state) {
15640
15715
  returning(...cols) {
15641
15716
  const retText = `${sqlText} RETURNING ${cols.join(", ")}`;
15642
15717
  const q = delParams.length > 0 ? _sql.unsafe(retText, delParams) : _sql.unsafe(retText);
15718
+ const runFirst = async () => {
15719
+ const rows = await runWithHooks(q, "delete");
15720
+ return Array.isArray(rows) ? rows[0] : rows;
15721
+ };
15643
15722
  const obj = {
15644
15723
  where: () => obj,
15645
15724
  andWhere: () => obj,
@@ -15648,7 +15727,22 @@ function createQueryBuilder(state) {
15648
15727
  limit: () => obj,
15649
15728
  offset: () => obj,
15650
15729
  toSQL: () => makeExecutableQuery(q, retText),
15651
- execute: () => runWithHooks(q, "delete")
15730
+ execute: () => runWithHooks(q, "delete"),
15731
+ get: () => runWithHooks(q, "delete"),
15732
+ first: runFirst,
15733
+ executeTakeFirst: runFirst,
15734
+ async firstOrFail() {
15735
+ const row = await runFirst();
15736
+ if (!row)
15737
+ throw new Error("Delete with RETURNING returned no rows");
15738
+ return row;
15739
+ },
15740
+ async executeTakeFirstOrThrow() {
15741
+ const row = await runFirst();
15742
+ if (!row)
15743
+ throw new Error("Delete with RETURNING returned no rows");
15744
+ return row;
15745
+ }
15652
15746
  };
15653
15747
  return obj;
15654
15748
  },
@@ -18000,8 +18094,8 @@ function loadPlanSnapshot(workspaceRoot, dialect) {
18000
18094
  return;
18001
18095
  }
18002
18096
  try {
18003
- const raw = readFileSync3(snapshotPath, "utf8");
18004
- const parsed = JSON.parse(raw);
18097
+ const raw2 = readFileSync3(snapshotPath, "utf8");
18098
+ const parsed = JSON.parse(raw2);
18005
18099
  if (parsed?.plan && Array.isArray(parsed.plan.tables) && parsed.plan.dialect) {
18006
18100
  return parsed.plan;
18007
18101
  }
@@ -18060,8 +18154,8 @@ async function generateMigration(dir, opts = {}) {
18060
18154
  const statePath = String(opts.state || defaultStatePath);
18061
18155
  if (existsSync16(statePath)) {
18062
18156
  try {
18063
- const raw = readFileSync3(statePath, "utf8");
18064
- const parsed = JSON.parse(raw);
18157
+ const raw2 = readFileSync3(statePath, "utf8");
18158
+ const parsed = JSON.parse(raw2);
18065
18159
  previous = parsed?.plan && parsed.plan.tables ? parsed.plan : parsed?.tables ? parsed : undefined;
18066
18160
  if (previous) {
18067
18161
  info("-- Comparing with legacy state file (will migrate to new snapshot format)");
@@ -24651,10 +24745,10 @@ class BelongsToManyRelationBuilder {
24651
24745
  hydrateRows(rows) {
24652
24746
  const fkParent = this.fkParent;
24653
24747
  const fkRelated = this.fkRelated;
24654
- return rows.map((raw) => {
24748
+ return rows.map((raw2) => {
24655
24749
  const relatedRow = {};
24656
24750
  const pivotExtras = {};
24657
- for (const [k2, v2] of Object.entries(raw)) {
24751
+ for (const [k2, v2] of Object.entries(raw2)) {
24658
24752
  if (k2.startsWith(BTM_RELATED_ALIAS))
24659
24753
  relatedRow[k2.slice(BTM_RELATED_ALIAS.length)] = v2;
24660
24754
  else if (k2 !== fkParent && k2 !== fkRelated)
@@ -28021,6 +28115,7 @@ export {
28021
28115
  relationDiagram,
28022
28116
  registerModel,
28023
28117
  registerBrowserModels,
28118
+ raw,
28024
28119
  queryExplainAll,
28025
28120
  ping,
28026
28121
  parseStacksModels,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-query-builder",
3
3
  "type": "module",
4
- "version": "0.1.31",
4
+ "version": "0.1.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",