bun-query-builder 0.1.29 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.js CHANGED
@@ -12391,6 +12391,61 @@ 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
+ throw new TypeError(`[query-builder] subquery condition: refusing to interpolate value of type ${typeof val}`);
12439
+ }
12440
+ function buildOverClause(partitionBy, orderBy) {
12441
+ const cols = Array.isArray(partitionBy) ? partitionBy : partitionBy ? [partitionBy] : [];
12442
+ const parts = [];
12443
+ if (cols.length)
12444
+ parts.push(`PARTITION BY ${cols.join(", ")}`);
12445
+ if (orderBy && orderBy.length)
12446
+ parts.push(`ORDER BY ${orderBy.map(([c, d]) => `${c} ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`);
12447
+ return parts.length ? `OVER (${parts.join(" ")})` : "OVER ()";
12448
+ }
12394
12449
 
12395
12450
  class QueryCache {
12396
12451
  cache = new Map;
@@ -12455,6 +12510,16 @@ function sleep(ms) {
12455
12510
  return new Promise((resolve13) => setTimeout(resolve13, ms));
12456
12511
  }
12457
12512
  function reorderSelectClauses(sql) {
12513
+ const hit = reorderCache.get(sql);
12514
+ if (hit !== undefined)
12515
+ return hit;
12516
+ const out = computeReorderedClauses(sql);
12517
+ if (reorderCache.size >= REORDER_CACHE_MAX)
12518
+ reorderCache.clear();
12519
+ reorderCache.set(sql, out);
12520
+ return out;
12521
+ }
12522
+ function computeReorderedClauses(sql) {
12458
12523
  const KEYWORDS = [
12459
12524
  { key: "GROUP_BY", tokens: /^GROUP\s+BY\b/i },
12460
12525
  { key: "ORDER_BY", tokens: /^ORDER\s+BY\b/i },
@@ -12540,6 +12605,7 @@ function createQueryBuilder(state) {
12540
12605
  const _sql = state?.sql ?? getOrCreateBunSql();
12541
12606
  const meta = state?.meta;
12542
12607
  const schema = state?.schema;
12608
+ const dynamicWhereColumnCache = new Map;
12543
12609
  function applyCondition(expr) {
12544
12610
  if (Array.isArray(expr)) {
12545
12611
  const [col, op, val] = expr;
@@ -12858,65 +12924,9 @@ function createQueryBuilder(state) {
12858
12924
  }
12859
12925
  }
12860
12926
  };
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
12927
  const addWindowFunction = (fnExpr, alias, partitionBy, orderBy) => {
12871
12928
  addToSelectClause(`${fnExpr} ${buildOverClause(partitionBy, orderBy)} AS ${alias}`);
12872
12929
  };
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
12930
  const buildHasSubquery = (parentTable, targetTable, pk, callback) => {
12921
12931
  validateIdentifier(parentTable, "relationship subquery (parent table)");
12922
12932
  validateIdentifier(targetTable, "relationship subquery (target table)");
@@ -12949,7 +12959,7 @@ function createQueryBuilder(state) {
12949
12959
  const subQb = {
12950
12960
  where: (col, op, val) => {
12951
12961
  validateIdentifier(col, "relationship subquery condition");
12952
- return `${targetTable}.${col} ${op} ${typeof val === "string" ? `'${val}'` : val}`;
12962
+ return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
12953
12963
  }
12954
12964
  };
12955
12965
  const condition = callback(subQb);
@@ -12978,7 +12988,7 @@ function createQueryBuilder(state) {
12978
12988
  const subQb = {
12979
12989
  where: (col, op, val) => {
12980
12990
  validateIdentifier(col, "relationship subquery condition");
12981
- return `${targetTable}.${col} ${op} ${typeof val === "string" ? `'${val}'` : val}`;
12991
+ return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
12982
12992
  }
12983
12993
  };
12984
12994
  const condition = callback(subQb);
@@ -14493,7 +14503,14 @@ function createQueryBuilder(state) {
14493
14503
  return this;
14494
14504
  },
14495
14505
  toSQL() {
14496
- return makeExecutableQuery(ensureBuilt(), reorderSelectClauses(text));
14506
+ const sqlText = reorderSelectClauses(text);
14507
+ return {
14508
+ sql: sqlText,
14509
+ toString: () => sqlText,
14510
+ execute: () => ensureBuilt().execute(),
14511
+ values: () => ensureBuilt().values(),
14512
+ raw: () => ensureBuilt().raw()
14513
+ };
14497
14514
  },
14498
14515
  async value(column) {
14499
14516
  const q = sql`${ensureBuilt()} LIMIT 1`;
@@ -14947,20 +14964,36 @@ function createQueryBuilder(state) {
14947
14964
  if (typeof prop === "string" && (prop.startsWith("where") || prop.startsWith("orWhere") || prop.startsWith("andWhere"))) {
14948
14965
  const isOr = prop.startsWith("orWhere");
14949
14966
  const isAnd = prop.startsWith("andWhere");
14950
- const raw = prop.replace(/^or?where/i, "").replace(/^andwhere/i, "");
14951
- if (!raw)
14967
+ const cacheKey = `${String(table)}|${prop}`;
14968
+ let chosen = dynamicWhereColumnCache.get(cacheKey);
14969
+ if (chosen === undefined) {
14970
+ const raw = prop.replace(/^(?:or|and)?where/i, "");
14971
+ if (!raw) {
14972
+ dynamicWhereColumnCache.set(cacheKey, "");
14973
+ chosen = "";
14974
+ } else {
14975
+ const lowerFirst = raw.charAt(0).toLowerCase() + raw.slice(1);
14976
+ const snake = raw.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
14977
+ const available = schema ? Object.keys(schema[String(table)]?.columns ?? {}) : [];
14978
+ chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
14979
+ dynamicWhereColumnCache.set(cacheKey, chosen);
14980
+ }
14981
+ }
14982
+ if (chosen === "")
14952
14983
  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;
14984
+ const column = chosen;
14959
14985
  return (value) => {
14960
- const expr = Array.isArray(value) ? sql`${sql(String(chosen))} IN ${sql(value)}` : sql`${sql(String(chosen))} = ${value}`;
14986
+ const expr = Array.isArray(value) ? sql`${sql(column)} IN ${sql(value)}` : sql`${sql(column)} = ${value}`;
14961
14987
  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);
14988
+ if (Array.isArray(value)) {
14989
+ const phs = getPlaceholders(value.length, whereParams.length + 1);
14990
+ addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} IN (${phs})`);
14991
+ whereParams.push(...value);
14992
+ } else {
14993
+ const ph = getPlaceholder(whereParams.length + 1);
14994
+ addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} = ${ph}`);
14995
+ whereParams.push(value);
14996
+ }
14964
14997
  return receiver;
14965
14998
  };
14966
14999
  }
@@ -16208,7 +16241,7 @@ function clearQueryCache() {
16208
16241
  function setQueryCacheMaxSize(size) {
16209
16242
  queryCache.setMaxSize(size);
16210
16243
  }
16211
- var SQL_PATTERNS, SAFE_WHERE_OPERATORS, queryCache;
16244
+ var SQL_PATTERNS, SAFE_WHERE_OPERATORS, warnedSqlFragmentContexts, queryCache, reorderCache, REORDER_CACHE_MAX = 500;
16212
16245
  var init_client = __esm(() => {
16213
16246
  init_config();
16214
16247
  init_db();
@@ -16243,7 +16276,9 @@ var init_client = __esm(() => {
16243
16276
  "between",
16244
16277
  "not between"
16245
16278
  ]);
16279
+ warnedSqlFragmentContexts = new Set;
16246
16280
  queryCache = new QueryCache;
16281
+ reorderCache = new Map;
16247
16282
  });
16248
16283
 
16249
16284
  // src/actions/cache.ts
@@ -23821,8 +23856,15 @@ function getModelFromRegistry(name) {
23821
23856
  return _getModel(name);
23822
23857
  }
23823
23858
  function toPostgresPlaceholders(sql2) {
23859
+ const hit = pgPlaceholderCache.get(sql2);
23860
+ if (hit !== undefined)
23861
+ return hit;
23824
23862
  let i2 = 0;
23825
- return sql2.replace(/\?/g, () => `$${++i2}`);
23863
+ const out = sql2.replace(/\?/g, () => `$${++i2}`);
23864
+ if (pgPlaceholderCache.size >= PG_PLACEHOLDER_CACHE_MAX)
23865
+ pgPlaceholderCache.clear();
23866
+ pgPlaceholderCache.set(sql2, out);
23867
+ return out;
23826
23868
  }
23827
23869
  function extractChanges(res) {
23828
23870
  if (res == null)
@@ -23954,21 +23996,25 @@ function timestampsEnabled(definition) {
23954
23996
  return Boolean(t2?.useTimestamps || t2?.timestampable);
23955
23997
  }
23956
23998
  function collectBelongsToManyKeys(definition) {
23999
+ const cached = btmKeysCache.get(definition);
24000
+ if (cached)
24001
+ return cached;
23957
24002
  const keys = new Set;
23958
24003
  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);
24004
+ if (rel) {
24005
+ if (Array.isArray(rel)) {
24006
+ for (const item of rel) {
24007
+ if (typeof item === "string")
24008
+ keys.add(item.toLowerCase());
24009
+ else if (item && typeof item === "object" && item.model)
24010
+ keys.add(item.model.toLowerCase());
24011
+ }
24012
+ } else if (typeof rel === "object") {
24013
+ for (const k2 of Object.keys(rel))
24014
+ keys.add(k2);
24015
+ }
23971
24016
  }
24017
+ btmKeysCache.set(definition, keys);
23972
24018
  return keys;
23973
24019
  }
23974
24020
 
@@ -25480,11 +25526,13 @@ async function seedModel(definition, count, faker) {
25480
25526
  await exec.run(`INSERT INTO ${definition.table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, Object.values(data));
