bun-query-builder 0.1.29 → 0.1.31

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
@@ -12391,6 +12391,63 @@ function validateIdentifier(name, context) {
12391
12391
  throw new Error(`[query-builder] Invalid identifier${contextMsg}: '${name}'. Identifiers must start with a letter or underscore and contain only alphanumeric characters, underscores, and dots.`);
12392
12392
  }
12393
12393
  }
12394
+ function assertSafeWhereOperator(op, context) {
12395
+ if (typeof op !== "string")
12396
+ throw new TypeError(`[query-builder] ${context}: operator must be a string, got ${typeof op}`);
12397
+ const lower = op.toLowerCase();
12398
+ if (!SAFE_WHERE_OPERATORS.has(lower))
12399
+ throw new TypeError(`[query-builder] ${context}: refusing to use '${op}' as a SQL operator \u2014 not in the allowed set (${[...SAFE_WHERE_OPERATORS].join(", ")})`);
12400
+ return op;
12401
+ }
12402
+ function validateQualifiedIdentifier(value, context) {
12403
+ if (typeof value !== "string" || value.length === 0)
12404
+ throw new TypeError(`[query-builder] ${context}: identifier must be a non-empty string, got ${typeof value}`);
12405
+ if (value.length > 129)
12406
+ throw new TypeError(`[query-builder] ${context}: identifier '${value}' too long`);
12407
+ const parts = value.split(".");
12408
+ if (parts.length > 2)
12409
+ throw new TypeError(`[query-builder] ${context}: identifier '${value}' has more than one dot \u2014 only \`table.column\` is allowed`);
12410
+ for (const part of parts) {
12411
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(part))
12412
+ throw new TypeError(`[query-builder] ${context}: identifier segment '${part}' contains characters outside [A-Za-z0-9_]`);
12413
+ }
12414
+ }
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
+ }
12419
+ if (typeof fragment === "string") {
12420
+ warnOnceBareSqlFragment(context);
12421
+ }
12422
+ }
12423
+ function warnOnceBareSqlFragment(context) {
12424
+ if (warnedSqlFragmentContexts.has(context))
12425
+ return;
12426
+ warnedSqlFragmentContexts.add(context);
12427
+ console.warn(`[query-builder] ${context}: bare string passed to a *Raw method. ` + `Prefer \`sql\`...\`\` tagged-template fragments so values are parameterised ` + `instead of concatenated \u2014 concatenating request input into SQL is an ` + `injection vector. This will become a hard error in a future release.`);
12428
+ }
12429
+ function formatSubqueryValue(val) {
12430
+ if (val === null)
12431
+ return "NULL";
12432
+ if (typeof val === "number" && Number.isFinite(val))
12433
+ return String(val);
12434
+ if (typeof val === "boolean")
12435
+ return val ? "1" : "0";
12436
+ if (typeof val === "string")
12437
+ return `'${val.replace(/'/g, "''")}'`;
12438
+ if (val instanceof Date)
12439
+ return `'${val.toISOString()}'`;
12440
+ throw new TypeError(`[query-builder] subquery condition: refusing to interpolate value of type ${typeof val}`);
12441
+ }
12442
+ function buildOverClause(partitionBy, orderBy) {
12443
+ const cols = Array.isArray(partitionBy) ? partitionBy : partitionBy ? [partitionBy] : [];
12444
+ const parts = [];
12445
+ if (cols.length)
12446
+ parts.push(`PARTITION BY ${cols.join(", ")}`);
12447
+ if (orderBy && orderBy.length)
12448
+ parts.push(`ORDER BY ${orderBy.map(([c, d]) => `${c} ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`);
12449
+ return parts.length ? `OVER (${parts.join(" ")})` : "OVER ()";
12450
+ }
12394
12451
 
12395
12452
  class QueryCache {
12396
12453
  cache = new Map;
@@ -12455,6 +12512,16 @@ function sleep(ms) {
12455
12512
  return new Promise((resolve13) => setTimeout(resolve13, ms));
12456
12513
  }
12457
12514
  function reorderSelectClauses(sql) {
12515
+ const hit = reorderCache.get(sql);
12516
+ if (hit !== undefined)
12517
+ return hit;
12518
+ const out = computeReorderedClauses(sql);
12519
+ if (reorderCache.size >= REORDER_CACHE_MAX)
12520
+ reorderCache.clear();
12521
+ reorderCache.set(sql, out);
12522
+ return out;
12523
+ }
12524
+ function computeReorderedClauses(sql) {
12458
12525
  const KEYWORDS = [
12459
12526
  { key: "GROUP_BY", tokens: /^GROUP\s+BY\b/i },
12460
12527
  { key: "ORDER_BY", tokens: /^ORDER\s+BY\b/i },
@@ -12540,6 +12607,7 @@ function createQueryBuilder(state) {
12540
12607
  const _sql = state?.sql ?? getOrCreateBunSql();
12541
12608
  const meta = state?.meta;
12542
12609
  const schema = state?.schema;
12610
+ const dynamicWhereColumnCache = new Map;
12543
12611
  function applyCondition(expr) {
12544
12612
  if (Array.isArray(expr)) {
12545
12613
  const [col, op, val] = expr;
@@ -12858,65 +12926,9 @@ function createQueryBuilder(state) {
12858
12926
  }
12859
12927
  }
12860
12928
  };
12861
- const buildOverClause = (partitionBy, orderBy) => {
12862
- const cols = Array.isArray(partitionBy) ? partitionBy : partitionBy ? [partitionBy] : [];
12863
- const parts = [];
12864
- if (cols.length)
12865
- parts.push(`PARTITION BY ${cols.join(", ")}`);
12866
- if (orderBy && orderBy.length)
12867
- parts.push(`ORDER BY ${orderBy.map(([c, d]) => `${c} ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`);
12868
- return parts.length ? `OVER (${parts.join(" ")})` : "OVER ()";
12869
- };
12870
12929
  const addWindowFunction = (fnExpr, alias, partitionBy, orderBy) => {
12871
12930
  addToSelectClause(`${fnExpr} ${buildOverClause(partitionBy, orderBy)} AS ${alias}`);
12872
12931
  };
12873
- function assertSafeWhereOperator(op, context) {
12874
- if (typeof op !== "string")
12875
- throw new TypeError(`[query-builder] ${context}: operator must be a string, got ${typeof op}`);
12876
- const lower = op.toLowerCase();
12877
- if (!SAFE_WHERE_OPERATORS.has(lower))
12878
- throw new TypeError(`[query-builder] ${context}: refusing to use '${op}' as a SQL operator \u2014 not in the allowed set (${[...SAFE_WHERE_OPERATORS].join(", ")})`);
12879
- return op;
12880
- }
12881
- function validateQualifiedIdentifier(value, context) {
12882
- if (typeof value !== "string" || value.length === 0)
12883
- throw new TypeError(`[query-builder] ${context}: identifier must be a non-empty string, got ${typeof value}`);
12884
- if (value.length > 129)
12885
- throw new TypeError(`[query-builder] ${context}: identifier '${value}' too long`);
12886
- const parts = value.split(".");
12887
- if (parts.length > 2)
12888
- throw new TypeError(`[query-builder] ${context}: identifier '${value}' has more than one dot \u2014 only \`table.column\` is allowed`);
12889
- for (const part of parts) {
12890
- if (!/^[A-Z_][A-Z0-9_]*$/i.test(part))
12891
- throw new TypeError(`[query-builder] ${context}: identifier segment '${part}' contains characters outside [A-Za-z0-9_]`);
12892
- }
12893
- }
12894
- function assertSqlFragment(fragment, context) {
12895
- if (fragment === null || fragment === undefined) {
12896
- throw new TypeError(`[query-builder] ${context}: fragment must be a SqlFragment, got ${fragment}`);
12897
- }
12898
- if (typeof fragment === "string") {
12899
- warnOnceBareSqlFragment(context);
12900
- }
12901
- }
12902
- const warnedSqlFragmentContexts = new Set;
12903
- function warnOnceBareSqlFragment(context) {
12904
- if (warnedSqlFragmentContexts.has(context))
12905
- return;
12906
- warnedSqlFragmentContexts.add(context);
12907
- console.warn(`[query-builder] ${context}: bare string passed to a *Raw method. ` + `Prefer \`sql\`...\`\` tagged-template fragments so values are parameterised ` + `instead of concatenated \u2014 concatenating request input into SQL is an ` + `injection vector. This will become a hard error in a future release.`);
12908
- }
12909
- function formatSubqueryValue(val) {
12910
- if (val === null)
12911
- return "NULL";
12912
- if (typeof val === "number" && Number.isFinite(val))
12913
- return String(val);
12914
- if (typeof val === "boolean")
12915
- return val ? "1" : "0";
12916
- if (typeof val === "string")
12917
- return `'${val.replace(/'/g, "''")}'`;
12918
- throw new TypeError(`[query-builder] subquery condition: refusing to interpolate value of type ${typeof val}`);
12919
- }
12920
12932
  const buildHasSubquery = (parentTable, targetTable, pk, callback) => {
12921
12933
  validateIdentifier(parentTable, "relationship subquery (parent table)");
12922
12934
  validateIdentifier(targetTable, "relationship subquery (target table)");
@@ -12949,7 +12961,7 @@ function createQueryBuilder(state) {
12949
12961
  const subQb = {
12950
12962
  where: (col, op, val) => {
12951
12963
  validateIdentifier(col, "relationship subquery condition");
12952
- return `${targetTable}.${col} ${op} ${typeof val === "string" ? `'${val}'` : val}`;
12964
+ return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
12953
12965
  }
12954
12966
  };
12955
12967
  const condition = callback(subQb);
@@ -12978,7 +12990,7 @@ function createQueryBuilder(state) {
12978
12990
  const subQb = {
12979
12991
  where: (col, op, val) => {
12980
12992
  validateIdentifier(col, "relationship subquery condition");
12981
- return `${targetTable}.${col} ${op} ${typeof val === "string" ? `'${val}'` : val}`;
12993
+ return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
12982
12994
  }
12983
12995
  };
12984
12996
  const condition = callback(subQb);
@@ -13282,6 +13294,54 @@ function createQueryBuilder(state) {
13282
13294
  }
13283
13295
  return "";
13284
13296
  };
13297
+ const buildJoinConstraint = (targetTbl) => {
13298
+ if (!condition)
13299
+ return "";
13300
+ const frags = [];
13301
+ const addCmp = (col, op, val) => {
13302
+ validateIdentifier(String(col), "with() constraint column");
13303
+ const operator = assertSafeWhereOperator(op, "with() constraint operator");
13304
+ frags.push(`${targetTbl}.${String(col)} ${operator} ${formatSubqueryValue(val)}`);
13305
+ };
13306
+ const unsupported = (m) => () => {
13307
+ throw new Error(`[query-builder] with('${relationKey}', ...): ${m} is not supported inside a constraint callback on the JOIN-based builder \u2014 apply it to the outer query, or use the model layer's eager loading. (Silently ignoring it would return wrong data.)`);
13308
+ };
13309
+ const constraintQb = {
13310
+ where: (expr, op, val) => {
13311
+ if (Array.isArray(expr))
13312
+ addCmp(expr[0], expr[1], expr[2]);
13313
+ else if (expr && typeof expr === "object")
13314
+ for (const k of Object.keys(expr))
13315
+ addCmp(k, "=", expr[k]);
13316
+ else if (op !== undefined && val !== undefined)
13317
+ addCmp(expr, op, val);
13318
+ else if (op !== undefined)
13319
+ addCmp(expr, "=", op);
13320
+ return constraintQb;
13321
+ },
13322
+ whereIn: (col, vals) => {
13323
+ validateIdentifier(String(col), "with() constraint column");
13324
+ frags.push(`${targetTbl}.${String(col)} IN (${vals.map(formatSubqueryValue).join(", ")})`);
13325
+ return constraintQb;
13326
+ },
13327
+ whereNull: (col) => {
13328
+ validateIdentifier(String(col), "with() constraint column");
13329
+ frags.push(`${targetTbl}.${String(col)} IS NULL`);
13330
+ return constraintQb;
13331
+ },
13332
+ whereNotNull: (col) => {
13333
+ validateIdentifier(String(col), "with() constraint column");
13334
+ frags.push(`${targetTbl}.${String(col)} IS NOT NULL`);
13335
+ return constraintQb;
13336
+ },
13337
+ orderBy: unsupported("orderBy()"),
13338
+ limit: unsupported("limit()"),
13339
+ offset: unsupported("offset()"),
13340
+ take: unsupported("take()")
13341
+ };
13342
+ condition(constraintQb);
13343
+ return frags.length ? ` AND ${frags.join(" AND ")}` : "";
13344
+ };
13285
13345
  const resolveTarget = () => {
13286
13346
  const pick = (m) => {
13287
13347
  const modelName = m?.[relationKey];
@@ -13326,7 +13386,7 @@ function createQueryBuilder(state) {
13326
13386
  const throughPk = meta.primaryKeys[throughTable] ?? "id";
13327
13387
  const fkInThrough = `${singularize(fromTable)}_id`;
13328
13388
  const fkInFinal = `${singularize(throughTable)}_id`;
13329
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(throughTable)} ON ${sql(`${throughTable}.${fkInThrough}`)} = ${sql(`${fromTable}.${fromPk}`)} LEFT JOIN ${sql(finalTable)} ON ${sql(`${finalTable}.${fkInFinal}`)} = ${sql(`${throughTable}.${throughPk}`)}`;
13389
+ insertJoin(`LEFT JOIN ${throughTable} ON ${throughTable}.${fkInThrough} = ${fromTable}.${fromPk} LEFT JOIN ${finalTable} ON ${finalTable}.${fkInFinal} = ${throughTable}.${throughPk}`);
13330
13390
  joinedTables.add(throughTable);
13331
13391
  joinedTables.add(finalTable);
13332
13392
  return finalTable;
@@ -13339,7 +13399,7 @@ function createQueryBuilder(state) {
13339
13399
  const childPk = meta.primaryKeys[childTable] ?? "id";
13340
13400
  const fkA = resolved?.fkParent ?? `${singularize(fromTable)}_id`;
13341
13401
  const fkB = resolved?.fkRelated ?? `${singularize(childTable)}_id`;
13342
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(pivot)} ON ${sql(`${pivot}.${fkA}`)} = ${sql(`${fromTable}.${fromPk}`)} LEFT JOIN ${sql(childTable)} ON ${sql(`${childTable}.${childPk}`)} = ${sql(`${pivot}.${fkB}`)}`;
13402
+ insertJoin(`LEFT JOIN ${pivot} ON ${pivot}.${fkA} = ${fromTable}.${fromPk} LEFT JOIN ${childTable} ON ${childTable}.${childPk} = ${pivot}.${fkB}${buildJoinConstraint(childTable)}`);
13343
13403
  joinedTables.add(pivot);
13344
13404
  joinedTables.add(childTable);
13345
13405
  return childTable;
@@ -13353,7 +13413,8 @@ function createQueryBuilder(state) {
13353
13413
  const morphType = `${morphName}_type`;
13354
13414
  const morphId = `${morphName}_id`;
13355
13415
  const targetFk = `${singularize(childTable)}_id`;
13356
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(pivotTable)} ON ${sql(`${pivotTable}.${morphId}`)} = ${sql(`${fromTable}.${fromPk}`)} AND ${sql(`${pivotTable}.${morphType}`)} = ${sql(meta.tableToModel[fromTable] || fromTable)} LEFT JOIN ${sql(childTable)} ON ${sql(`${childTable}.${childPk}`)} = ${sql(`${pivotTable}.${targetFk}`)}`;
13416
+ const morphVal = formatSubqueryValue(meta.tableToModel[fromTable] || fromTable);
13417
+ insertJoin(`LEFT JOIN ${pivotTable} ON ${pivotTable}.${morphId} = ${fromTable}.${fromPk} AND ${pivotTable}.${morphType} = ${morphVal} LEFT JOIN ${childTable} ON ${childTable}.${childPk} = ${pivotTable}.${targetFk}`);
13357
13418
  joinedTables.add(pivotTable);
13358
13419
  joinedTables.add(childTable);
13359
13420
  return childTable;
@@ -13369,7 +13430,8 @@ function createQueryBuilder(state) {
13369
13430
  const morphType = `${morphName}_type`;
13370
13431
  const morphId = `${morphName}_id`;
13371
13432
  const relatedFk = `${singularize(relatedTable)}_id`;
13372
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(pivotTable)} ON ${sql(`${pivotTable}.${relatedFk}`)} = ${sql(`${fromTable}.${fromPk}`)} LEFT JOIN ${sql(relatedTable)} ON ${sql(`${relatedTable}.${relatedPk}`)} = ${sql(`${pivotTable}.${morphId}`)} AND ${sql(`${pivotTable}.${morphType}`)} = ${sql(meta.tableToModel[relatedTable] || relatedTable)}`;
13433
+ const morphVal = formatSubqueryValue(meta.tableToModel[relatedTable] || relatedTable);
13434
+ insertJoin(`LEFT JOIN ${pivotTable} ON ${pivotTable}.${relatedFk} = ${fromTable}.${fromPk} LEFT JOIN ${relatedTable} ON ${relatedTable}.${relatedPk} = ${pivotTable}.${morphId} AND ${pivotTable}.${morphType} = ${morphVal}`);
13373
13435
  joinedTables.add(pivotTable);
13374
13436
  joinedTables.add(relatedTable);
13375
13437
  return relatedTable;
@@ -13378,7 +13440,7 @@ function createQueryBuilder(state) {
13378
13440
  if (isBt) {
13379
13441
  const fkInParent = `${singularize(childTable)}_id`;
13380
13442
  const childPk = meta.primaryKeys[childTable] ?? "id";
13381
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(childTable)} ON ${sql(`${fromTable}.${fkInParent}`)} = ${sql(`${childTable}.${childPk}`)}`;
13443
+ insertJoin(`LEFT JOIN ${childTable} ON ${fromTable}.${fkInParent} = ${childTable}.${childPk}${buildJoinConstraint(childTable)}`);
13382
13444
  joinedTables.add(childTable);
13383
13445
  return childTable;
13384
13446
  }
@@ -13388,20 +13450,15 @@ function createQueryBuilder(state) {
13388
13450
  const morphType = `${relationKey}_type`;
13389
13451
  const morphId = `${relationKey}_id`;
13390
13452
  const fromPk = meta.primaryKeys[fromTable] ?? "id";
13391
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(childTable)} ON ${sql(`${childTable}.${morphId}`)} = ${sql(`${fromTable}.${fromPk}`)} AND ${sql(`${childTable}.${morphType}`)} = ${sql(meta.tableToModel[fromTable] || fromTable)}`;
13453
+ const morphVal = formatSubqueryValue(meta.tableToModel[fromTable] || fromTable);
13454
+ insertJoin(`LEFT JOIN ${childTable} ON ${childTable}.${morphId} = ${fromTable}.${fromPk} AND ${childTable}.${morphType} = ${morphVal}`);
13392
13455
  joinedTables.add(childTable);
13393
13456
  return childTable;
13394
13457
  }
13395
13458
  const fkInChild = `${singularize(fromTable)}_id`;
13396
13459
  const pk = meta.primaryKeys[fromTable] ?? "id";
13397
- const softDeleteCheck = addSoftDeleteCheck(childTable);
13398
- if (softDeleteCheck) {
13399
- const currentSql = String(ensureBuilt());
13400
- const joinCondition = `${childTable}.${fkInChild} = ${fromTable}.${pk}${softDeleteCheck}`;
13401
- built = sql`${sql(currentSql)} LEFT JOIN ${sql(childTable)} ON ${sql(joinCondition)}`;
13402
- } else {
13403
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(childTable)} ON ${sql(`${childTable}.${fkInChild}`)} = ${sql(`${fromTable}.${pk}`)}`;
13404
- }
13460
+ const extraOn = `${addSoftDeleteCheck(childTable)}${buildJoinConstraint(childTable)}`;
13461
+ insertJoin(`LEFT JOIN ${childTable} ON ${childTable}.${fkInChild} = ${fromTable}.${pk}${extraOn}`);
13405
13462
  joinedTables.add(childTable);
13406
13463
  return childTable;
13407
13464
  };
@@ -13438,7 +13495,7 @@ function createQueryBuilder(state) {
13438
13495
  addToSelectClause(pivotColumnsStr);
13439
13496
  }
13440
13497
  }
13441
- text = computeSqlText(ensureBuilt());
13498
+ built = null;
13442
13499
  return this;
13443
13500
  },
13444
13501
  whereHas(relation, callback) {
@@ -14493,7 +14550,14 @@ function createQueryBuilder(state) {
14493
14550
  return this;
14494
14551
  },
14495
14552
  toSQL() {
14496
- return makeExecutableQuery(ensureBuilt(), reorderSelectClauses(text));
14553
+ const sqlText = reorderSelectClauses(text);
14554
+ return {
14555
+ sql: sqlText,
14556
+ toString: () => sqlText,
14557
+ execute: () => ensureBuilt().execute(),
14558
+ values: () => ensureBuilt().values(),
14559
+ raw: () => ensureBuilt().raw()
14560
+ };
14497
14561
  },
14498
14562
  async value(column) {
14499
14563
  const q = sql`${ensureBuilt()} LIMIT 1`;
@@ -14947,20 +15011,36 @@ function createQueryBuilder(state) {
14947
15011
  if (typeof prop === "string" && (prop.startsWith("where") || prop.startsWith("orWhere") || prop.startsWith("andWhere"))) {
14948
15012
  const isOr = prop.startsWith("orWhere");
14949
15013
  const isAnd = prop.startsWith("andWhere");
14950
- const raw = prop.replace(/^or?where/i, "").replace(/^andwhere/i, "");
14951
- if (!raw)
15014
+ const cacheKey = `${String(table)}|${prop}`;
15015
+ let chosen = dynamicWhereColumnCache.get(cacheKey);
15016
+ if (chosen === undefined) {
15017
+ const raw = prop.replace(/^(?:or|and)?where/i, "");
15018
+ if (!raw) {
15019
+ dynamicWhereColumnCache.set(cacheKey, "");
15020
+ chosen = "";
15021
+ } else {
15022
+ const lowerFirst = raw.charAt(0).toLowerCase() + raw.slice(1);
15023
+ const snake = raw.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
15024
+ const available = schema ? Object.keys(schema[String(table)]?.columns ?? {}) : [];
15025
+ chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
15026
+ dynamicWhereColumnCache.set(cacheKey, chosen);
15027
+ }
15028
+ }
15029
+ if (chosen === "")
14952
15030
  return () => receiver;
14953
- const lowerFirst = raw.charAt(0).toLowerCase() + raw.slice(1);
14954
- const toSnake = (s) => s.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
14955
- const snake = toSnake(raw);
14956
- const tbl = String(table);
14957
- const available = schema ? Object.keys(schema[tbl]?.columns ?? {}) : [];
14958
- const chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
15031
+ const column = chosen;
14959
15032
  return (value) => {
14960
- const expr = Array.isArray(value) ? sql`${sql(String(chosen))} IN ${sql(value)}` : sql`${sql(String(chosen))} = ${value}`;
15033
+ const expr = Array.isArray(value) ? sql`${sql(column)} IN ${sql(value)}` : sql`${sql(column)} = ${value}`;
14961
15034
  built = isOr ? sql`${ensureBuilt()} OR ${expr}` : isAnd ? sql`${ensureBuilt()} AND ${expr}` : sql`${ensureBuilt()} WHERE ${expr}`;
14962
- const clause = Array.isArray(value) ? `${String(chosen)} IN (?)` : `${String(chosen)} = ?`;
14963
- addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", clause);
15035
+ if (Array.isArray(value)) {
15036
+ const phs = getPlaceholders(value.length, whereParams.length + 1);
15037
+ addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} IN (${phs})`);
15038
+ whereParams.push(...value);
15039
+ } else {
15040
+ const ph = getPlaceholder(whereParams.length + 1);
15041
+ addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} = ${ph}`);
15042
+ whereParams.push(value);
15043
+ }
14964
15044
  return receiver;
14965
15045
  };
14966
15046
  }
@@ -16208,7 +16288,7 @@ function clearQueryCache() {
16208
16288
  function setQueryCacheMaxSize(size) {
16209
16289
  queryCache.setMaxSize(size);
16210
16290
  }
16211
- var SQL_PATTERNS, SAFE_WHERE_OPERATORS, queryCache;
16291
+ var SQL_PATTERNS, SAFE_WHERE_OPERATORS, warnedSqlFragmentContexts, queryCache, reorderCache, REORDER_CACHE_MAX = 500;
16212
16292
  var init_client = __esm(() => {
16213
16293
  init_config();
16214
16294
  init_db();
@@ -16243,7 +16323,9 @@ var init_client = __esm(() => {
16243
16323
  "between",
16244
16324
  "not between"
16245
16325
  ]);
16326
+ warnedSqlFragmentContexts = new Set;
16246
16327
  queryCache = new QueryCache;
16328
+ reorderCache = new Map;
16247
16329
  });
16248
16330
 
16249
16331
  // src/actions/cache.ts
@@ -23821,8 +23903,15 @@ function getModelFromRegistry(name) {
23821
23903
  return _getModel(name);
23822
23904
  }
23823
23905
  function toPostgresPlaceholders(sql2) {
23906
+ const hit = pgPlaceholderCache.get(sql2);
23907
+ if (hit !== undefined)
23908
+ return hit;
23824
23909
  let i2 = 0;
23825
- return sql2.replace(/\?/g, () => `$${++i2}`);
23910
+ const out = sql2.replace(/\?/g, () => `$${++i2}`);
23911
+ if (pgPlaceholderCache.size >= PG_PLACEHOLDER_CACHE_MAX)
23912
+ pgPlaceholderCache.clear();
23913
+ pgPlaceholderCache.set(sql2, out);
23914
+ return out;
23826
23915
  }
23827
23916
  function extractChanges(res) {
23828
23917
  if (res == null)
@@ -23954,21 +24043,25 @@ function timestampsEnabled(definition) {
23954
24043
  return Boolean(t2?.useTimestamps || t2?.timestampable);
23955
24044
  }
23956
24045
  function collectBelongsToManyKeys(definition) {
24046
+ const cached = btmKeysCache.get(definition);
24047
+ if (cached)
24048
+ return cached;
23957
24049
  const keys = new Set;
23958
24050
  const rel = definition.belongsToMany;
23959
- if (!rel)
23960
- return keys;
23961
- if (Array.isArray(rel)) {
23962
- for (const item of rel) {
23963
- if (typeof item === "string")
23964
- keys.add(item.toLowerCase());
23965
- else if (item && typeof item === "object" && item.model)
23966
- keys.add(item.model.toLowerCase());
23967
- }
23968
- } else if (typeof rel === "object") {
23969
- for (const k2 of Object.keys(rel))
23970
- keys.add(k2);
24051
+ if (rel) {
24052
+ if (Array.isArray(rel)) {
24053
+ for (const item of rel) {
24054
+ if (typeof item === "string")
24055
+ keys.add(item.toLowerCase());
24056
+ else if (item && typeof item === "object" && item.model)
24057
+ keys.add(item.model.toLowerCase());
24058
+ }
24059
+ } else if (typeof rel === "object") {
24060
+ for (const k2 of Object.keys(rel))
24061
+ keys.add(k2);
24062
+ }
23971
24063
  }
24064
+ btmKeysCache.set(definition, keys);
23972
24065
  return keys;
23973
24066
  }
23974
24067
 
@@ -25480,11 +25573,13 @@ async function seedModel(definition, count, faker) {
25480
25573
  await exec.run(`INSERT INTO ${definition.table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, Object.values(data));
25481
25574
  }
25482
25575
  }
25483
- var SAFE_SQL_IDENTIFIER, _getModel = null, globalDb = null, _executor = null, _executorForDb = null, _executorDialect = null, _executorDatabase = null, SOFT_DELETE_COLUMN = "deleted_at", BTM_RELATED_ALIAS = "__btm_rel__", snakeCaseCache, tableNameCache, relationCache, RELATION_CACHE_MAX = 1000;
25576
+ var SAFE_SQL_IDENTIFIER, _getModel = null, pgPlaceholderCache, PG_PLACEHOLDER_CACHE_MAX = 500, globalDb = null, _executor = null, _executorForDb = null, _executorDialect = null, _executorDatabase = null, SOFT_DELETE_COLUMN = "deleted_at", BTM_RELATED_ALIAS = "__btm_rel__", btmKeysCache, snakeCaseCache, tableNameCache, relationCache, RELATION_CACHE_MAX = 1000;
25484
25577
  var init_orm = __esm(() => {
25485
25578
  init_config();
25486
25579
  init_db();
25487
25580
  SAFE_SQL_IDENTIFIER = /^[A-Z_][A-Z0-9_]*$/i;
25581
+ pgPlaceholderCache = new Map;
25582
+ btmKeysCache = new WeakMap;
25488
25583
  snakeCaseCache = new Map;
25489
25584
  tableNameCache = new Map;
25490
25585
  relationCache = new Map;
@@ -29936,7 +30031,7 @@ function getPrefix() {
29936
30031
  }
29937
30032
  var prefix = getPrefix();
29938
30033
  // package.json
29939
- var version2 = "0.1.29";
30034
+ var version2 = "0.1.31";
29940
30035
 
29941
30036
  // bin/cli.ts
29942
30037
  init_actions();
package/dist/src/index.js CHANGED
@@ -12391,6 +12391,63 @@ function validateIdentifier(name, context) {
12391
12391
  throw new Error(`[query-builder] Invalid identifier${contextMsg}: '${name}'. Identifiers must start with a letter or underscore and contain only alphanumeric characters, underscores, and dots.`);
12392
12392
  }
12393
12393
  }
12394
+ function assertSafeWhereOperator(op, context) {
12395
+ if (typeof op !== "string")
12396
+ throw new TypeError(`[query-builder] ${context}: operator must be a string, got ${typeof op}`);
12397
+ const lower = op.toLowerCase();
12398
+ if (!SAFE_WHERE_OPERATORS.has(lower))
12399
+ throw new TypeError(`[query-builder] ${context}: refusing to use '${op}' as a SQL operator \u2014 not in the allowed set (${[...SAFE_WHERE_OPERATORS].join(", ")})`);
12400
+ return op;
12401
+ }
12402
+ function validateQualifiedIdentifier(value, context) {
12403
+ if (typeof value !== "string" || value.length === 0)
12404
+ throw new TypeError(`[query-builder] ${context}: identifier must be a non-empty string, got ${typeof value}`);
12405
+ if (value.length > 129)
12406
+ throw new TypeError(`[query-builder] ${context}: identifier '${value}' too long`);
12407
+ const parts = value.split(".");
12408
+ if (parts.length > 2)
12409
+ throw new TypeError(`[query-builder] ${context}: identifier '${value}' has more than one dot \u2014 only \`table.column\` is allowed`);
12410
+ for (const part of parts) {
12411
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(part))
12412
+ throw new TypeError(`[query-builder] ${context}: identifier segment '${part}' contains characters outside [A-Za-z0-9_]`);
12413
+ }
12414
+ }
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
+ }
12419
+ if (typeof fragment === "string") {
12420
+ warnOnceBareSqlFragment(context);
12421
+ }
12422
+ }
12423
+ function warnOnceBareSqlFragment(context) {
12424
+ if (warnedSqlFragmentContexts.has(context))
12425
+ return;
12426
+ warnedSqlFragmentContexts.add(context);
12427
+ console.warn(`[query-builder] ${context}: bare string passed to a *Raw method. ` + `Prefer \`sql\`...\`\` tagged-template fragments so values are parameterised ` + `instead of concatenated \u2014 concatenating request input into SQL is an ` + `injection vector. This will become a hard error in a future release.`);
12428
+ }
12429
+ function formatSubqueryValue(val) {
12430
+ if (val === null)
12431
+ return "NULL";
12432
+ if (typeof val === "number" && Number.isFinite(val))
12433
+ return String(val);
12434
+ if (typeof val === "boolean")
12435
+ return val ? "1" : "0";
12436
+ if (typeof val === "string")
12437
+ return `'${val.replace(/'/g, "''")}'`;
12438
+ if (val instanceof Date)
12439
+ return `'${val.toISOString()}'`;
12440
+ throw new TypeError(`[query-builder] subquery condition: refusing to interpolate value of type ${typeof val}`);
12441
+ }
12442
+ function buildOverClause(partitionBy, orderBy) {
12443
+ const cols = Array.isArray(partitionBy) ? partitionBy : partitionBy ? [partitionBy] : [];
12444
+ const parts = [];
12445
+ if (cols.length)
12446
+ parts.push(`PARTITION BY ${cols.join(", ")}`);
12447
+ if (orderBy && orderBy.length)
12448
+ parts.push(`ORDER BY ${orderBy.map(([c, d]) => `${c} ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`);
12449
+ return parts.length ? `OVER (${parts.join(" ")})` : "OVER ()";
12450
+ }
12394
12451
 
12395
12452
  class QueryCache {
12396
12453
  cache = new Map;
@@ -12455,6 +12512,16 @@ function sleep(ms) {
12455
12512
  return new Promise((resolve13) => setTimeout(resolve13, ms));
12456
12513
  }
12457
12514
  function reorderSelectClauses(sql) {
12515
+ const hit = reorderCache.get(sql);
12516
+ if (hit !== undefined)
12517
+ return hit;
12518
+ const out = computeReorderedClauses(sql);
12519
+ if (reorderCache.size >= REORDER_CACHE_MAX)
12520
+ reorderCache.clear();
12521
+ reorderCache.set(sql, out);
12522
+ return out;
12523
+ }
12524
+ function computeReorderedClauses(sql) {
12458
12525
  const KEYWORDS = [
12459
12526
  { key: "GROUP_BY", tokens: /^GROUP\s+BY\b/i },
12460
12527
  { key: "ORDER_BY", tokens: /^ORDER\s+BY\b/i },
@@ -12540,6 +12607,7 @@ function createQueryBuilder(state) {
12540
12607
  const _sql = state?.sql ?? getOrCreateBunSql();
12541
12608
  const meta = state?.meta;
12542
12609
  const schema = state?.schema;
12610
+ const dynamicWhereColumnCache = new Map;
12543
12611
  function applyCondition(expr) {
12544
12612
  if (Array.isArray(expr)) {
12545
12613
  const [col, op, val] = expr;
@@ -12858,65 +12926,9 @@ function createQueryBuilder(state) {
12858
12926
  }
12859
12927
  }
12860
12928
  };
12861
- const buildOverClause = (partitionBy, orderBy) => {
12862
- const cols = Array.isArray(partitionBy) ? partitionBy : partitionBy ? [partitionBy] : [];
12863
- const parts = [];
12864
- if (cols.length)
12865
- parts.push(`PARTITION BY ${cols.join(", ")}`);
12866
- if (orderBy && orderBy.length)
12867
- parts.push(`ORDER BY ${orderBy.map(([c, d]) => `${c} ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`);
12868
- return parts.length ? `OVER (${parts.join(" ")})` : "OVER ()";
12869
- };
12870
12929
  const addWindowFunction = (fnExpr, alias, partitionBy, orderBy) => {
12871
12930
  addToSelectClause(`${fnExpr} ${buildOverClause(partitionBy, orderBy)} AS ${alias}`);
12872
12931
  };
12873
- function assertSafeWhereOperator(op, context) {
12874
- if (typeof op !== "string")
12875
- throw new TypeError(`[query-builder] ${context}: operator must be a string, got ${typeof op}`);
12876
- const lower = op.toLowerCase();
12877
- if (!SAFE_WHERE_OPERATORS.has(lower))
12878
- throw new TypeError(`[query-builder] ${context}: refusing to use '${op}' as a SQL operator \u2014 not in the allowed set (${[...SAFE_WHERE_OPERATORS].join(", ")})`);
12879
- return op;
12880
- }
12881
- function validateQualifiedIdentifier(value, context) {
12882
- if (typeof value !== "string" || value.length === 0)
12883
- throw new TypeError(`[query-builder] ${context}: identifier must be a non-empty string, got ${typeof value}`);
12884
- if (value.length > 129)
12885
- throw new TypeError(`[query-builder] ${context}: identifier '${value}' too long`);
12886
- const parts = value.split(".");
12887
- if (parts.length > 2)
12888
- throw new TypeError(`[query-builder] ${context}: identifier '${value}' has more than one dot \u2014 only \`table.column\` is allowed`);
12889
- for (const part of parts) {
12890
- if (!/^[A-Z_][A-Z0-9_]*$/i.test(part))
12891
- throw new TypeError(`[query-builder] ${context}: identifier segment '${part}' contains characters outside [A-Za-z0-9_]`);
12892
- }
12893
- }
12894
- function assertSqlFragment(fragment, context) {
12895
- if (fragment === null || fragment === undefined) {
12896
- throw new TypeError(`[query-builder] ${context}: fragment must be a SqlFragment, got ${fragment}`);
12897
- }
12898
- if (typeof fragment === "string") {
12899
- warnOnceBareSqlFragment(context);
12900
- }
12901
- }
12902
- const warnedSqlFragmentContexts = new Set;
12903
- function warnOnceBareSqlFragment(context) {
12904
- if (warnedSqlFragmentContexts.has(context))
12905
- return;
12906
- warnedSqlFragmentContexts.add(context);
12907
- console.warn(`[query-builder] ${context}: bare string passed to a *Raw method. ` + `Prefer \`sql\`...\`\` tagged-template fragments so values are parameterised ` + `instead of concatenated \u2014 concatenating request input into SQL is an ` + `injection vector. This will become a hard error in a future release.`);
12908
- }
12909
- function formatSubqueryValue(val) {
12910
- if (val === null)
12911
- return "NULL";
12912
- if (typeof val === "number" && Number.isFinite(val))
12913
- return String(val);
12914
- if (typeof val === "boolean")
12915
- return val ? "1" : "0";
12916
- if (typeof val === "string")
12917
- return `'${val.replace(/'/g, "''")}'`;
12918
- throw new TypeError(`[query-builder] subquery condition: refusing to interpolate value of type ${typeof val}`);
12919
- }
12920
12932
  const buildHasSubquery = (parentTable, targetTable, pk, callback) => {
12921
12933
  validateIdentifier(parentTable, "relationship subquery (parent table)");
12922
12934
  validateIdentifier(targetTable, "relationship subquery (target table)");
@@ -12949,7 +12961,7 @@ function createQueryBuilder(state) {
12949
12961
  const subQb = {
12950
12962
  where: (col, op, val) => {
12951
12963
  validateIdentifier(col, "relationship subquery condition");
12952
- return `${targetTable}.${col} ${op} ${typeof val === "string" ? `'${val}'` : val}`;
12964
+ return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
12953
12965
  }
12954
12966
  };
12955
12967
  const condition = callback(subQb);
@@ -12978,7 +12990,7 @@ function createQueryBuilder(state) {
12978
12990
  const subQb = {
12979
12991
  where: (col, op, val) => {
12980
12992
  validateIdentifier(col, "relationship subquery condition");
12981
- return `${targetTable}.${col} ${op} ${typeof val === "string" ? `'${val}'` : val}`;
12993
+ return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
12982
12994
  }
12983
12995
  };
12984
12996
  const condition = callback(subQb);
@@ -13282,6 +13294,54 @@ function createQueryBuilder(state) {
13282
13294
  }
13283
13295
  return "";
13284
13296
  };
13297
+ const buildJoinConstraint = (targetTbl) => {
13298
+ if (!condition)
13299
+ return "";
13300
+ const frags = [];
13301
+ const addCmp = (col, op, val) => {
13302
+ validateIdentifier(String(col), "with() constraint column");
13303
+ const operator = assertSafeWhereOperator(op, "with() constraint operator");
13304
+ frags.push(`${targetTbl}.${String(col)} ${operator} ${formatSubqueryValue(val)}`);
13305
+ };
13306
+ const unsupported = (m) => () => {
13307
+ throw new Error(`[query-builder] with('${relationKey}', ...): ${m} is not supported inside a constraint callback on the JOIN-based builder \u2014 apply it to the outer query, or use the model layer's eager loading. (Silently ignoring it would return wrong data.)`);
13308
+ };
13309
+ const constraintQb = {
13310
+ where: (expr, op, val) => {
13311
+ if (Array.isArray(expr))
13312
+ addCmp(expr[0], expr[1], expr[2]);
13313
+ else if (expr && typeof expr === "object")
13314
+ for (const k of Object.keys(expr))
13315
+ addCmp(k, "=", expr[k]);
13316
+ else if (op !== undefined && val !== undefined)
13317
+ addCmp(expr, op, val);
13318
+ else if (op !== undefined)
13319
+ addCmp(expr, "=", op);
13320
+ return constraintQb;
13321
+ },
13322
+ whereIn: (col, vals) => {
13323
+ validateIdentifier(String(col), "with() constraint column");
13324
+ frags.push(`${targetTbl}.${String(col)} IN (${vals.map(formatSubqueryValue).join(", ")})`);
13325
+ return constraintQb;
13326
+ },
13327
+ whereNull: (col) => {
13328
+ validateIdentifier(String(col), "with() constraint column");
13329
+ frags.push(`${targetTbl}.${String(col)} IS NULL`);
13330
+ return constraintQb;
13331
+ },
13332
+ whereNotNull: (col) => {
13333
+ validateIdentifier(String(col), "with() constraint column");
13334
+ frags.push(`${targetTbl}.${String(col)} IS NOT NULL`);
13335
+ return constraintQb;
13336
+ },
13337
+ orderBy: unsupported("orderBy()"),
13338
+ limit: unsupported("limit()"),
13339
+ offset: unsupported("offset()"),
13340
+ take: unsupported("take()")
13341
+ };
13342
+ condition(constraintQb);
13343
+ return frags.length ? ` AND ${frags.join(" AND ")}` : "";
13344
+ };
13285
13345
  const resolveTarget = () => {
13286
13346
  const pick = (m) => {
13287
13347
  const modelName = m?.[relationKey];
@@ -13326,7 +13386,7 @@ function createQueryBuilder(state) {
13326
13386
  const throughPk = meta.primaryKeys[throughTable] ?? "id";
13327
13387
  const fkInThrough = `${singularize(fromTable)}_id`;
13328
13388
  const fkInFinal = `${singularize(throughTable)}_id`;
13329
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(throughTable)} ON ${sql(`${throughTable}.${fkInThrough}`)} = ${sql(`${fromTable}.${fromPk}`)} LEFT JOIN ${sql(finalTable)} ON ${sql(`${finalTable}.${fkInFinal}`)} = ${sql(`${throughTable}.${throughPk}`)}`;
13389
+ insertJoin(`LEFT JOIN ${throughTable} ON ${throughTable}.${fkInThrough} = ${fromTable}.${fromPk} LEFT JOIN ${finalTable} ON ${finalTable}.${fkInFinal} = ${throughTable}.${throughPk}`);
13330
13390
  joinedTables.add(throughTable);
13331
13391
  joinedTables.add(finalTable);
13332
13392
  return finalTable;
@@ -13339,7 +13399,7 @@ function createQueryBuilder(state) {
13339
13399
  const childPk = meta.primaryKeys[childTable] ?? "id";
13340
13400
  const fkA = resolved?.fkParent ?? `${singularize(fromTable)}_id`;
13341
13401
  const fkB = resolved?.fkRelated ?? `${singularize(childTable)}_id`;
13342
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(pivot)} ON ${sql(`${pivot}.${fkA}`)} = ${sql(`${fromTable}.${fromPk}`)} LEFT JOIN ${sql(childTable)} ON ${sql(`${childTable}.${childPk}`)} = ${sql(`${pivot}.${fkB}`)}`;
13402
+ insertJoin(`LEFT JOIN ${pivot} ON ${pivot}.${fkA} = ${fromTable}.${fromPk} LEFT JOIN ${childTable} ON ${childTable}.${childPk} = ${pivot}.${fkB}${buildJoinConstraint(childTable)}`);
13343
13403
  joinedTables.add(pivot);
13344
13404
  joinedTables.add(childTable);
13345
13405
  return childTable;
@@ -13353,7 +13413,8 @@ function createQueryBuilder(state) {
13353
13413
  const morphType = `${morphName}_type`;
13354
13414
  const morphId = `${morphName}_id`;
13355
13415
  const targetFk = `${singularize(childTable)}_id`;
13356
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(pivotTable)} ON ${sql(`${pivotTable}.${morphId}`)} = ${sql(`${fromTable}.${fromPk}`)} AND ${sql(`${pivotTable}.${morphType}`)} = ${sql(meta.tableToModel[fromTable] || fromTable)} LEFT JOIN ${sql(childTable)} ON ${sql(`${childTable}.${childPk}`)} = ${sql(`${pivotTable}.${targetFk}`)}`;
13416
+ const morphVal = formatSubqueryValue(meta.tableToModel[fromTable] || fromTable);
13417
+ insertJoin(`LEFT JOIN ${pivotTable} ON ${pivotTable}.${morphId} = ${fromTable}.${fromPk} AND ${pivotTable}.${morphType} = ${morphVal} LEFT JOIN ${childTable} ON ${childTable}.${childPk} = ${pivotTable}.${targetFk}`);
13357
13418
  joinedTables.add(pivotTable);
13358
13419
  joinedTables.add(childTable);
13359
13420
  return childTable;
@@ -13369,7 +13430,8 @@ function createQueryBuilder(state) {
13369
13430
  const morphType = `${morphName}_type`;
13370
13431
  const morphId = `${morphName}_id`;
13371
13432
  const relatedFk = `${singularize(relatedTable)}_id`;
13372
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(pivotTable)} ON ${sql(`${pivotTable}.${relatedFk}`)} = ${sql(`${fromTable}.${fromPk}`)} LEFT JOIN ${sql(relatedTable)} ON ${sql(`${relatedTable}.${relatedPk}`)} = ${sql(`${pivotTable}.${morphId}`)} AND ${sql(`${pivotTable}.${morphType}`)} = ${sql(meta.tableToModel[relatedTable] || relatedTable)}`;
13433
+ const morphVal = formatSubqueryValue(meta.tableToModel[relatedTable] || relatedTable);
13434
+ insertJoin(`LEFT JOIN ${pivotTable} ON ${pivotTable}.${relatedFk} = ${fromTable}.${fromPk} LEFT JOIN ${relatedTable} ON ${relatedTable}.${relatedPk} = ${pivotTable}.${morphId} AND ${pivotTable}.${morphType} = ${morphVal}`);
13373
13435
  joinedTables.add(pivotTable);
13374
13436
  joinedTables.add(relatedTable);
13375
13437
  return relatedTable;
@@ -13378,7 +13440,7 @@ function createQueryBuilder(state) {
13378
13440
  if (isBt) {
13379
13441
  const fkInParent = `${singularize(childTable)}_id`;
13380
13442
  const childPk = meta.primaryKeys[childTable] ?? "id";
13381
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(childTable)} ON ${sql(`${fromTable}.${fkInParent}`)} = ${sql(`${childTable}.${childPk}`)}`;
13443
+ insertJoin(`LEFT JOIN ${childTable} ON ${fromTable}.${fkInParent} = ${childTable}.${childPk}${buildJoinConstraint(childTable)}`);
13382
13444
  joinedTables.add(childTable);
13383
13445
  return childTable;
13384
13446
  }
@@ -13388,20 +13450,15 @@ function createQueryBuilder(state) {
13388
13450
  const morphType = `${relationKey}_type`;
13389
13451
  const morphId = `${relationKey}_id`;
13390
13452
  const fromPk = meta.primaryKeys[fromTable] ?? "id";
13391
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(childTable)} ON ${sql(`${childTable}.${morphId}`)} = ${sql(`${fromTable}.${fromPk}`)} AND ${sql(`${childTable}.${morphType}`)} = ${sql(meta.tableToModel[fromTable] || fromTable)}`;
13453
+ const morphVal = formatSubqueryValue(meta.tableToModel[fromTable] || fromTable);
13454
+ insertJoin(`LEFT JOIN ${childTable} ON ${childTable}.${morphId} = ${fromTable}.${fromPk} AND ${childTable}.${morphType} = ${morphVal}`);
13392
13455
  joinedTables.add(childTable);
13393
13456
  return childTable;
13394
13457
  }
13395
13458
  const fkInChild = `${singularize(fromTable)}_id`;
13396
13459
  const pk = meta.primaryKeys[fromTable] ?? "id";
13397
- const softDeleteCheck = addSoftDeleteCheck(childTable);
13398
- if (softDeleteCheck) {
13399
- const currentSql = String(ensureBuilt());
13400
- const joinCondition = `${childTable}.${fkInChild} = ${fromTable}.${pk}${softDeleteCheck}`;
13401
- built = sql`${sql(currentSql)} LEFT JOIN ${sql(childTable)} ON ${sql(joinCondition)}`;
13402
- } else {
13403
- built = sql`${ensureBuilt()} LEFT JOIN ${sql(childTable)} ON ${sql(`${childTable}.${fkInChild}`)} = ${sql(`${fromTable}.${pk}`)}`;
13404
- }
13460
+ const extraOn = `${addSoftDeleteCheck(childTable)}${buildJoinConstraint(childTable)}`;
13461
+ insertJoin(`LEFT JOIN ${childTable} ON ${childTable}.${fkInChild} = ${fromTable}.${pk}${extraOn}`);
13405
13462
  joinedTables.add(childTable);
13406
13463
  return childTable;
13407
13464
  };
@@ -13438,7 +13495,7 @@ function createQueryBuilder(state) {
13438
13495
  addToSelectClause(pivotColumnsStr);
13439
13496
  }
13440
13497
  }
13441
- text = computeSqlText(ensureBuilt());
13498
+ built = null;
13442
13499
  return this;
13443
13500
  },
13444
13501
  whereHas(relation, callback) {
@@ -14493,7 +14550,14 @@ function createQueryBuilder(state) {
14493
14550
  return this;
14494
14551
  },
14495
14552
  toSQL() {
14496
- return makeExecutableQuery(ensureBuilt(), reorderSelectClauses(text));
14553
+ const sqlText = reorderSelectClauses(text);
14554
+ return {
14555
+ sql: sqlText,
14556
+ toString: () => sqlText,
14557
+ execute: () => ensureBuilt().execute(),
14558
+ values: () => ensureBuilt().values(),
14559
+ raw: () => ensureBuilt().raw()
14560
+ };
14497
14561
  },
14498
14562
  async value(column) {
14499
14563
  const q = sql`${ensureBuilt()} LIMIT 1`;
@@ -14947,20 +15011,36 @@ function createQueryBuilder(state) {
14947
15011
  if (typeof prop === "string" && (prop.startsWith("where") || prop.startsWith("orWhere") || prop.startsWith("andWhere"))) {
14948
15012
  const isOr = prop.startsWith("orWhere");
14949
15013
  const isAnd = prop.startsWith("andWhere");
14950
- const raw = prop.replace(/^or?where/i, "").replace(/^andwhere/i, "");
14951
- if (!raw)
15014
+ const cacheKey = `${String(table)}|${prop}`;
15015
+ let chosen = dynamicWhereColumnCache.get(cacheKey);
15016
+ if (chosen === undefined) {
15017
+ const raw = prop.replace(/^(?:or|and)?where/i, "");
15018
+ if (!raw) {
15019
+ dynamicWhereColumnCache.set(cacheKey, "");
15020
+ chosen = "";
15021
+ } else {
15022
+ const lowerFirst = raw.charAt(0).toLowerCase() + raw.slice(1);
15023
+ const snake = raw.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
15024
+ const available = schema ? Object.keys(schema[String(table)]?.columns ?? {}) : [];
15025
+ chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
15026
+ dynamicWhereColumnCache.set(cacheKey, chosen);
15027
+ }
15028
+ }
15029
+ if (chosen === "")
14952
15030
  return () => receiver;
14953
- const lowerFirst = raw.charAt(0).toLowerCase() + raw.slice(1);
14954
- const toSnake = (s) => s.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
14955
- const snake = toSnake(raw);
14956
- const tbl = String(table);
14957
- const available = schema ? Object.keys(schema[tbl]?.columns ?? {}) : [];
14958
- const chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
15031
+ const column = chosen;
14959
15032
  return (value) => {
14960
- const expr = Array.isArray(value) ? sql`${sql(String(chosen))} IN ${sql(value)}` : sql`${sql(String(chosen))} = ${value}`;
15033
+ const expr = Array.isArray(value) ? sql`${sql(column)} IN ${sql(value)}` : sql`${sql(column)} = ${value}`;
14961
15034
  built = isOr ? sql`${ensureBuilt()} OR ${expr}` : isAnd ? sql`${ensureBuilt()} AND ${expr}` : sql`${ensureBuilt()} WHERE ${expr}`;
14962
- const clause = Array.isArray(value) ? `${String(chosen)} IN (?)` : `${String(chosen)} = ?`;
14963
- addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", clause);
15035
+ if (Array.isArray(value)) {
15036
+ const phs = getPlaceholders(value.length, whereParams.length + 1);
15037
+ addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} IN (${phs})`);
15038
+ whereParams.push(...value);
15039
+ } else {
15040
+ const ph = getPlaceholder(whereParams.length + 1);
15041
+ addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} = ${ph}`);
15042
+ whereParams.push(value);
15043
+ }
14964
15044
  return receiver;
14965
15045
  };
14966
15046
  }
@@ -16208,7 +16288,7 @@ function clearQueryCache() {
16208
16288
  function setQueryCacheMaxSize(size) {
16209
16289
  queryCache.setMaxSize(size);
16210
16290
  }
16211
- var SQL_PATTERNS, SAFE_WHERE_OPERATORS, queryCache;
16291
+ var SQL_PATTERNS, SAFE_WHERE_OPERATORS, warnedSqlFragmentContexts, queryCache, reorderCache, REORDER_CACHE_MAX = 500;
16212
16292
  var init_client = __esm(() => {
16213
16293
  init_config();
16214
16294
  init_db();
@@ -16243,7 +16323,9 @@ var init_client = __esm(() => {
16243
16323
  "between",
16244
16324
  "not between"
16245
16325
  ]);
16326
+ warnedSqlFragmentContexts = new Set;
16246
16327
  queryCache = new QueryCache;
16328
+ reorderCache = new Map;
16247
16329
  });
16248
16330
 
16249
16331
  // src/actions/cache.ts
@@ -23821,8 +23903,15 @@ function getModelFromRegistry(name) {
23821
23903
  return _getModel(name);
23822
23904
  }
23823
23905
  function toPostgresPlaceholders(sql2) {
23906
+ const hit = pgPlaceholderCache.get(sql2);
23907
+ if (hit !== undefined)
23908
+ return hit;
23824
23909
  let i2 = 0;
23825
- return sql2.replace(/\?/g, () => `$${++i2}`);
23910
+ const out = sql2.replace(/\?/g, () => `$${++i2}`);
23911
+ if (pgPlaceholderCache.size >= PG_PLACEHOLDER_CACHE_MAX)
23912
+ pgPlaceholderCache.clear();
23913
+ pgPlaceholderCache.set(sql2, out);
23914
+ return out;
23826
23915
  }
23827
23916
  function extractChanges(res) {
23828
23917
  if (res == null)
@@ -23954,21 +24043,25 @@ function timestampsEnabled(definition) {
23954
24043
  return Boolean(t2?.useTimestamps || t2?.timestampable);
23955
24044
  }
23956
24045
  function collectBelongsToManyKeys(definition) {
24046
+ const cached = btmKeysCache.get(definition);
24047
+ if (cached)
24048
+ return cached;
23957
24049
  const keys = new Set;
23958
24050
  const rel = definition.belongsToMany;
23959
- if (!rel)
23960
- return keys;
23961
- if (Array.isArray(rel)) {
23962
- for (const item of rel) {
23963
- if (typeof item === "string")
23964
- keys.add(item.toLowerCase());
23965
- else if (item && typeof item === "object" && item.model)
23966
- keys.add(item.model.toLowerCase());
23967
- }
23968
- } else if (typeof rel === "object") {
23969
- for (const k2 of Object.keys(rel))
23970
- keys.add(k2);
24051
+ if (rel) {
24052
+ if (Array.isArray(rel)) {
24053
+ for (const item of rel) {
24054
+ if (typeof item === "string")
24055
+ keys.add(item.toLowerCase());
24056
+ else if (item && typeof item === "object" && item.model)
24057
+ keys.add(item.model.toLowerCase());
24058
+ }
24059
+ } else if (typeof rel === "object") {
24060
+ for (const k2 of Object.keys(rel))
24061
+ keys.add(k2);
24062
+ }
23971
24063
  }
24064
+ btmKeysCache.set(definition, keys);
23972
24065
  return keys;
23973
24066
  }
23974
24067
 
@@ -25480,11 +25573,13 @@ async function seedModel(definition, count, faker) {
25480
25573
  await exec.run(`INSERT INTO ${definition.table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, Object.values(data));
25481
25574
  }
25482
25575
  }
25483
- var SAFE_SQL_IDENTIFIER, _getModel = null, globalDb = null, _executor = null, _executorForDb = null, _executorDialect = null, _executorDatabase = null, SOFT_DELETE_COLUMN = "deleted_at", BTM_RELATED_ALIAS = "__btm_rel__", snakeCaseCache, tableNameCache, relationCache, RELATION_CACHE_MAX = 1000;
25576
+ var SAFE_SQL_IDENTIFIER, _getModel = null, pgPlaceholderCache, PG_PLACEHOLDER_CACHE_MAX = 500, globalDb = null, _executor = null, _executorForDb = null, _executorDialect = null, _executorDatabase = null, SOFT_DELETE_COLUMN = "deleted_at", BTM_RELATED_ALIAS = "__btm_rel__", btmKeysCache, snakeCaseCache, tableNameCache, relationCache, RELATION_CACHE_MAX = 1000;
25484
25577
  var init_orm = __esm(() => {
25485
25578
  init_config();
25486
25579
  init_db();
25487
25580
  SAFE_SQL_IDENTIFIER = /^[A-Z_][A-Z0-9_]*$/i;
25581
+ pgPlaceholderCache = new Map;
25582
+ btmKeysCache = new WeakMap;
25488
25583
  snakeCaseCache = new Map;
25489
25584
  tableNameCache = new Map;
25490
25585
  relationCache = new Map;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-query-builder",
3
3
  "type": "module",
4
- "version": "0.1.29",
4
+ "version": "0.1.31",
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",