bun-query-builder 0.1.28 → 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
 
@@ -25203,7 +25249,14 @@ class ModelQueryBuilder {
25203
25249
  sql2 += ` WHERE ${whereBody}`;
25204
25250
  }
25205
25251
  const row = await exec.get(sql2, params);
25206
- return row?.v == null ? null : Number(row.v);
25252
+ const v2 = row?.v;
25253
+ if (v2 == null)
25254
+ return null;
25255
+ if (typeof v2 === "string") {
25256
+ const n2 = Number(v2);
25257
+ return v2.trim() !== "" && !Number.isNaN(n2) ? n2 : v2;
25258
+ }
25259
+ return v2;
25207
25260
  }
25208
25261
  max(column) {
25209
25262
  return this.aggregate("MAX", column);
@@ -25212,10 +25265,10 @@ class ModelQueryBuilder {
25212
25265
  return this.aggregate("MIN", column);
25213
25266
  }
25214
25267
  async avg(column) {
25215
- return await this.aggregate("AVG", column) || 0;
25268
+ return Number(await this.aggregate("AVG", column) ?? 0) || 0;
25216
25269
  }
25217
25270
  async sum(column) {
25218
- return await this.aggregate("SUM", column) || 0;
25271
+ return Number(await this.aggregate("SUM", column) ?? 0) || 0;
25219
25272
  }
25220
25273
  async delete() {
25221
25274
  const exec = getExecutor();
@@ -25473,11 +25526,13 @@ async function seedModel(definition, count, faker) {
25473
25526
  await exec.run(`INSERT INTO ${definition.table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, Object.values(data));
25474
25527
  }
25475
25528
  }
25476
- 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;
25477
25530
  var init_orm = __esm(() => {
25478
25531
  init_config();
25479
25532
  init_db();
25480
25533
  SAFE_SQL_IDENTIFIER = /^[A-Z_][A-Z0-9_]*$/i;
25534
+ pgPlaceholderCache = new Map;
25535
+ btmKeysCache = new WeakMap;
25481
25536
  snakeCaseCache = new Map;
25482
25537
  tableNameCache = new Map;
25483
25538
  relationCache = new Map;
@@ -29929,7 +29984,7 @@ function getPrefix() {
29929
29984
  }
29930
29985
  var prefix = getPrefix();
29931
29986
  // package.json
29932
- var version2 = "0.1.28";
29987
+ var version2 = "0.1.30";
29933
29988
 
29934
29989
  // bin/cli.ts
29935
29990
  init_actions();
package/dist/client.d.ts CHANGED
@@ -104,8 +104,11 @@ export declare interface BaseSelectQueryBuilder<DB extends DatabaseSchema<any>,
104
104
  having: (expr: WhereExpression<any>) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
105
105
  havingRaw: (fragment: SqlFragment) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
106
106
  addSelect: (...columns: ((keyof DB[TTable]['columns'] & string) | string | SqlFragment)[]) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
107
- select?: (columns: string | SqlFragment | ((keyof DB[TTable]['columns'] & string) | string | SqlFragment)[]) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
108
- selectAll?: () => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
107
+ select?: {
108
+ <K extends keyof DB[TTable]['columns'] & string>(columns: K[]): SelectQueryBuilder<DB, TTable, Pick<DB[TTable]['columns'], K>, TJoined>
109
+ (columns: string | SqlFragment | (string | SqlFragment)[]): SelectQueryBuilder<DB, TTable, TSelected, TJoined>
110
+ }
111
+ selectAll?: () => SelectQueryBuilder<DB, TTable, DB[TTable]['columns'], TJoined>
109
112
  orderByRaw: (fragment: SqlFragment) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
110
113
  union: (other: { toSQL: () => any }) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
111
114
  unionAll: (other: { toSQL: () => any }) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
@@ -146,7 +149,7 @@ export declare interface BaseSelectQueryBuilder<DB extends DatabaseSchema<any>,
146
149
  }
147
150
  exists: () => Promise<boolean>
148
151
  doesntExist: () => Promise<boolean>
149
- cursorPaginate: (perPage: number, cursor?: string | number, column?: string, direction?: 'asc' | 'desc') => Promise<{ data: any[], meta: { perPage: number, nextCursor: string | number | null } }>
152
+ cursorPaginate: (perPage: number, cursor?: string | number, column?: string, direction?: 'asc' | 'desc') => Promise<{ data: SelectedRow<DB, TTable, TSelected>[], meta: { perPage: number, nextCursor: string | number | null } }>
150
153
  chunk: (size: number, handler: (rows: any[]) => Promise<void> | void) => Promise<void>
151
154
  chunkById: (size: number, column?: string, handler?: (rows: any[]) => Promise<void> | void) => Promise<void>
152
155
  eachById: (size: number, column?: string, handler?: (row: any) => Promise<void> | void) => Promise<void>
@@ -175,8 +178,8 @@ export declare interface BaseSelectQueryBuilder<DB extends DatabaseSchema<any>,
175
178
  count: () => Promise<number>
176
179
  avg: (column: keyof DB[TTable]['columns'] & string) => Promise<number>
177
180
  sum: (column: keyof DB[TTable]['columns'] & string) => Promise<number>
178
- max: (column: keyof DB[TTable]['columns'] & string) => Promise<any>
179
- min: (column: keyof DB[TTable]['columns'] & string) => Promise<any>
181
+ max: <K extends keyof DB[TTable]['columns'] & string>(column: K) => Promise<DB[TTable]['columns'][K] | null>
182
+ min: <K extends keyof DB[TTable]['columns'] & string>(column: K) => Promise<DB[TTable]['columns'][K] | null>
180
183
  readonly rows: TSelected[]
181
184
  readonly row: TSelected
182
185
  values: () => Promise<any[][]>
@@ -224,13 +227,19 @@ export declare interface TableQueryBuilder<DB extends DatabaseSchema<any>, TTabl
224
227
  insert: (data: Partial<DB[TTable]['columns']> | Partial<DB[TTable]['columns']>[]) => InsertQueryBuilder<DB, TTable>
225
228
  update: (values: Partial<DB[TTable]['columns']>) => UpdateQueryBuilder<DB, TTable>
226
229
  delete: () => DeleteQueryBuilder<DB, TTable>
227
- select: (...columns: (keyof DB[TTable]['columns'] & string)[]) => SelectQueryBuilder<DB, TTable, any>
230
+ select: <K extends keyof DB[TTable]['columns'] & string>(...columns: K[]) => SelectQueryBuilder<DB, TTable, Pick<DB[TTable]['columns'], K>>
228
231
  }
229
232
  export declare interface QueryBuilder<DB extends DatabaseSchema<any>> {
230
- select: <TTable extends keyof DB & string, K extends keyof DB[TTable]['columns'] & string>(
231
- table: TTable,
232
- ...columns: (K | `${string} as ${string}`)[]
233
- ) => SelectQueryBuilder<DB, TTable, any>
233
+ select: {
234
+ <TTable extends keyof DB & string, K extends keyof DB[TTable]['columns'] & string>(
235
+ table: TTable,
236
+ ...columns: K[]
237
+ ): SelectQueryBuilder<DB, TTable, Pick<DB[TTable]['columns'], K>>
238
+ <TTable extends keyof DB & string>(
239
+ table: TTable,
240
+ ...columns: ((keyof DB[TTable]['columns'] & string) | `${string} as ${string}`)[]
241
+ ): SelectQueryBuilder<DB, TTable, any>
242
+ }
234
243
  selectFrom: <TTable extends keyof DB & string>(table: TTable) => TypedSelectQueryBuilder<DB, TTable, DB[TTable]['columns'], TTable, `SELECT * FROM ${TTable}`>
235
244
  insertInto: <TTable extends keyof DB & string>(table: TTable) => TypedInsertQueryBuilder<DB, TTable>
236
245
  updateTable: <TTable extends keyof DB & string>(table: TTable) => UpdateQueryBuilder<DB, TTable>
@@ -456,11 +465,15 @@ declare type _TypedDynamicWhereMethods<DB extends DatabaseSchema<any>, TTable ex
456
465
  value: DB[TTable]['columns'][K],
457
466
  ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} AND ${K} = ?`>
458
467
  }
468
+ // NOTE: TypedSelectQueryBuilder must NOT also intersect DynamicWhereMethods —
469
+ // _TypedDynamicWhereMethods declares the same `where<Column>` keys, and the
470
+ // untyped variant (returning a plain SelectQueryBuilder) would win overload
471
+ // resolution, silently downgrading `toSQL()` from the composed literal SQL
472
+ // type back to `string` after any dynamic-where call.
459
473
  export type TypedSelectQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string, TSelected, TJoined extends string = TTable, TSql extends string = `SELECT * FROM ${TTable}`,> = Omit<
460
474
  BaseSelectQueryBuilder<DB, TTable, TSelected, TJoined>,
461
475
  'toSQL' | 'where' | 'andWhere' | 'orWhere' | 'orderBy' | 'limit'
462
- > & DynamicWhereMethods<DB, TTable, TSelected, TJoined>
463
- & _TypedDynamicWhereMethods<DB, TTable, TSelected, TJoined, TSql>
476
+ > & _TypedDynamicWhereMethods<DB, TTable, TSelected, TJoined, TSql>
464
477
  & {
465
478
  toSQL: () => TSql
466
479
  where: (<K extends keyof DB[TTable]['columns'] & string>(
package/dist/orm.d.ts CHANGED
@@ -317,8 +317,8 @@ declare class ModelInstance<TDef extends ModelDefinition, TSelected extends Colu
317
317
  getAttribute<K extends TSelected>(key: K): K extends keyof ModelAttributes<TDef> ? ModelAttributes<TDef>[K] : unknown;
318
318
  getAttributes(): Pick<ModelAttributes<TDef>, TSelected & keyof ModelAttributes<TDef>>;
319
319
  set<K extends ColumnName<TDef>>(key: K, value: K extends keyof ModelAttributes<TDef> ? ModelAttributes<TDef>[K] : unknown): void;
320
- only<K extends TSelected>(keys: ReadonlyArray<K>): Partial<ModelAttributes<TDef>>;
321
- except<K extends TSelected>(keys: ReadonlyArray<K>): Partial<ModelAttributes<TDef>>;
320
+ only<K extends TSelected>(keys: ReadonlyArray<K>): Pick<ModelAttributes<TDef>, K & keyof ModelAttributes<TDef>>;
321
+ except<K extends TSelected>(keys: ReadonlyArray<K>): Omit<Pick<ModelAttributes<TDef>, TSelected & keyof ModelAttributes<TDef>>, K>;
322
322
  getRelation<R extends InferRelationNames<TDef> & string>(name: R): LoadedRelationValue<TDef, R>;
323
323
  setRelation(name: string, data: ModelInstance<any, any>[] | ModelInstance<any, any> | null): void;
324
324
  getLoadedRelations(): Record<string, ModelInstance<any, any>[] | ModelInstance<any, any> | null>;
@@ -440,8 +440,8 @@ declare class ModelQueryBuilder<TDef extends ModelDefinition, TSelected extends
440
440
  to: number | null
441
441
  }>;
442
442
  pluck<K extends ColumnName<TDef>>(column: K): Promise<(K extends keyof ModelAttributes<TDef> ? ModelAttributes<TDef>[K] : unknown)[]>;
443
- max<K extends ColumnName<TDef>>(column: K): Promise<number | null>;
444
- min<K extends ColumnName<TDef>>(column: K): Promise<number | null>;
443
+ max<K extends ColumnName<TDef>>(column: K): Promise<(K extends keyof ModelAttributes<TDef> ? ModelAttributes<TDef>[K] : number) | null>;
444
+ min<K extends ColumnName<TDef>>(column: K): Promise<(K extends keyof ModelAttributes<TDef> ? ModelAttributes<TDef>[K] : number) | null>;
445
445
  avg<K extends NumericColumns<TDef>>(column: K): Promise<number>;
446
446
  sum<K extends NumericColumns<TDef>>(column: K): Promise<number>;
447
447
  delete(): Promise<number>;
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
 
@@ -25203,7 +25249,14 @@ class ModelQueryBuilder {
25203
25249
  sql2 += ` WHERE ${whereBody}`;
25204
25250
  }
25205
25251
  const row = await exec.get(sql2, params);
25206
- return row?.v == null ? null : Number(row.v);
25252
+ const v2 = row?.v;
25253
+ if (v2 == null)
25254
+ return null;
25255
+ if (typeof v2 === "string") {
25256
+ const n2 = Number(v2);
25257
+ return v2.trim() !== "" && !Number.isNaN(n2) ? n2 : v2;
25258
+ }
25259
+ return v2;
25207
25260
  }
25208
25261
  max(column) {
25209
25262
  return this.aggregate("MAX", column);
@@ -25212,10 +25265,10 @@ class ModelQueryBuilder {
25212
25265
  return this.aggregate("MIN", column);
25213
25266
  }
25214
25267
  async avg(column) {
25215
- return await this.aggregate("AVG", column) || 0;
25268
+ return Number(await this.aggregate("AVG", column) ?? 0) || 0;
25216
25269
  }
25217
25270
  async sum(column) {
25218
- return await this.aggregate("SUM", column) || 0;
25271
+ return Number(await this.aggregate("SUM", column) ?? 0) || 0;
25219
25272
  }
25220
25273
  async delete() {
25221
25274
  const exec = getExecutor();
@@ -25473,11 +25526,13 @@ async function seedModel(definition, count, faker) {
25473
25526
  await exec.run(`INSERT INTO ${definition.table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, Object.values(data));
25474
25527
  }
25475
25528
  }
25476
- 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;
25477
25530
  var init_orm = __esm(() => {
25478
25531
  init_config();
25479
25532
  init_db();
25480
25533
  SAFE_SQL_IDENTIFIER = /^[A-Z_][A-Z0-9_]*$/i;
25534
+ pgPlaceholderCache = new Map;
25535
+ btmKeysCache = new WeakMap;
25481
25536
  snakeCaseCache = new Map;
25482
25537
  tableNameCache = new Map;
25483
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.28",
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",