25481
25527
  }
25482
25528
  }
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;
25529
+ 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
25530
  var init_orm = __esm(() => {
25485
25531
  init_config();
25486
25532
  init_db();
25487
25533
  SAFE_SQL_IDENTIFIER = /^[A-Z_][A-Z0-9_]*$/i;
25534
+ pgPlaceholderCache = new Map;
25535
+ btmKeysCache = new WeakMap;
25488
25536
  snakeCaseCache = new Map;
25489
25537
  tableNameCache = new Map;
25490
25538
  relationCache = new Map;
@@ -29936,7 +29984,7 @@ function getPrefix() {
29936
29984
  }
29937
29985
  var prefix = getPrefix();
29938
29986
  // package.json
29939
- var version2 = "0.1.29";
29987
+ var version2 = "0.1.30";
29940
29988
 
29941
29989
  // bin/cli.ts
29942
29990
  init_actions();
package/dist/src/index.js CHANGED
@@ -12391,6 +12391,61 @@ 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
+ throw new TypeError(`[query-builder] subquery condition: refusing to interpolate value of type ${typeof val}`);
12439
+ }
12440
+ function buildOverClause(partitionBy, orderBy) {
12441
+ const cols = Array.isArray(partitionBy) ? partitionBy : partitionBy ? [partitionBy] : [];
12442
+ const parts = [];
12443
+ if (cols.length)
12444
+ parts.push(`PARTITION BY ${cols.join(", ")}`);
12445
+ if (orderBy && orderBy.length)
12446
+ parts.push(`ORDER BY ${orderBy.map(([c, d]) => `${c} ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`);
12447
+ return parts.length ? `OVER (${parts.join(" ")})` : "OVER ()";
12448
+ }
12394
12449
 
12395
12450
  class QueryCache {
12396
12451
  cache = new Map;
@@ -12455,6 +12510,16 @@ function sleep(ms) {
12455
12510
  return new Promise((resolve13) => setTimeout(resolve13, ms));
12456
12511
  }
12457
12512
  function reorderSelectClauses(sql) {
12513
+ const hit = reorderCache.get(sql);
12514
+ if (hit !== undefined)
12515
+ return hit;
12516
+ const out = computeReorderedClauses(sql);
12517
+ if (reorderCache.size >= REORDER_CACHE_MAX)
12518
+ reorderCache.clear();
12519
+ reorderCache.set(sql, out);
12520
+ return out;
12521
+ }
12522
+ function computeReorderedClauses(sql) {
12458
12523
  const KEYWORDS = [
12459
12524
  { key: "GROUP_BY", tokens: /^GROUP\s+BY\b/i },
12460
12525
  { key: "ORDER_BY", tokens: /^ORDER\s+BY\b/i },
@@ -12540,6 +12605,7 @@ function createQueryBuilder(state) {
12540
12605
  const _sql = state?.sql ?? getOrCreateBunSql();
12541
12606
  const meta = state?.meta;
12542
12607
  const schema = state?.schema;
12608
+ const dynamicWhereColumnCache = new Map;
12543
12609
  function applyCondition(expr) {
12544
12610
  if (Array.isArray(expr)) {
12545
12611
  const [col, op, val] = expr;
@@ -12858,65 +12924,9 @@ function createQueryBuilder(state) {
12858
12924
  }
12859
12925
  }
12860
12926
  };
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
12927
  const addWindowFunction = (fnExpr, alias, partitionBy, orderBy) => {
12871
12928
  addToSelectClause(`${fnExpr} ${buildOverClause(partitionBy, orderBy)} AS ${alias}`);
12872
12929
  };
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
12930
  const buildHasSubquery = (parentTable, targetTable, pk, callback) => {
12921
12931
  validateIdentifier(parentTable, "relationship subquery (parent table)");
12922
12932
  validateIdentifier(targetTable, "relationship subquery (target table)");
@@ -12949,7 +12959,7 @@ function createQueryBuilder(state) {
12949
12959
  const subQb = {
12950
12960
  where: (col, op, val) => {
12951
12961
  validateIdentifier(col, "relationship subquery condition");
12952
- return `${targetTable}.${col} ${op} ${typeof val === "string" ? `'${val}'` : val}`;
12962
+ return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
12953
12963
  }
12954
12964
  };
12955
12965
  const condition = callback(subQb);
@@ -12978,7 +12988,7 @@ function createQueryBuilder(state) {
12978
12988
  const subQb = {
12979
12989
  where: (col, op, val) => {
12980
12990
  validateIdentifier(col, "relationship subquery condition");
12981
- return `${targetTable}.${col} ${op} ${typeof val === "string" ? `'${val}'` : val}`;
12991
+ return `${targetTable}.${col} ${assertSafeWhereOperator(op, "whereHas callback")} ${formatSubqueryValue(val)}`;
12982
12992
  }
12983
12993
  };
12984
12994
  const condition = callback(subQb);
@@ -14493,7 +14503,14 @@ function createQueryBuilder(state) {
14493
14503
  return this;
14494
14504
  },
14495
14505
  toSQL() {
14496
- return makeExecutableQuery(ensureBuilt(), reorderSelectClauses(text));
14506
+ const sqlText = reorderSelectClauses(text);
14507
+ return {
14508
+ sql: sqlText,
14509
+ toString: () => sqlText,
14510
+ execute: () => ensureBuilt().execute(),
14511
+ values: () => ensureBuilt().values(),
14512
+ raw: () => ensureBuilt().raw()
14513
+ };
14497
14514
  },
14498
14515
  async value(column) {
14499
14516
  const q = sql`${ensureBuilt()} LIMIT 1`;
@@ -14947,20 +14964,36 @@ function createQueryBuilder(state) {
14947
14964
  if (typeof prop === "string" && (prop.startsWith("where") || prop.startsWith("orWhere") || prop.startsWith("andWhere"))) {
14948
14965
  const isOr = prop.startsWith("orWhere");
14949
14966
  const isAnd = prop.startsWith("andWhere");
14950
- const raw = prop.replace(/^or?where/i, "").replace(/^andwhere/i, "");
14951
- if (!raw)
14967
+ const cacheKey = `${String(table)}|${prop}`;
14968
+ let chosen = dynamicWhereColumnCache.get(cacheKey);
14969
+ if (chosen === undefined) {
14970
+ const raw = prop.replace(/^(?:or|and)?where/i, "");
14971
+ if (!raw) {
14972
+ dynamicWhereColumnCache.set(cacheKey, "");
14973
+ chosen = "";
14974
+ } else {
14975
+ const lowerFirst = raw.charAt(0).toLowerCase() + raw.slice(1);
14976
+ const snake = raw.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
14977
+ const available = schema ? Object.keys(schema[String(table)]?.columns ?? {}) : [];
14978
+ chosen = [snake, lowerFirst, lowerFirst.toLowerCase()].find((n) => available.includes(n)) ?? snake;
14979
+ dynamicWhereColumnCache.set(cacheKey, chosen);
14980
+ }
14981
+ }
14982
+ if (chosen === "")
14952
14983
  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;
14984
+ const column = chosen;
14959
14985
  return (value) => {
14960
- const expr = Array.isArray(value) ? sql`${sql(String(chosen))} IN ${sql(value)}` : sql`${sql(String(chosen))} = ${value}`;
14986
+ const expr = Array.isArray(value) ? sql`${sql(column)} IN ${sql(value)}` : sql`${sql(column)} = ${value}`;
14961
14987
  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);
14988
+ if (Array.isArray(value)) {
14989
+ const phs = getPlaceholders(value.length, whereParams.length + 1);
14990
+ addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} IN (${phs})`);
14991
+ whereParams.push(...value);
14992
+ } else {
14993
+ const ph = getPlaceholder(whereParams.length + 1);
14994
+ addWhereText(isOr ? "OR" : isAnd ? "AND" : "WHERE", `${column} = ${ph}`);
14995
+ whereParams.push(value);
14996
+ }
14964
14997
  return receiver;
14965
14998
  };
14966
14999
  }
@@ -16208,7 +16241,7 @@ function clearQueryCache() {
16208
16241
  function setQueryCacheMaxSize(size) {
16209
16242
  queryCache.setMaxSize(size);
16210
16243
  }
16211
- var SQL_PATTERNS, SAFE_WHERE_OPERATORS, queryCache;
16244
+ var SQL_PATTERNS, SAFE_WHERE_OPERATORS, warnedSqlFragmentContexts, queryCache, reorderCache, REORDER_CACHE_MAX = 500;
16212
16245
  var init_client = __esm(() => {
16213
16246
  init_config();
16214
16247
  init_db();
@@ -16243,7 +16276,9 @@ var init_client = __esm(() => {
16243
16276
  "between",
16244
16277
  "not between"
16245
16278
  ]);
16279
+ warnedSqlFragmentContexts = new Set;
16246
16280
  queryCache = new QueryCache;
16281
+ reorderCache = new Map;
16247
16282
  });
16248
16283
 
16249
16284
  // src/actions/cache.ts
@@ -23821,8 +23856,15 @@ function getModelFromRegistry(name) {
23821
23856
  return _getModel(name);
23822
23857
  }
23823
23858
  function toPostgresPlaceholders(sql2) {
23859
+ const hit = pgPlaceholderCache.get(sql2);
23860
+ if (hit !== undefined)
23861
+ return hit;
23824
23862
  let i2 = 0;
23825
- return sql2.replace(/\?/g, () => `$${++i2}`);
23863
+ const out = sql2.replace(/\?/g, () => `$${++i2}`);
23864
+ if (pgPlaceholderCache.size >= PG_PLACEHOLDER_CACHE_MAX)
23865
+ pgPlaceholderCache.clear();
23866
+ pgPlaceholderCache.set(sql2, out);
23867
+ return out;
23826
23868
  }
23827
23869
  function extractChanges(res) {
23828
23870
  if (res == null)
@@ -23954,21 +23996,25 @@ function timestampsEnabled(definition) {
23954
23996
  return Boolean(t2?.useTimestamps || t2?.timestampable);
23955
23997
  }
23956
23998
  function collectBelongsToManyKeys(definition) {
23999
+ const cached = btmKeysCache.get(definition);
24000
+ if (cached)
24001
+ return cached;
23957
24002
  const keys = new Set;
23958
24003
  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);
24004
+ if (rel) {
24005
+ if (Array.isArray(rel)) {
24006
+ for (const item of rel) {
24007
+ if (typeof item === "string")
24008
+ keys.add(item.toLowerCase());
24009
+ else if (item && typeof item === "object" && item.model)
24010
+ keys.add(item.model.toLowerCase());
24011
+ }
24012
+ } else if (typeof rel === "object") {
24013
+ for (const k2 of Object.keys(rel))
24014
+ keys.add(k2);
24015
+ }
23971
24016
  }
24017
+ btmKeysCache.set(definition, keys);
23972
24018
  return keys;
23973
24019
  }
23974
24020
 
@@ -25480,11 +25526,13 @@ async function seedModel(definition, count, faker) {
25480
25526
  await exec.run(`INSERT INTO ${definition.table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, Object.values(data));
25481
25527
  }
25482
25528
  }
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;
25529
+ 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
25530
  var init_orm = __esm(() => {
25485
25531
  init_config();
25486
25532
  init_db();
25487
25533
  SAFE_SQL_IDENTIFIER = /^[A-Z_][A-Z0-9_]*$/i;
25534
+ pgPlaceholderCache = new Map;
25535
+ btmKeysCache = new WeakMap;
25488
25536
  snakeCaseCache = new Map;
25489
25537
  tableNameCache = new Map;
25490
25538
  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.30",
5
5
  "description": "A simple yet performant query builder for TypeScript. Built with Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",