bun-query-builder 0.1.25 → 0.1.27

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
@@ -11694,7 +11694,7 @@ function setConfig(userConfig) {
11694
11694
  Object.assign(_config, config5);
11695
11695
  }
11696
11696
  }
11697
- var defaultConfig4, config5, _config = null, _lastConfiguredDialect = null, _warnedDialectConflicts;
11697
+ var defaultConfig4, CONFIG_SINGLETON_KEY, config5, _config = null, _lastConfiguredDialect = null, _warnedDialectConflicts;
11698
11698
  var init_config = __esm(() => {
11699
11699
  init_dist();
11700
11700
  defaultConfig4 = {
@@ -11754,7 +11754,8 @@ var init_config = __esm(() => {
11754
11754
  defaultFilter: true
11755
11755
  }
11756
11756
  };
11757
- config5 = { ...defaultConfig4 };
11757
+ CONFIG_SINGLETON_KEY = Symbol.for("bun-query-builder.config");
11758
+ config5 = globalThis[CONFIG_SINGLETON_KEY] ??= { ...defaultConfig4 };
11758
11759
  _warnedDialectConflicts = new Set;
11759
11760
  });
11760
11761
 
@@ -11975,6 +11976,16 @@ function createSQLiteSQL(filename) {
11975
11976
  return sqlFunction;
11976
11977
  }
11977
11978
  function createConnectionString(dialect, dbConfig) {
11979
+ if ((dialect === "postgres" || dialect === "mysql") && process19.env.DB_CONNECTION === dialect) {
11980
+ const e = process19.env;
11981
+ const envDb = (e.DB_DATABASE || dbConfig.database || "").replace(/^['"]|['"]$/g, "");
11982
+ const envUser = e.DB_USERNAME || dbConfig.username;
11983
+ const envPass = e.DB_PASSWORD ?? dbConfig.password ?? "";
11984
+ const envHost = e.DB_HOST || dbConfig.host || "localhost";
11985
+ const envPort = e.DB_PORT || dbConfig.port;
11986
+ const scheme = dialect === "postgres" ? "postgres" : "mysql";
11987
+ return `${scheme}://${envUser}:${envPass}@${envHost}${envPort ? `:${envPort}` : ""}/${envDb}`;
11988
+ }
11978
11989
  if (dbConfig.url) {
11979
11990
  return dbConfig.url;
11980
11991
  }
@@ -11993,6 +12004,21 @@ function createConnectionString(dialect, dbConfig) {
11993
12004
  throw new Error(`Unsupported dialect: ${dialect}`);
11994
12005
  }
11995
12006
  }
12007
+ function resolvePoolOptions(pool) {
12008
+ if (!pool)
12009
+ return {};
12010
+ const out = {};
12011
+ const toSeconds = (ms) => Math.max(0, Math.round(ms / 1000));
12012
+ if (typeof pool.max === "number" && Number.isFinite(pool.max))
12013
+ out.max = pool.max;
12014
+ if (typeof pool.idleTimeoutMs === "number" && Number.isFinite(pool.idleTimeoutMs))
12015
+ out.idleTimeout = toSeconds(pool.idleTimeoutMs);
12016
+ if (typeof pool.acquireTimeoutMs === "number" && Number.isFinite(pool.acquireTimeoutMs))
12017
+ out.connectionTimeout = toSeconds(pool.acquireTimeoutMs);
12018
+ if (typeof pool.maxLifetimeMs === "number" && Number.isFinite(pool.maxLifetimeMs))
12019
+ out.maxLifetime = toSeconds(pool.maxLifetimeMs);
12020
+ return out;
12021
+ }
11996
12022
  function getBunSql() {
11997
12023
  const dialect = config5.dialect;
11998
12024
  const connectionString = createConnectionString(dialect, config5.database);
@@ -12000,14 +12026,8 @@ function getBunSql() {
12000
12026
  if (dialect === "sqlite") {
12001
12027
  return createSQLiteSQL(connectionString);
12002
12028
  }
12003
- const sql = new SQL(connectionString);
12004
- if (sql && typeof sql.catch === "function") {
12005
- sql.catch((error) => {
12006
- if (config5.verbose && !error.message.includes("database") && !error.message.includes("does not exist")) {
12007
- console.warn(`[query-builder] Database connection error: ${error.message}`);
12008
- }
12009
- });
12010
- }
12029
+ const poolOptions = resolvePoolOptions(config5.database.pool);
12030
+ const sql = Object.keys(poolOptions).length > 0 ? new SQL(connectionString, poolOptions) : new SQL(connectionString);
12011
12031
  return sql;
12012
12032
  } catch (error) {
12013
12033
  console.error(`[query-builder] Failed to create database connection for dialect '${dialect}': ${error.message}`);
@@ -12017,19 +12037,31 @@ function getBunSql() {
12017
12037
  throw error;
12018
12038
  }
12019
12039
  }
12040
+ function connectionSignature() {
12041
+ const d = config5.database;
12042
+ return JSON.stringify({
12043
+ dialect: config5.dialect,
12044
+ database: d.database,
12045
+ username: d.username,
12046
+ password: d.password,
12047
+ host: d.host,
12048
+ port: d.port,
12049
+ url: d.url,
12050
+ pool: resolvePoolOptions(d.pool)
12051
+ });
12052
+ }
12020
12053
  function getOrCreateBunSql(forceNew = false) {
12021
- const configChanged = _bunSqlInstance !== null && (_currentDialect !== config5.dialect || _currentDatabase !== config5.database.database);
12054
+ const signature = connectionSignature();
12055
+ const configChanged = _bunSqlInstance !== null && _currentSignature !== signature;
12022
12056
  if (forceNew || configChanged || !_bunSqlInstance) {
12023
12057
  _bunSqlInstance = getBunSql();
12024
- _currentDialect = config5.dialect;
12025
- _currentDatabase = config5.database.database;
12058
+ _currentSignature = signature;
12026
12059
  }
12027
12060
  return _bunSqlInstance;
12028
12061
  }
12029
12062
  function resetConnection() {
12030
12063
  _bunSqlInstance = null;
12031
- _currentDialect = null;
12032
- _currentDatabase = null;
12064
+ _currentSignature = null;
12033
12065
  }
12034
12066
  async function withFreshConnection(fn) {
12035
12067
  try {
@@ -12044,7 +12076,7 @@ async function withFreshConnection(fn) {
12044
12076
  }
12045
12077
  }
12046
12078
  function createLazyBunSql() {
12047
- return new Proxy({}, {
12079
+ return new Proxy(function lazyBunSql() {}, {
12048
12080
  get(_target, prop) {
12049
12081
  const sql = getOrCreateBunSql();
12050
12082
  const value = sql[prop];
@@ -12059,20 +12091,10 @@ function createLazyBunSql() {
12059
12091
  }
12060
12092
  });
12061
12093
  }
12062
- var _bunSqlInstance = null, _currentDialect = null, _currentDatabase = null, bunSql;
12094
+ var _bunSqlInstance = null, _currentSignature = null, bunSql;
12063
12095
  var init_db = __esm(() => {
12064
12096
  init_config();
12065
12097
  bunSql = createLazyBunSql();
12066
- if (typeof process19 !== "undefined" && process19.on) {
12067
- const existingHandler = process19.listeners("unhandledRejection").find((h) => h.name === "sqlConnectionErrorHandler");
12068
- if (!existingHandler) {
12069
- let sqlConnectionErrorHandler = function(reason) {
12070
- if (reason && (reason.message?.includes("database") || reason.message?.includes("does not exist") || reason.code === "ERR_POSTGRES_SERVER_ERROR" || reason.code === "3D000")) {}
12071
- };
12072
- Object.defineProperty(sqlConnectionErrorHandler, "name", { value: "sqlConnectionErrorHandler" });
12073
- process19.on("unhandledRejection", sqlConnectionErrorHandler);
12074
- }
12075
- }
12076
12098
  });
12077
12099
 
12078
12100
  // src/actions/benchmark.ts
@@ -12322,6 +12344,47 @@ function* iterateAllPivots(meta, options = {}) {
12322
12344
  function isRawExpression(expr) {
12323
12345
  return typeof expr === "object" && expr !== null && "raw" in expr && typeof expr.raw === "string";
12324
12346
  }
12347
+ function quoteInsertIdent(id) {
12348
+ return config5.dialect === "mysql" ? `\`${id.replace(/`/g, "``")}\`` : `"${id.replace(/"/g, '""')}"`;
12349
+ }
12350
+ function buildInsertClause(rows, startIndex = 1) {
12351
+ const cols = Object.keys(rows[0] ?? {});
12352
+ const params = [];
12353
+ let idx = startIndex;
12354
+ const tuples = rows.map((row) => {
12355
+ const phs = cols.map((c) => {
12356
+ params.push(row[c]);
12357
+ return getPlaceholder(idx++);
12358
+ });
12359
+ return `(${phs.join(", ")})`;
12360
+ });
12361
+ return {
12362
+ colsSql: cols.map(quoteInsertIdent).join(", "),
12363
+ valuesSql: tuples.join(", "),
12364
+ params,
12365
+ nextIndex: idx
12366
+ };
12367
+ }
12368
+ function hasSlowQueryHook(h) {
12369
+ return Boolean(h && (h.onSlowQuery || h.slowQueryThresholdMs != null && h.slowQueryThresholdMs >= 0));
12370
+ }
12371
+ function renderSelectColumn(col) {
12372
+ if (typeof col === "string")
12373
+ return col;
12374
+ if (isRawExpression(col))
12375
+ return col.raw;
12376
+ if (col && typeof col === "object") {
12377
+ const anyCol = col;
12378
+ if (typeof anyCol.raw === "function")
12379
+ return String(anyCol.raw());
12380
+ if (typeof anyCol.sql === "string")
12381
+ return anyCol.sql;
12382
+ const str = String(col);
12383
+ if (str !== "[object Object]")
12384
+ return str;
12385
+ }
12386
+ throw new TypeError(`[query-builder] select(): unsupported column ${String(col)} \u2014 pass a column name, a string[], or a SQL fragment (e.g. sql\`count(*) as c\`)`);
12387
+ }
12325
12388
  function validateIdentifier(name, context) {
12326
12389
  if (!SQL_PATTERNS.IDENTIFIER.test(name)) {
12327
12390
  const contextMsg = context ? ` in ${context}` : "";
@@ -12340,13 +12403,29 @@ class QueryCache {
12340
12403
  this.cache.delete(key);
12341
12404
  return null;
12342
12405
  }
12406
+ this.cache.delete(key);
12407
+ this.cache.set(key, entry);
12343
12408
  return entry.data;
12344
12409
  }
12345
12410
  set(key, data, ttlMs) {
12411
+ if (this.cache.has(key))
12412
+ this.cache.delete(key);
12346
12413
  if (this.cache.size >= this.maxSize) {
12347
- const firstKey = this.cache.keys().next().value;
12348
- if (firstKey)
12349
- this.cache.delete(firstKey);
12414
+ const now = Date.now();
12415
+ let evicted = false;
12416
+ for (const [k, v] of this.cache) {
12417
+ if (now > v.expiresAt) {
12418
+ this.cache.delete(k);
12419
+ evicted = true;
12420
+ if (this.cache.size < this.maxSize)
12421
+ break;
12422
+ }
12423
+ }
12424
+ if (!evicted) {
12425
+ const lruKey = this.cache.keys().next().value;
12426
+ if (lruKey !== undefined)
12427
+ this.cache.delete(lruKey);
12428
+ }
12350
12429
  }
12351
12430
  this.cache.set(key, {
12352
12431
  data,
@@ -12417,6 +12496,9 @@ function reorderSelectClauses(sql) {
12417
12496
  continue;
12418
12497
  if (i === 0 || !/\s/.test(sql[i - 1]))
12419
12498
  continue;
12499
+ const lead = sql.charCodeAt(i) & ~32;
12500
+ if (lead !== 71 && lead !== 79 && lead !== 72 && lead !== 76 && lead !== 87)
12501
+ continue;
12420
12502
  const rest = sql.slice(i);
12421
12503
  for (const { key, tokens } of KEYWORDS) {
12422
12504
  const m = rest.match(tokens);
@@ -12536,20 +12618,35 @@ function createQueryBuilder(state) {
12536
12618
  config5.debug.captureText = prev;
12537
12619
  return s;
12538
12620
  }
12621
+ function computeParams(q) {
12622
+ if (!q || typeof q !== "object")
12623
+ return;
12624
+ if (Array.isArray(q.values))
12625
+ return q.values;
12626
+ if (Array.isArray(q.parameters))
12627
+ return q.parameters;
12628
+ if (Array.isArray(q.params))
12629
+ return q.params;
12630
+ return;
12631
+ }
12539
12632
  function runWithHooks(q, kind, opts) {
12540
12633
  const hooks = config5.hooks;
12541
- const hasHooks = hooks && (hooks.onQueryStart || hooks.onQueryEnd || hooks.onQueryError || hooks.startSpan);
12634
+ const slowMs = hooks?.slowQueryThresholdMs;
12635
+ const slowEnabled = slowMs != null && slowMs >= 0;
12636
+ const hasSlowQuery = Boolean(hooks?.onSlowQuery || slowEnabled);
12637
+ const hasHooks = hooks && (hooks.onQueryStart || hooks.onQueryEnd || hooks.onQueryError || hooks.startSpan || hasSlowQuery);
12542
12638
  const hasTimeoutOrSignal = opts?.timeoutMs && opts.timeoutMs > 0 || opts?.signal;
12543
12639
  if (!hasHooks && !hasTimeoutOrSignal) {
12544
12640
  return q.execute();
12545
12641
  }
12546
12642
  const text = computeSqlText(q);
12643
+ const params = computeParams(q);
12547
12644
  const startAt = Date.now();
12548
12645
  let span;
12549
12646
  try {
12550
- hooks?.onQueryStart?.({ sql: text, kind });
12647
+ hooks?.onQueryStart?.({ sql: text, params, kind });
12551
12648
  if (hooks?.startSpan)
12552
- span = hooks.startSpan({ sql: text, kind });
12649
+ span = hooks.startSpan({ sql: text, params, kind });
12553
12650
  } catch {}
12554
12651
  let finished = false;
12555
12652
  const finish = (err, rowCount) => {
@@ -12559,9 +12656,15 @@ function createQueryBuilder(state) {
12559
12656
  const durationMs = Date.now() - startAt;
12560
12657
  try {
12561
12658
  if (err) {
12562
- hooks?.onQueryError?.({ sql: text, error: err, durationMs, kind });
12659
+ hooks?.onQueryError?.({ sql: text, params, error: err, durationMs, kind });
12563
12660
  } else {
12564
- hooks?.onQueryEnd?.({ sql: text, durationMs, rowCount, kind });
12661
+ hooks?.onQueryEnd?.({ sql: text, params, durationMs, rowCount, kind });
12662
+ if (slowEnabled && durationMs >= slowMs) {
12663
+ if (hooks?.onSlowQuery)
12664
+ hooks.onSlowQuery({ sql: text, params, durationMs, kind });
12665
+ else
12666
+ console.warn(`[query-builder] slow query (${durationMs}ms >= ${slowMs}ms): ${text}`);
12667
+ }
12565
12668
  }
12566
12669
  } catch {}
12567
12670
  try {
@@ -12641,6 +12744,36 @@ function createQueryBuilder(state) {
12641
12744
  const p = hasWhere ? prefix : "WHERE";
12642
12745
  text = `${text} ${p} ${clause}`;
12643
12746
  };
12747
+ const appendSetOp = (op, other) => {
12748
+ const st = other.__rawState?.();
12749
+ if (st) {
12750
+ const offset = whereParams.length;
12751
+ const otherSql = config5.dialect === "postgres" ? st.sql.replace(/\$(\d+)/g, (_m, n) => `$${Number(n) + offset}`) : st.sql;
12752
+ text += ` ${op} ${otherSql}`;
12753
+ whereParams.push(...st.params);
12754
+ } else {
12755
+ text += ` ${op} ${String(other.toSQL())}`;
12756
+ }
12757
+ built = null;
12758
+ };
12759
+ const insertJoin = (joinClause) => {
12760
+ const re = /\(|\)|\b(?:WHERE|GROUP BY|HAVING|ORDER BY|LIMIT|OFFSET|UNION)\b/gi;
12761
+ let depth = 0;
12762
+ let cut = -1;
12763
+ let mm;
12764
+ while (mm = re.exec(text)) {
12765
+ if (mm[0] === "(") {
12766
+ depth++;
12767
+ } else if (mm[0] === ")") {
12768
+ depth = Math.max(0, depth - 1);
12769
+ } else if (depth === 0) {
12770
+ cut = mm.index;
12771
+ break;
12772
+ }
12773
+ }
12774
+ text = cut >= 0 ? `${text.slice(0, cut)}${joinClause} ${text.slice(cut)}` : `${text} ${joinClause}`;
12775
+ built = null;
12776
+ };
12644
12777
  const joinedTables = new Set;
12645
12778
  let timeoutMs;
12646
12779
  let abortSignal;
@@ -12725,6 +12858,18 @@ function createQueryBuilder(state) {
12725
12858
  }
12726
12859
  }
12727
12860
  };
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
+ const addWindowFunction = (fnExpr, alias, partitionBy, orderBy) => {
12871
+ addToSelectClause(`${fnExpr} ${buildOverClause(partitionBy, orderBy)} AS ${alias}`);
12872
+ };
12728
12873
  function assertSafeWhereOperator(op, context) {
12729
12874
  if (typeof op !== "string")
12730
12875
  throw new TypeError(`[query-builder] ${context}: operator must be a string, got ${typeof op}`);
@@ -12864,6 +13009,46 @@ function createQueryBuilder(state) {
12864
13009
  validateIdentifier(fkA, "withCount (foreign key)");
12865
13010
  return `(SELECT COUNT(*) FROM ${pivot} WHERE ${pivot}.${fkA} = ${parentTable}.${pk})`;
12866
13011
  };
13012
+ const applyRelationAggregate = (fn, relation, column) => {
13013
+ if (!meta)
13014
+ return;
13015
+ validateIdentifier(column, `with${fn[0]}${fn.slice(1).toLowerCase()} (column)`);
13016
+ const parentTable = String(table);
13017
+ const rels = meta.relations?.[parentTable];
13018
+ if (!rels)
13019
+ return;
13020
+ const found = Object.entries(rels).find(([_t, relMap2]) => relMap2 && typeof relMap2 === "object" && (relation in relMap2));
13021
+ if (!found)
13022
+ return;
13023
+ const [type, relMap] = found;
13024
+ const entry = relMap[relation];
13025
+ const targetModel = typeof entry === "string" ? entry : entry?.model || entry?.target || entry;
13026
+ const targetTable = meta.modelToTable[targetModel] || targetModel;
13027
+ const pk = meta.primaryKeys[parentTable] ?? "id";
13028
+ validateIdentifier(targetTable, `with${fn} (target table)`);
13029
+ const aggExpr = `${fn}(${targetTable}.${column})`;
13030
+ let sub;
13031
+ if (type === "hasMany" || type === "hasOne") {
13032
+ const fk = `${parentTable.endsWith("s") ? parentTable.slice(0, -1) : parentTable}_id`;
13033
+ validateIdentifier(fk, `with${fn} (foreign key)`);
13034
+ sub = `(SELECT ${aggExpr} FROM ${targetTable} WHERE ${targetTable}.${fk} = ${parentTable}.${pk})`;
13035
+ } else if (type === "belongsToMany") {
13036
+ const resolved = meta ? resolvePivot(meta, parentTable, relation, { singularize, models: meta.models }) : null;
13037
+ const a = singularize(parentTable);
13038
+ const b = singularize(targetTable);
13039
+ const pivot = resolved?.pivotTable ?? [a, b].sort().join("_");
13040
+ const fkA = resolved?.fkParent ?? `${a}_id`;
13041
+ const fkB = resolved?.fkRelated ?? `${b}_id`;
13042
+ const targetPk = meta.primaryKeys[targetTable] ?? "id";
13043
+ validateIdentifier(pivot, `with${fn} (pivot table)`);
13044
+ validateIdentifier(fkA, `with${fn} (foreign key)`);
13045
+ validateIdentifier(fkB, `with${fn} (related key)`);
13046
+ sub = `(SELECT ${aggExpr} FROM ${pivot} JOIN ${targetTable} ON ${targetTable}.${targetPk} = ${pivot}.${fkB} WHERE ${pivot}.${fkA} = ${parentTable}.${pk})`;
13047
+ } else {
13048
+ return;
13049
+ }
13050
+ addToSelectClause(`${sub} AS ${relation}_${fn.toLowerCase()}_${column}`);
13051
+ };
12867
13052
  const applyPivotColumnsToQuery = () => {
12868
13053
  if (pivotColumns.size === 0)
12869
13054
  return;
@@ -12958,6 +13143,52 @@ function createQueryBuilder(state) {
12958
13143
  built = null;
12959
13144
  return this;
12960
13145
  },
13146
+ over(expression, alias, opts = {}) {
13147
+ addWindowFunction(expression, alias, opts.partitionBy, opts.orderBy);
13148
+ return this;
13149
+ },
13150
+ lag(column, opts = {}) {
13151
+ const args = [column, String(opts.offset ?? 1)];
13152
+ if (opts.defaultValue !== undefined)
13153
+ args.push(String(opts.defaultValue));
13154
+ addWindowFunction(`LAG(${args.join(", ")})`, opts.alias ?? `${column}_lag`, opts.partitionBy, opts.orderBy);
13155
+ return this;
13156
+ },
13157
+ lead(column, opts = {}) {
13158
+ const args = [column, String(opts.offset ?? 1)];
13159
+ if (opts.defaultValue !== undefined)
13160
+ args.push(String(opts.defaultValue));
13161
+ addWindowFunction(`LEAD(${args.join(", ")})`, opts.alias ?? `${column}_lead`, opts.partitionBy, opts.orderBy);
13162
+ return this;
13163
+ },
13164
+ sumOver(column, opts = {}) {
13165
+ addWindowFunction(`SUM(${column})`, opts.alias ?? `${column}_sum`, opts.partitionBy, opts.orderBy);
13166
+ return this;
13167
+ },
13168
+ avgOver(column, opts = {}) {
13169
+ addWindowFunction(`AVG(${column})`, opts.alias ?? `${column}_avg`, opts.partitionBy, opts.orderBy);
13170
+ return this;
13171
+ },
13172
+ countOver(column = "*", opts = {}) {
13173
+ addWindowFunction(`COUNT(${column})`, opts.alias ?? "count_over", opts.partitionBy, opts.orderBy);
13174
+ return this;
13175
+ },
13176
+ minOver(column, opts = {}) {
13177
+ addWindowFunction(`MIN(${column})`, opts.alias ?? `${column}_min`, opts.partitionBy, opts.orderBy);
13178
+ return this;
13179
+ },
13180
+ maxOver(column, opts = {}) {
13181
+ addWindowFunction(`MAX(${column})`, opts.alias ?? `${column}_max`, opts.partitionBy, opts.orderBy);
13182
+ return this;
13183
+ },
13184
+ firstValue(column, opts = {}) {
13185
+ addWindowFunction(`FIRST_VALUE(${column})`, opts.alias ?? `${column}_first`, opts.partitionBy, opts.orderBy);
13186
+ return this;
13187
+ },
13188
+ lastValue(column, opts = {}) {
13189
+ addWindowFunction(`LAST_VALUE(${column})`, opts.alias ?? `${column}_last`, opts.partitionBy, opts.orderBy);
13190
+ return this;
13191
+ },
12961
13192
  selectAll() {
12962
13193
  return this;
12963
13194
  },
@@ -12967,22 +13198,24 @@ function createQueryBuilder(state) {
12967
13198
  const cols = Array.isArray(columns2) ? columns2 : [columns2];
12968
13199
  if (cols.length === 0)
12969
13200
  return this;
13201
+ const rendered = cols.map(renderSelectColumn);
12970
13202
  const fromIndex = text.indexOf(" FROM ");
12971
13203
  if (fromIndex !== -1) {
12972
- text = `SELECT ${cols.join(", ")}${text.substring(fromIndex)}`;
13204
+ text = `SELECT ${rendered.join(", ")}${text.substring(fromIndex)}`;
12973
13205
  } else {
12974
- text = `SELECT ${cols.join(", ")} FROM ${table}`;
13206
+ text = `SELECT ${rendered.join(", ")} FROM ${table}`;
12975
13207
  }
12976
13208
  return this;
12977
13209
  },
12978
13210
  addSelect(...columns2) {
12979
13211
  if (!columns2.length)
12980
13212
  return this;
13213
+ const rendered = columns2.map(renderSelectColumn);
12981
13214
  const fromIdx = text.indexOf(" FROM ");
12982
13215
  if (fromIdx !== -1) {
12983
- text = `${text.substring(0, fromIdx)}, ${columns2.join(", ")}${text.substring(fromIdx)}`;
13216
+ text = `${text.substring(0, fromIdx)}, ${rendered.join(", ")}${text.substring(fromIdx)}`;
12984
13217
  } else {
12985
- text += `, ${columns2.join(", ")}`;
13218
+ text += `, ${rendered.join(", ")}`;
12986
13219
  }
12987
13220
  built = null;
12988
13221
  return this;
@@ -13042,28 +13275,6 @@ function createQueryBuilder(state) {
13042
13275
  if (!rels) {
13043
13276
  return fromTable;
13044
13277
  }
13045
- const _buildConditionalJoin = (baseJoinCondition, targetTable2) => {
13046
- let joinCondition = baseJoinCondition;
13047
- if (config5.softDeletes?.enabled && config5.softDeletes?.defaultFilter) {
13048
- const softDeleteColumn = config5.softDeletes.column || "deleted_at";
13049
- joinCondition = `${joinCondition} AND ${targetTable2}.${softDeleteColumn} IS NULL`;
13050
- }
13051
- if (!condition)
13052
- return joinCondition;
13053
- const conditionBuilder = {
13054
- where: (col, op, val) => {
13055
- const valStr = typeof val === "string" ? `'${val}'` : String(val);
13056
- return `${targetTable2}.${col} ${op} ${valStr}`;
13057
- }
13058
- };
13059
- try {
13060
- const additionalCondition = condition(conditionBuilder);
13061
- if (additionalCondition && typeof additionalCondition === "string") {
13062
- return `${joinCondition} AND ${additionalCondition}`;
13063
- }
13064
- } catch {}
13065
- return joinCondition;
13066
- };
13067
13278
  const addSoftDeleteCheck = (table2) => {
13068
13279
  if (config5.softDeletes?.enabled && config5.softDeletes?.defaultFilter) {
13069
13280
  const softDeleteColumn = config5.softDeletes.column || "deleted_at";
@@ -13399,6 +13610,22 @@ function createQueryBuilder(state) {
13399
13610
  }
13400
13611
  return this;
13401
13612
  },
13613
+ withSum(relation, column) {
13614
+ applyRelationAggregate("SUM", relation, column);
13615
+ return this;
13616
+ },
13617
+ withAvg(relation, column) {
13618
+ applyRelationAggregate("AVG", relation, column);
13619
+ return this;
13620
+ },
13621
+ withMax(relation, column) {
13622
+ applyRelationAggregate("MAX", relation, column);
13623
+ return this;
13624
+ },
13625
+ withMin(relation, column) {
13626
+ applyRelationAggregate("MIN", relation, column);
13627
+ return this;
13628
+ },
13402
13629
  applyPivotColumns() {
13403
13630
  applyPivotColumnsToQuery();
13404
13631
  return this;
@@ -13526,6 +13753,8 @@ function createQueryBuilder(state) {
13526
13753
  where(expr, op, value) {
13527
13754
  const getWhereKeyword = () => SQL_PATTERNS.WHERE.test(text) ? "AND" : "WHERE";
13528
13755
  if (typeof expr === "string" && op !== undefined) {
13756
+ validateIdentifier(String(expr), "where(column)");
13757
+ assertSafeWhereOperator(op, "where(operator)");
13529
13758
  const operator = String(op).toLowerCase();
13530
13759
  if (operator === "in" || operator === "not in") {
13531
13760
  const values = Array.isArray(value) ? value : [value];
@@ -13547,7 +13776,8 @@ function createQueryBuilder(state) {
13547
13776
  if (Array.isArray(expr)) {
13548
13777
  const [col, op2, val] = expr;
13549
13778
  const colName = String(col);
13550
- const operator = String(op2);
13779
+ validateIdentifier(colName, "where(column)");
13780
+ const operator = assertSafeWhereOperator(op2, "where(operator)");
13551
13781
  if (operator === "in" || operator === "not in") {
13552
13782
  const values = Array.isArray(val) ? val : [val];
13553
13783
  const placeholders = getPlaceholders(values.length, whereParams.length + 1);
@@ -13569,6 +13799,7 @@ function createQueryBuilder(state) {
13569
13799
  const keys = Object.keys(whereObject);
13570
13800
  const conditions = [];
13571
13801
  for (const key of keys) {
13802
+ validateIdentifier(key, "where(column)");
13572
13803
  const value2 = whereObject[key];
13573
13804
  if (Array.isArray(value2)) {
13574
13805
  const placeholders = getPlaceholders(value2.length, whereParams.length + 1);
@@ -13597,21 +13828,25 @@ function createQueryBuilder(state) {
13597
13828
  return this;
13598
13829
  },
13599
13830
  whereNull(column) {
13831
+ validateIdentifier(String(column), "whereNull(column)");
13600
13832
  const keyword = SQL_PATTERNS.WHERE.test(text) ? "AND" : "WHERE";
13601
13833
  text = `${text} ${keyword} ${String(column)} IS NULL`;
13602
13834
  built = null;
13603
13835
  return this;
13604
13836
  },
13605
13837
  whereNotNull(column) {
13838
+ validateIdentifier(String(column), "whereNotNull(column)");
13606
13839
  const keyword = SQL_PATTERNS.WHERE.test(text) ? "AND" : "WHERE";
13607
13840
  text = `${text} ${keyword} ${String(column)} IS NOT NULL`;
13608
13841
  built = null;
13609
13842
  return this;
13610
13843
  },
13611
13844
  whereBetween(column, start, end) {
13845
+ validateIdentifier(String(column), "whereBetween(column)");
13612
13846
  const keyword = SQL_PATTERNS.WHERE.test(text) ? "AND" : "WHERE";
13847
+ const i = whereParams.length + 1;
13848
+ text = `${text} ${keyword} ${String(column)} BETWEEN ${getPlaceholder(i)} AND ${getPlaceholder(i + 1)}`;
13613
13849
  whereParams.push(start, end);
13614
- text = `${text} ${keyword} ${String(column)} BETWEEN ? AND ?`;
13615
13850
  built = null;
13616
13851
  return this;
13617
13852
  },
@@ -13622,10 +13857,32 @@ function createQueryBuilder(state) {
13622
13857
  return this;
13623
13858
  },
13624
13859
  whereJsonContains(column, json) {
13860
+ validateIdentifier(String(column), "whereJsonContains(column)");
13625
13861
  const keyword = SQL_PATTERNS.WHERE.test(text) ? "AND" : "WHERE";
13862
+ const dialect = config5.dialect;
13626
13863
  const idx = whereParams.length + 1;
13627
- text += ` ${keyword} ${column} @> ${getPlaceholder(idx)}`;
13628
- whereParams.push(JSON.stringify(json));
13864
+ if (dialect === "postgres") {
13865
+ if (config5.sql?.jsonContainsMode === "function")
13866
+ text += ` ${keyword} jsonb_contains(${column}, ${getPlaceholder(idx)})`;
13867
+ else
13868
+ text += ` ${keyword} ${column} @> ${getPlaceholder(idx)}`;
13869
+ whereParams.push(JSON.stringify(json));
13870
+ } else if (dialect === "mysql") {
13871
+ text += ` ${keyword} JSON_CONTAINS(${column}, ${getPlaceholder(idx)})`;
13872
+ whereParams.push(JSON.stringify(json));
13873
+ } else {
13874
+ if (Array.isArray(json)) {
13875
+ const conds = json.map((_, i) => `EXISTS (SELECT 1 FROM json_each(${column}) WHERE json_each.value = ${getPlaceholder(idx + i)})`);
13876
+ text += ` ${keyword} (${conds.join(" AND ")})`;
13877
+ for (const v of json)
13878
+ whereParams.push(v);
13879
+ } else if (json !== null && typeof json === "object") {
13880
+ throw new Error("[query-builder] whereJsonContains: object containment is not supported on SQLite \u2014 pass a scalar or array, or use whereJsonPath.");
13881
+ } else {
13882
+ text += ` ${keyword} EXISTS (SELECT 1 FROM json_each(${column}) WHERE json_each.value = ${getPlaceholder(idx)})`;
13883
+ whereParams.push(json);
13884
+ }
13885
+ }
13629
13886
  built = null;
13630
13887
  return this;
13631
13888
  },
@@ -13652,69 +13909,85 @@ function createQueryBuilder(state) {
13652
13909
  whereLike(column, pattern, caseSensitive = false) {
13653
13910
  const expr = caseSensitive ? sql`${sql(String(column))} LIKE ${pattern}` : sql`LOWER(${sql(String(column))}) LIKE LOWER(${pattern})`;
13654
13911
  built = sql`${ensureBuilt()} WHERE ${expr}`;
13655
- addWhereText("WHERE", `${caseSensitive ? String(column) : `LOWER(${String(column)})`} LIKE ${caseSensitive ? "?" : "LOWER(?)"}`);
13912
+ const ph = getPlaceholder(whereParams.length + 1);
13913
+ addWhereText("WHERE", `${caseSensitive ? String(column) : `LOWER(${String(column)})`} LIKE ${caseSensitive ? ph : `LOWER(${ph})`}`);
13914
+ whereParams.push(pattern);
13656
13915
  return this;
13657
13916
  },
13658
13917
  whereILike(column, pattern) {
13918
+ const ph = getPlaceholder(whereParams.length + 1);
13659
13919
  if (config5.dialect === "postgres") {
13660
13920
  built = sql`${ensureBuilt()} WHERE ${sql(String(column))} ILIKE ${pattern}`;
13661
- addWhereText("WHERE", `${String(column)} ILIKE ?`);
13921
+ addWhereText("WHERE", `${String(column)} ILIKE ${ph}`);
13662
13922
  } else {
13663
13923
  const expr = sql`LOWER(${sql(String(column))}) LIKE LOWER(${pattern})`;
13664
13924
  built = sql`${ensureBuilt()} WHERE ${expr}`;
13665
- addWhereText("WHERE", `LOWER(${String(column)}) LIKE LOWER(?)`);
13925
+ addWhereText("WHERE", `LOWER(${String(column)}) LIKE LOWER(${ph})`);
13666
13926
  }
13927
+ whereParams.push(pattern);
13667
13928
  return this;
13668
13929
  },
13669
13930
  orWhereLike(column, pattern, caseSensitive = false) {
13670
13931
  const expr = caseSensitive ? sql`${sql(String(column))} LIKE ${pattern}` : sql`LOWER(${sql(String(column))}) LIKE LOWER(${pattern})`;
13671
13932
  built = sql`${ensureBuilt()} OR ${expr}`;
13672
- addWhereText("OR", `${caseSensitive ? String(column) : `LOWER(${String(column)})`} LIKE ${caseSensitive ? "?" : "LOWER(?)"}`);
13933
+ const ph = getPlaceholder(whereParams.length + 1);
13934
+ addWhereText("OR", `${caseSensitive ? String(column) : `LOWER(${String(column)})`} LIKE ${caseSensitive ? ph : `LOWER(${ph})`}`);
13935
+ whereParams.push(pattern);
13673
13936
  return this;
13674
13937
  },
13675
13938
  orWhereILike(column, pattern) {
13939
+ const ph = getPlaceholder(whereParams.length + 1);
13676
13940
  if (config5.dialect === "postgres") {
13677
13941
  built = sql`${ensureBuilt()} OR ${sql(String(column))} ILIKE ${pattern}`;
13678
- addWhereText("OR", `${String(column)} ILIKE ?`);
13942
+ addWhereText("OR", `${String(column)} ILIKE ${ph}`);
13679
13943
  } else {
13680
13944
  const expr = sql`LOWER(${sql(String(column))}) LIKE LOWER(${pattern})`;
13681
13945
  built = sql`${ensureBuilt()} OR ${expr}`;
13682
- addWhereText("OR", `LOWER(${String(column)}) LIKE LOWER(?)`);
13946
+ addWhereText("OR", `LOWER(${String(column)}) LIKE LOWER(${ph})`);
13683
13947
  }
13948
+ whereParams.push(pattern);
13684
13949
  return this;
13685
13950
  },
13686
13951
  whereNotLike(column, pattern, caseSensitive = false) {
13687
13952
  const expr = caseSensitive ? sql`${sql(String(column))} NOT LIKE ${pattern}` : sql`LOWER(${sql(String(column))}) NOT LIKE LOWER(${pattern})`;
13688
13953
  built = sql`${ensureBuilt()} WHERE ${expr}`;
13689
- addWhereText("WHERE", `${caseSensitive ? String(column) : `LOWER(${String(column)})`} NOT LIKE ${caseSensitive ? "?" : "LOWER(?)"}`);
13954
+ const ph = getPlaceholder(whereParams.length + 1);
13955
+ addWhereText("WHERE", `${caseSensitive ? String(column) : `LOWER(${String(column)})`} NOT LIKE ${caseSensitive ? ph : `LOWER(${ph})`}`);
13956
+ whereParams.push(pattern);
13690
13957
  return this;
13691
13958
  },
13692
13959
  whereNotILike(column, pattern) {
13960
+ const ph = getPlaceholder(whereParams.length + 1);
13693
13961
  if (config5.dialect === "postgres") {
13694
13962
  built = sql`${ensureBuilt()} WHERE ${sql(String(column))} NOT ILIKE ${pattern}`;
13695
- addWhereText("WHERE", `${String(column)} NOT ILIKE ?`);
13963
+ addWhereText("WHERE", `${String(column)} NOT ILIKE ${ph}`);
13696
13964
  } else {
13697
13965
  const expr = sql`LOWER(${sql(String(column))}) NOT LIKE LOWER(${pattern})`;
13698
13966
  built = sql`${ensureBuilt()} WHERE ${expr}`;
13699
- addWhereText("WHERE", `LOWER(${String(column)}) NOT LIKE LOWER(?)`);
13967
+ addWhereText("WHERE", `LOWER(${String(column)}) NOT LIKE LOWER(${ph})`);
13700
13968
  }
13969
+ whereParams.push(pattern);
13701
13970
  return this;
13702
13971
  },
13703
13972
  orWhereNotLike(column, pattern, caseSensitive = false) {
13704
13973
  const expr = caseSensitive ? sql`${sql(String(column))} NOT LIKE ${pattern}` : sql`LOWER(${sql(String(column))}) NOT LIKE LOWER(${pattern})`;
13705
13974
  built = sql`${ensureBuilt()} OR ${expr}`;
13706
- addWhereText("OR", `${caseSensitive ? String(column) : `LOWER(${String(column)})`} NOT LIKE ${caseSensitive ? "?" : "LOWER(?)"}`);
13975
+ const ph = getPlaceholder(whereParams.length + 1);
13976
+ addWhereText("OR", `${caseSensitive ? String(column) : `LOWER(${String(column)})`} NOT LIKE ${caseSensitive ? ph : `LOWER(${ph})`}`);
13977
+ whereParams.push(pattern);
13707
13978
  return this;
13708
13979
  },
13709
13980
  orWhereNotILike(column, pattern) {
13981
+ const ph = getPlaceholder(whereParams.length + 1);
13710
13982
  if (config5.dialect === "postgres") {
13711
13983
  built = sql`${ensureBuilt()} OR ${sql(String(column))} NOT ILIKE ${pattern}`;
13712
- addWhereText("OR", `${String(column)} NOT ILIKE ?`);
13984
+ addWhereText("OR", `${String(column)} NOT ILIKE ${ph}`);
13713
13985
  } else {
13714
13986
  const expr = sql`LOWER(${sql(String(column))}) NOT LIKE LOWER(${pattern})`;
13715
13987
  built = sql`${ensureBuilt()} OR ${expr}`;
13716
- addWhereText("OR", `LOWER(${String(column)}) NOT LIKE LOWER(?)`);
13988
+ addWhereText("OR", `LOWER(${String(column)}) NOT LIKE LOWER(${ph})`);
13717
13989
  }
13990
+ whereParams.push(pattern);
13718
13991
  return this;
13719
13992
  },
13720
13993
  whereAny(cols, op, value) {
@@ -13754,8 +14027,10 @@ function createQueryBuilder(state) {
13754
14027
  return this;
13755
14028
  },
13756
14029
  whereNotBetween(column, start, end) {
14030
+ validateIdentifier(String(column), "whereNotBetween(column)");
13757
14031
  const keyword = SQL_PATTERNS.WHERE.test(text) ? "AND" : "WHERE";
13758
- text += ` ${keyword} ${column} NOT BETWEEN ? AND ?`;
14032
+ const i = whereParams.length + 1;
14033
+ text += ` ${keyword} ${column} NOT BETWEEN ${getPlaceholder(i)} AND ${getPlaceholder(i + 1)}`;
13759
14034
  whereParams.push(start, end);
13760
14035
  built = null;
13761
14036
  return this;
@@ -13795,6 +14070,7 @@ function createQueryBuilder(state) {
13795
14070
  return this;
13796
14071
  },
13797
14072
  whereIn(column, values) {
14073
+ validateIdentifier(String(column), "whereIn(column)");
13798
14074
  const keyword = SQL_PATTERNS.WHERE.test(text) ? "AND" : "WHERE";
13799
14075
  if (Array.isArray(values)) {
13800
14076
  const placeholders = getPlaceholders(values.length, whereParams.length + 1);
@@ -13807,6 +14083,7 @@ function createQueryBuilder(state) {
13807
14083
  return this;
13808
14084
  },
13809
14085
  orWhereIn(column, values) {
14086
+ validateIdentifier(String(column), "orWhereIn(column)");
13810
14087
  if (Array.isArray(values)) {
13811
14088
  const placeholders = getPlaceholders(values.length, whereParams.length + 1);
13812
14089
  text += ` OR ${column} IN (${placeholders})`;
@@ -13818,6 +14095,7 @@ function createQueryBuilder(state) {
13818
14095
  return this;
13819
14096
  },
13820
14097
  whereNotIn(column, values) {
14098
+ validateIdentifier(String(column), "whereNotIn(column)");
13821
14099
  const keyword = SQL_PATTERNS.WHERE.test(text) ? "AND" : "WHERE";
13822
14100
  if (Array.isArray(values)) {
13823
14101
  const placeholders = getPlaceholders(values.length, whereParams.length + 1);
@@ -13830,6 +14108,7 @@ function createQueryBuilder(state) {
13830
14108
  return this;
13831
14109
  },
13832
14110
  orWhereNotIn(column, values) {
14111
+ validateIdentifier(String(column), "orWhereNotIn(column)");
13833
14112
  if (Array.isArray(values)) {
13834
14113
  const placeholders = getPlaceholders(values.length, whereParams.length + 1);
13835
14114
  text += ` OR ${column} NOT IN (${placeholders})`;
@@ -13855,6 +14134,8 @@ function createQueryBuilder(state) {
13855
14134
  },
13856
14135
  andWhere(expr, op, value) {
13857
14136
  if (typeof expr === "string" && op !== undefined) {
14137
+ validateIdentifier(String(expr), "andWhere(column)");
14138
+ assertSafeWhereOperator(op, "andWhere(operator)");
13858
14139
  const paramIndex = whereParams.length + 1;
13859
14140
  whereConditions.push(`${String(expr)} ${String(op)} ${getPlaceholder(paramIndex)}`);
13860
14141
  whereParams.push(value);
@@ -13865,7 +14146,8 @@ function createQueryBuilder(state) {
13865
14146
  if (Array.isArray(expr)) {
13866
14147
  const [col, op2, val] = expr;
13867
14148
  const colName = String(col);
13868
- const operator = String(op2);
14149
+ validateIdentifier(colName, "andWhere(column)");
14150
+ const operator = assertSafeWhereOperator(op2, "andWhere(operator)");
13869
14151
  if (operator === "in" || operator === "not in") {
13870
14152
  const values = Array.isArray(val) ? val : [val];
13871
14153
  const placeholders = getPlaceholders(values.length, whereParams.length + 1);
@@ -13915,6 +14197,8 @@ function createQueryBuilder(state) {
13915
14197
  },
13916
14198
  orWhere(expr, op, value) {
13917
14199
  if (typeof expr === "string" && op !== undefined) {
14200
+ validateIdentifier(String(expr), "orWhere(column)");
14201
+ assertSafeWhereOperator(op, "orWhere(operator)");
13918
14202
  const paramIndex = whereParams.length + 1;
13919
14203
  whereConditions.push(`OR ${String(expr)} ${String(op)} ${getPlaceholder(paramIndex)}`);
13920
14204
  whereParams.push(value);
@@ -13925,7 +14209,8 @@ function createQueryBuilder(state) {
13925
14209
  if (Array.isArray(expr)) {
13926
14210
  const [col, op2, val] = expr;
13927
14211
  const colName = String(col);
13928
- const operator = String(op2);
14212
+ validateIdentifier(colName, "orWhere(column)");
14213
+ const operator = assertSafeWhereOperator(op2, "orWhere(operator)");
13929
14214
  if (operator === "in" || operator === "not in") {
13930
14215
  const values = Array.isArray(val) ? val : [val];
13931
14216
  const placeholders = getPlaceholders(values.length, whereParams.length + 1);
@@ -14023,7 +14308,11 @@ function createQueryBuilder(state) {
14023
14308
  return this;
14024
14309
  },
14025
14310
  join(table2, onLeft, operator, onRight) {
14026
- text = `${text} JOIN ${table2} ON ${onLeft} ${operator} ${onRight}`;
14311
+ validateIdentifier(table2, "join(table)");
14312
+ validateQualifiedIdentifier(onLeft, "join(onLeft)");
14313
+ validateQualifiedIdentifier(onRight, "join(onRight)");
14314
+ assertSafeWhereOperator(operator, "join(operator)");
14315
+ insertJoin(`JOIN ${table2} ON ${onLeft} ${operator} ${onRight}`);
14027
14316
  joinedTables.add(table2);
14028
14317
  return this;
14029
14318
  },
@@ -14032,44 +14321,55 @@ function createQueryBuilder(state) {
14032
14321
  validateQualifiedIdentifier(onLeft, "joinSub(onLeft)");
14033
14322
  validateQualifiedIdentifier(onRight, "joinSub(onRight)");
14034
14323
  assertSafeWhereOperator(operator, "joinSub(operator)");
14035
- text += ` JOIN (${String(sub.toSQL())}) AS ${alias} ON ${onLeft} ${operator} ${onRight}`;
14036
- built = null;
14324
+ insertJoin(`JOIN (${String(sub.toSQL())}) AS ${alias} ON ${onLeft} ${operator} ${onRight}`);
14037
14325
  joinedTables.add(alias);
14038
14326
  return this;
14039
14327
  },
14040
14328
  innerJoin(table2, onLeft, operator, onRight) {
14041
- text = `${text} INNER JOIN ${table2} ON ${onLeft} ${operator} ${onRight}`;
14042
- built = null;
14329
+ validateIdentifier(table2, "innerJoin(table)");
14330
+ validateQualifiedIdentifier(onLeft, "innerJoin(onLeft)");
14331
+ validateQualifiedIdentifier(onRight, "innerJoin(onRight)");
14332
+ assertSafeWhereOperator(operator, "innerJoin(operator)");
14333
+ insertJoin(`INNER JOIN ${table2} ON ${onLeft} ${operator} ${onRight}`);
14043
14334
  joinedTables.add(table2);
14044
14335
  return this;
14045
14336
  },
14046
14337
  leftJoin(table2, onLeft, operator, onRight) {
14047
- text = `${text} LEFT JOIN ${table2} ON ${onLeft} ${operator} ${onRight}`;
14048
- built = null;
14338
+ validateIdentifier(table2, "leftJoin(table)");
14339
+ validateQualifiedIdentifier(onLeft, "leftJoin(onLeft)");
14340
+ validateQualifiedIdentifier(onRight, "leftJoin(onRight)");
14341
+ assertSafeWhereOperator(operator, "leftJoin(operator)");
14342
+ insertJoin(`LEFT JOIN ${table2} ON ${onLeft} ${operator} ${onRight}`);
14049
14343
  joinedTables.add(table2);
14050
14344
  return this;
14051
14345
  },
14052
14346
  leftJoinSub(sub, alias, onLeft, operator, onRight) {
14053
- text += ` LEFT JOIN (${String(sub.toSQL())}) AS ${alias} ON ${onLeft} ${operator} ${onRight}`;
14054
- built = null;
14347
+ validateIdentifier(alias, "leftJoinSub(alias)");
14348
+ validateQualifiedIdentifier(onLeft, "leftJoinSub(onLeft)");
14349
+ validateQualifiedIdentifier(onRight, "leftJoinSub(onRight)");
14350
+ assertSafeWhereOperator(operator, "leftJoinSub(operator)");
14351
+ insertJoin(`LEFT JOIN (${String(sub.toSQL())}) AS ${alias} ON ${onLeft} ${operator} ${onRight}`);
14055
14352
  joinedTables.add(alias);
14056
14353
  return this;
14057
14354
  },
14058
14355
  rightJoin(table2, onLeft, operator, onRight) {
14059
- text = `${text} RIGHT JOIN ${table2} ON ${onLeft} ${operator} ${onRight}`;
14060
- built = null;
14356
+ validateIdentifier(table2, "rightJoin(table)");
14357
+ validateQualifiedIdentifier(onLeft, "rightJoin(onLeft)");
14358
+ validateQualifiedIdentifier(onRight, "rightJoin(onRight)");
14359
+ assertSafeWhereOperator(operator, "rightJoin(operator)");
14360
+ insertJoin(`RIGHT JOIN ${table2} ON ${onLeft} ${operator} ${onRight}`);
14061
14361
  joinedTables.add(table2);
14062
14362
  return this;
14063
14363
  },
14064
14364
  crossJoin(table2) {
14065
- text = `${text} CROSS JOIN ${table2}`;
14066
- built = null;
14365
+ validateIdentifier(table2, "crossJoin(table)");
14366
+ insertJoin(`CROSS JOIN ${table2}`);
14067
14367
  joinedTables.add(table2);
14068
14368
  return this;
14069
14369
  },
14070
14370
  crossJoinSub(sub, alias) {
14071
- text += ` CROSS JOIN (${String(sub.toSQL())}) AS ${alias}`;
14072
- built = null;
14371
+ validateIdentifier(alias, "crossJoinSub(alias)");
14372
+ insertJoin(`CROSS JOIN (${String(sub.toSQL())}) AS ${alias}`);
14073
14373
  joinedTables.add(alias);
14074
14374
  return this;
14075
14375
  },
@@ -14122,9 +14422,10 @@ function createQueryBuilder(state) {
14122
14422
  return this;
14123
14423
  },
14124
14424
  having(expr) {
14425
+ const kw = /\bHAVING\b/i.test(text) ? "AND" : "HAVING";
14125
14426
  if (Array.isArray(expr)) {
14126
14427
  const paramIdx = whereParams.length + 1;
14127
- text = `${text} HAVING ${expr[0]} ${expr[1]} ${getPlaceholder(paramIdx)}`;
14428
+ text = `${text} ${kw} ${expr[0]} ${expr[1]} ${getPlaceholder(paramIdx)}`;
14128
14429
  whereParams.push(expr[2]);
14129
14430
  built = null;
14130
14431
  } else if (expr && typeof expr === "object" && !("raw" in expr)) {
@@ -14138,18 +14439,19 @@ function createQueryBuilder(state) {
14138
14439
  conditions[i] = `${key} = ${getPlaceholder(baseIdx + i + 1)}`;
14139
14440
  whereParams.push(expr[key]);
14140
14441
  }
14141
- text = `${text} HAVING ${conditions.join(" AND ")}`;
14442
+ text = `${text} ${kw} ${conditions.join(" AND ")}`;
14142
14443
  built = null;
14143
14444
  }
14144
14445
  } else if (expr && typeof expr.raw !== "undefined") {
14145
- text += ` HAVING ${expr.raw}`;
14446
+ text += ` ${kw} ${expr.raw}`;
14146
14447
  built = null;
14147
14448
  }
14148
14449
  return this;
14149
14450
  },
14150
14451
  havingRaw(fragment) {
14151
14452
  assertSqlFragment(fragment, "havingRaw(fragment)");
14152
- text += ` HAVING ${String(fragment)}`;
14453
+ const kw = /\bHAVING\b/i.test(text) ? "AND" : "HAVING";
14454
+ text += ` ${kw} ${String(fragment)}`;
14153
14455
  built = null;
14154
14456
  return this;
14155
14457
  },
@@ -14160,13 +14462,27 @@ function createQueryBuilder(state) {
14160
14462
  return this;
14161
14463
  },
14162
14464
  union(other) {
14163
- text += ` UNION ${String(other.toSQL())}`;
14164
- built = null;
14465
+ appendSetOp("UNION", other);
14165
14466
  return this;
14166
14467
  },
14167
14468
  unionAll(other) {
14168
- text += ` UNION ALL ${String(other.toSQL())}`;
14169
- built = null;
14469
+ appendSetOp("UNION ALL", other);
14470
+ return this;
14471
+ },
14472
+ intersect(other) {
14473
+ appendSetOp("INTERSECT", other);
14474
+ return this;
14475
+ },
14476
+ intersectAll(other) {
14477
+ appendSetOp("INTERSECT ALL", other);
14478
+ return this;
14479
+ },
14480
+ except(other) {
14481
+ appendSetOp("EXCEPT", other);
14482
+ return this;
14483
+ },
14484
+ exceptAll(other) {
14485
+ appendSetOp("EXCEPT ALL", other);
14170
14486
  return this;
14171
14487
  },
14172
14488
  forPage(page, perPage) {
@@ -14210,11 +14526,22 @@ function createQueryBuilder(state) {
14210
14526
  const e = await this.exists();
14211
14527
  return !e;
14212
14528
  },
14213
- async paginate(perPage, page = 1) {
14529
+ async paginate(perPage, page = 1, opts = {}) {
14214
14530
  if (!Number.isFinite(perPage) || perPage <= 0 || !Number.isInteger(perPage))
14215
14531
  throw new TypeError(`[query-builder] paginate(perPage): expected positive integer, got ${perPage}`);
14216
14532
  if (!Number.isFinite(page) || page < 1 || !Number.isInteger(page))
14217
14533
  throw new TypeError(`[query-builder] paginate(page): expected integer >= 1, got ${page}`);
14534
+ if (opts.tx) {
14535
+ const baseSql = reorderSelectClauses(text);
14536
+ const baseParams = [...whereParams];
14537
+ const cRows2 = await opts.tx.unsafe(`SELECT COUNT(*) as c FROM (${baseSql}) as sub`, baseParams);
14538
+ const total2 = Number(cRows2?.[0]?.c ?? 0);
14539
+ const lastPage2 = Math.max(1, Math.ceil(total2 / perPage));
14540
+ const p2 = Math.max(1, Math.min(page, lastPage2));
14541
+ const offset2 = (p2 - 1) * perPage;
14542
+ const data2 = await opts.tx.unsafe(`${baseSql} LIMIT ${perPage} OFFSET ${offset2}`, baseParams);
14543
+ return { data: data2, meta: { perPage, page: p2, total: total2, lastPage: lastPage2 } };
14544
+ }
14218
14545
  const countQ = sql`SELECT COUNT(*) as c FROM (${ensureBuilt()}) as sub`;
14219
14546
  const cRows = await runWithHooks(countQ, "select", { signal: abortSignal, timeoutMs });
14220
14547
  const [cRow] = cRows;
@@ -14371,7 +14698,7 @@ function createQueryBuilder(state) {
14371
14698
  },
14372
14699
  async get() {
14373
14700
  const hooks = config5.hooks;
14374
- const hasQueryHooks = hooks && (hooks.onQueryStart || hooks.onQueryEnd || hooks.onQueryError || hooks.startSpan);
14701
+ const hasQueryHooks = hooks && (hooks.onQueryStart || hooks.onQueryEnd || hooks.onQueryError || hooks.startSpan || hasSlowQueryHook(hooks));
14375
14702
  if (!config5.softDeletes?.enabled && !useCache && !timeoutMs && !abortSignal && !hasQueryHooks) {
14376
14703
  const prepareFn = _sql._prepareStatement;
14377
14704
  if (prepareFn) {
@@ -14426,7 +14753,7 @@ function createQueryBuilder(state) {
14426
14753
  },
14427
14754
  async first() {
14428
14755
  const fHooks = config5.hooks;
14429
- const fHasQueryHooks = fHooks && (fHooks.onQueryStart || fHooks.onQueryEnd || fHooks.onQueryError || fHooks.startSpan);
14756
+ const fHasQueryHooks = fHooks && (fHooks.onQueryStart || fHooks.onQueryEnd || fHooks.onQueryError || fHooks.startSpan || hasSlowQueryHook(fHooks));
14430
14757
  if (!config5.softDeletes?.enabled && !useCache && !timeoutMs && !abortSignal && !fHasQueryHooks) {
14431
14758
  const prepareFn = _sql._prepareStatement;
14432
14759
  if (prepareFn) {
@@ -14508,7 +14835,7 @@ function createQueryBuilder(state) {
14508
14835
  countText = `SELECT COUNT(*) as c FROM ${table}`;
14509
14836
  }
14510
14837
  const cHooks = config5.hooks;
14511
- const cHasHooks = cHooks && (cHooks.onQueryStart || cHooks.onQueryEnd || cHooks.onQueryError || cHooks.startSpan);
14838
+ const cHasHooks = cHooks && (cHooks.onQueryStart || cHooks.onQueryEnd || cHooks.onQueryError || cHooks.startSpan || hasSlowQueryHook(cHooks));
14512
14839
  if (!config5.softDeletes?.enabled && !useCache && !timeoutMs && !abortSignal && !cHasHooks) {
14513
14840
  const prepareFn = _sql._prepareStatement;
14514
14841
  if (prepareFn) {
@@ -14526,7 +14853,7 @@ function createQueryBuilder(state) {
14526
14853
  const fromIdx = text.indexOf(" FROM ");
14527
14854
  const avgText = fromIdx !== -1 ? `SELECT AVG(${column}) as a${text.substring(fromIdx)}` : `SELECT AVG(${column}) as a FROM ${table}`;
14528
14855
  const aHooks = config5.hooks;
14529
- const aHasHooks = aHooks && (aHooks.onQueryStart || aHooks.onQueryEnd || aHooks.onQueryError || aHooks.startSpan);
14856
+ const aHasHooks = aHooks && (aHooks.onQueryStart || aHooks.onQueryEnd || aHooks.onQueryError || aHooks.startSpan || hasSlowQueryHook(aHooks));
14530
14857
  if (!config5.softDeletes?.enabled && !useCache && !timeoutMs && !abortSignal && !aHasHooks) {
14531
14858
  const prepareFn = _sql._prepareStatement;
14532
14859
  if (prepareFn) {
@@ -14596,6 +14923,9 @@ function createQueryBuilder(state) {
14596
14923
  toParams() {
14597
14924
  return ensureBuilt().values?.() ?? [];
14598
14925
  },
14926
+ __rawState() {
14927
+ return { sql: reorderSelectClauses(text), params: [...whereParams] };
14928
+ },
14599
14929
  raw() {
14600
14930
  return ensureBuilt().raw();
14601
14931
  },
@@ -14954,15 +15284,15 @@ function createQueryBuilder(state) {
14954
15284
  params.length = totalParams;
14955
15285
  if (rowCount === 1) {
14956
15286
  if (!isPostgres) {
14957
- let cols = keys[0];
15287
+ let cols = quoteId(keys[0]);
14958
15288
  let placeholders = "?";
14959
15289
  params[0] = firstRow[keys[0]];
14960
15290
  for (let c = 1;c < colCount; c++) {
14961
- cols += `,${keys[c]}`;
15291
+ cols += `,${quoteId(keys[c])}`;
14962
15292
  placeholders += ",?";
14963
15293
  params[c] = firstRow[keys[c]];
14964
15294
  }
14965
- sqlText = `INSERT INTO ${table}(${cols})VALUES(${placeholders})`;
15295
+ sqlText = `INSERT INTO ${quoteId(table)}(${cols})VALUES(${placeholders})`;
14966
15296
  } else {
14967
15297
  const columnList = keys.map((k) => quoteId(k)).join(",");
14968
15298
  sqlText = `INSERT INTO ${quoteId(table)}(${columnList})VALUES(`;
@@ -15020,7 +15350,7 @@ function createQueryBuilder(state) {
15020
15350
  },
15021
15351
  execute() {
15022
15352
  const hooks = config5.hooks;
15023
- const hasHooks = hooks && (hooks.onQueryStart || hooks.onQueryEnd || hooks.onQueryError || hooks.startSpan || hooks.beforeCreate || hooks.afterCreate);
15353
+ const hasHooks = hooks && (hooks.onQueryStart || hooks.onQueryEnd || hooks.onQueryError || hooks.startSpan || hooks.beforeCreate || hooks.afterCreate || hasSlowQueryHook(hooks));
15024
15354
  if (!hasHooks) {
15025
15355
  const prepareFn = _sql._prepareStatement;
15026
15356
  if (prepareFn) {
@@ -15494,55 +15824,72 @@ function createQueryBuilder(state) {
15494
15824
  };
15495
15825
  },
15496
15826
  async insertOrIgnore(table, values) {
15497
- if (config5.dialect === "mysql") {
15498
- const built2 = bunSql`INSERT IGNORE INTO ${bunSql(String(table))} ${bunSql(values)}`;
15499
- return built2.execute();
15500
- }
15501
- const built = bunSql`INSERT INTO ${bunSql(String(table))} ${bunSql(values)} ON CONFLICT DO NOTHING`;
15502
- return built.execute();
15827
+ const rows = Array.isArray(values) ? values : [values];
15828
+ if (!rows.length)
15829
+ return;
15830
+ const { colsSql, valuesSql, params } = buildInsertClause(rows);
15831
+ const tbl = quoteInsertIdent(String(table));
15832
+ const sqlText = config5.dialect === "mysql" ? `INSERT IGNORE INTO ${tbl} (${colsSql}) VALUES ${valuesSql}` : `INSERT INTO ${tbl} (${colsSql}) VALUES ${valuesSql} ON CONFLICT DO NOTHING`;
15833
+ return bunSql.unsafe(sqlText, params).execute();
15503
15834
  },
15504
15835
  async insertGetId(table, values, idColumn = "id") {
15836
+ const { colsSql, valuesSql, params } = buildInsertClause([values]);
15837
+ const tbl = quoteInsertIdent(String(table));
15505
15838
  if (config5.dialect === "mysql") {
15506
- const insertQuery = bunSql`INSERT INTO ${bunSql(String(table))} ${bunSql(values)}`;
15507
- const result = await insertQuery.execute();
15508
- if (result && typeof result === "object" && "insertId" in result) {
15509
- return result.insertId;
15510
- }
15511
- const [lastIdResult] = await bunSql`SELECT LAST_INSERT_ID() as id`.execute();
15512
- return lastIdResult?.id;
15513
- } else {
15514
- const q = bunSql`INSERT INTO ${bunSql(String(table))} ${bunSql(values)} RETURNING ${bunSql(String(idColumn))} as id`;
15515
- const [row] = await q.execute();
15516
- return row?.id;
15517
- }
15839
+ await bunSql.unsafe(`INSERT INTO ${tbl} (${colsSql}) VALUES ${valuesSql}`, params).execute();
15840
+ const [row2] = await bunSql.unsafe(`SELECT LAST_INSERT_ID() as id`).execute();
15841
+ return row2?.id;
15842
+ }
15843
+ if (config5.dialect === "sqlite") {
15844
+ const res = await bunSql.unsafe(`INSERT INTO ${tbl} (${colsSql}) VALUES ${valuesSql}`, params).execute();
15845
+ if (res?.lastInsertRowid != null)
15846
+ return res.lastInsertRowid;
15847
+ const [row2] = await bunSql.unsafe(`SELECT last_insert_rowid() as id`).execute();
15848
+ return row2?.id;
15849
+ }
15850
+ const [row] = await bunSql.unsafe(`INSERT INTO ${tbl} (${colsSql}) VALUES ${valuesSql} RETURNING ${quoteInsertIdent(String(idColumn))} as id`, params).execute();
15851
+ return row?.id;
15518
15852
  },
15519
15853
  async updateOrInsert(table, match, values) {
15520
- const whereParts = Object.keys(match).map((k) => bunSql`${bunSql(String(k))} = ${bunSql(match[k])}`);
15521
- const existsQ = bunSql`SELECT 1 FROM ${bunSql(String(table))} WHERE ${bunSql(whereParts)} LIMIT 1`;
15522
- const existsRows = await existsQ.execute();
15854
+ const tbl = quoteInsertIdent(String(table));
15855
+ const matchKeys = Object.keys(match);
15856
+ let idx = 1;
15857
+ const whereSql = matchKeys.map((k) => `${quoteInsertIdent(k)} = ${getPlaceholder(idx++)}`).join(" AND ");
15858
+ const whereParams = matchKeys.map((k) => match[k]);
15859
+ const existsRows = await bunSql.unsafe(`SELECT 1 FROM ${tbl} WHERE ${whereSql} LIMIT 1`, whereParams).execute();
15523
15860
  if (existsRows.length) {
15524
- const upd = bunSql`UPDATE ${bunSql(String(table))} SET ${bunSql(values)} WHERE ${bunSql(whereParts)}`;
15525
- await upd.execute();
15526
- return true;
15527
- } else {
15528
- const ins = bunSql`INSERT INTO ${bunSql(String(table))} ${bunSql({ ...match, ...values })}`;
15529
- await ins.execute();
15861
+ const setKeys = Object.keys(values);
15862
+ let i = 1;
15863
+ const setSql = setKeys.map((k) => `${quoteInsertIdent(k)} = ${getPlaceholder(i++)}`).join(", ");
15864
+ const whereSql2 = matchKeys.map((k) => `${quoteInsertIdent(k)} = ${getPlaceholder(i++)}`).join(" AND ");
15865
+ const params2 = [...setKeys.map((k) => values[k]), ...matchKeys.map((k) => match[k])];
15866
+ await bunSql.unsafe(`UPDATE ${tbl} SET ${setSql} WHERE ${whereSql2}`, params2).execute();
15530
15867
  return true;
15531
15868
  }
15869
+ const { colsSql, valuesSql, params } = buildInsertClause([{ ...match, ...values }]);
15870
+ await bunSql.unsafe(`INSERT INTO ${tbl} (${colsSql}) VALUES ${valuesSql}`, params).execute();
15871
+ return true;
15532
15872
  },
15533
15873
  async upsert(table, rows, conflictColumns, mergeColumns) {
15534
15874
  const targetCols = conflictColumns.map((c) => String(c));
15535
15875
  const setCols = (mergeColumns ?? []).map((c) => String(c));
15876
+ const list = rows;
15877
+ if (!list.length)
15878
+ return;
15879
+ const { colsSql, valuesSql, params } = buildInsertClause(list);
15880
+ const tbl = quoteInsertIdent(String(table));
15881
+ const insert = `INSERT INTO ${tbl} (${colsSql}) VALUES ${valuesSql}`;
15536
15882
  if (config5.dialect === "mysql") {
15537
- const updateList2 = setCols.map((c) => `\`${c.replace(/`/g, "``")}\` = VALUES(\`${c.replace(/`/g, "``")}\`)`).join(", ");
15538
- const built2 = bunSql`INSERT INTO ${bunSql(String(table))} ${bunSql(rows)} ON DUPLICATE KEY UPDATE ${bunSql.unsafe(updateList2)}`;
15539
- return built2.execute();
15540
- }
15541
- const isPostgres = config5.dialect === "postgres";
15542
- const quoteCol = (column) => isPostgres ? `"${column.replace(/"/g, '""')}"` : `"${column.replace(/"/g, '""')}"`;
15543
- const updateList = setCols.map((column) => `${quoteCol(column)} = EXCLUDED.${quoteCol(column)}`).join(", ");
15544
- const built = bunSql`INSERT INTO ${bunSql(String(table))} ${bunSql(rows)} ON CONFLICT (${bunSql(targetCols)}) DO UPDATE SET ${bunSql.unsafe(updateList)}`;
15545
- return built.execute();
15883
+ if (setCols.length === 0)
15884
+ return bunSql.unsafe(`INSERT IGNORE INTO ${tbl} (${colsSql}) VALUES ${valuesSql}`, params).execute();
15885
+ const updateList2 = setCols.map((c) => `${quoteInsertIdent(c)} = VALUES(${quoteInsertIdent(c)})`).join(", ");
15886
+ return bunSql.unsafe(`${insert} ON DUPLICATE KEY UPDATE ${updateList2}`, params).execute();
15887
+ }
15888
+ const targets = targetCols.map(quoteInsertIdent).join(", ");
15889
+ if (setCols.length === 0)
15890
+ return bunSql.unsafe(`${insert} ON CONFLICT (${targets}) DO NOTHING`, params).execute();
15891
+ const updateList = setCols.map((c) => `${quoteInsertIdent(c)} = EXCLUDED.${quoteInsertIdent(c)}`).join(", ");
15892
+ return bunSql.unsafe(`${insert} ON CONFLICT (${targets}) DO UPDATE SET ${updateList}`, params).execute();
15546
15893
  },
15547
15894
  async save(table, values) {
15548
15895
  const pk = meta?.primaryKeys[String(table)] ?? "id";
@@ -15645,7 +15992,8 @@ function createQueryBuilder(state) {
15645
15992
  const colCount = keys.length;
15646
15993
  const rowCount = rows.length;
15647
15994
  const params = Array.from({ length: rowCount * colCount });
15648
- let sql = `INSERT INTO ${table}(${keys.join(",")})VALUES`;
15995
+ const quoteId = config5.dialect === "mysql" ? (id) => `\`${String(id).replace(/`/g, "``")}\`` : (id) => `"${String(id).replace(/"/g, '""')}"`;
15996
+ let sql = `INSERT INTO ${quoteId(String(table))}(${keys.map(quoteId).join(",")})VALUES`;
15649
15997
  let pidx = 0;
15650
15998
  for (let r = 0;r < rowCount; r++) {
15651
15999
  if (r > 0)
@@ -16865,6 +17213,126 @@ var init_introspect = __esm(() => {
16865
17213
  init_src2();
16866
17214
  });
16867
17215
 
17216
+ // src/actions/introspect-db.ts
17217
+ function sqlTypeToAttr(sqlType) {
17218
+ const t = sqlType.toLowerCase();
17219
+ if (/^bool|boolean|tinyint\(1\)/.test(t))
17220
+ return "boolean";
17221
+ if (/int|serial|numeric|decimal|double|real|float|money/.test(t))
17222
+ return "number";
17223
+ if (/timestamp|datetime|date|time/.test(t))
17224
+ return "datetime";
17225
+ if (/json/.test(t))
17226
+ return "json";
17227
+ return "string";
17228
+ }
17229
+ function singularize(name) {
17230
+ if (/ies$/i.test(name))
17231
+ return name.replace(/ies$/i, "y");
17232
+ if (/ses$/i.test(name))
17233
+ return name.replace(/es$/i, "");
17234
+ if (/s$/i.test(name) && !/ss$/i.test(name))
17235
+ return name.replace(/s$/i, "");
17236
+ return name;
17237
+ }
17238
+ function pascalCase(name) {
17239
+ return name.replace(/[-_\s]+/g, " ").split(" ").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
17240
+ }
17241
+ function modelNameForTable(table) {
17242
+ return pascalCase(singularize(table));
17243
+ }
17244
+ async function listTables(qb, dialect) {
17245
+ if (dialect === "postgres") {
17246
+ const rows2 = await qb.unsafe(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ORDER BY table_name`);
17247
+ return rows2.map((r) => r.table_name);
17248
+ }
17249
+ if (dialect === "mysql") {
17250
+ const rows2 = await qb.unsafe(`SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE' ORDER BY table_name`);
17251
+ return rows2.map((r) => r.table_name ?? r.TABLE_NAME);
17252
+ }
17253
+ const rows = await qb.unsafe(`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
17254
+ return rows.map((r) => r.name);
17255
+ }
17256
+ async function readColumns(qb, dialect, table) {
17257
+ if (dialect === "sqlite") {
17258
+ const rows2 = await qb.unsafe(`PRAGMA table_info(${table})`);
17259
+ return rows2.map((r) => ({
17260
+ name: r.name,
17261
+ sqlType: String(r.type || ""),
17262
+ nullable: Number(r.notnull) === 0,
17263
+ isPrimaryKey: Number(r.pk) > 0
17264
+ }));
17265
+ }
17266
+ if (dialect === "postgres") {
17267
+ const rows2 = await qb.unsafe(`SELECT c.column_name, c.data_type, c.is_nullable,
17268
+ (SELECT COUNT(*) FROM information_schema.table_constraints tc
17269
+ JOIN information_schema.key_column_usage k ON k.constraint_name = tc.constraint_name
17270
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_name = c.table_name AND k.column_name = c.column_name) AS is_pk
17271
+ FROM information_schema.columns c
17272
+ WHERE c.table_name = $1 AND c.table_schema = 'public'
17273
+ ORDER BY c.ordinal_position`, [table]);
17274
+ return rows2.map((r) => ({
17275
+ name: r.column_name,
17276
+ sqlType: String(r.data_type || ""),
17277
+ nullable: String(r.is_nullable).toUpperCase() === "YES",
17278
+ isPrimaryKey: Number(r.is_pk) > 0
17279
+ }));
17280
+ }
17281
+ const rows = await qb.unsafe(`SELECT column_name, data_type, is_nullable, column_key
17282
+ FROM information_schema.columns
17283
+ WHERE table_name = ? AND table_schema = DATABASE()
17284
+ ORDER BY ordinal_position`, [table]);
17285
+ return rows.map((r) => ({
17286
+ name: r.column_name ?? r.COLUMN_NAME,
17287
+ sqlType: String(r.data_type ?? r.DATA_TYPE ?? ""),
17288
+ nullable: String(r.is_nullable ?? r.IS_NULLABLE).toUpperCase() === "YES",
17289
+ isPrimaryKey: String(r.column_key ?? r.COLUMN_KEY).toUpperCase() === "PRI"
17290
+ }));
17291
+ }
17292
+ function generateModelSource(table, columns) {
17293
+ const modelName = modelNameForTable(table);
17294
+ const pk = columns.find((c) => c.isPrimaryKey)?.name ?? "id";
17295
+ const attrLines = columns.map((c) => {
17296
+ const parts = [`type: '${sqlTypeToAttr(c.sqlType)}'`];
17297
+ if (!c.nullable && !c.isPrimaryKey)
17298
+ parts.push("required: true");
17299
+ return ` ${c.name}: { ${parts.join(", ")} },`;
17300
+ }).join(`
17301
+ `);
17302
+ return `export const ${modelName} = defineModel({
17303
+ name: '${modelName}',
17304
+ table: '${table}',
17305
+ primaryKey: '${pk}',
17306
+ attributes: {
17307
+ ${attrLines}
17308
+ },
17309
+ })
17310
+ `;
17311
+ }
17312
+ async function introspectDatabase(opts = {}) {
17313
+ const dialect = config5.dialect || "postgres";
17314
+ const qb = createQueryBuilder();
17315
+ const tables = opts.tables?.length ? opts.tables : await listTables(qb, dialect);
17316
+ const out = [];
17317
+ for (const table of tables) {
17318
+ const columns = await readColumns(qb, dialect, table);
17319
+ if (!columns.length)
17320
+ continue;
17321
+ out.push({
17322
+ table,
17323
+ modelName: modelNameForTable(table),
17324
+ primaryKey: columns.find((c) => c.isPrimaryKey)?.name ?? "id",
17325
+ columns,
17326
+ source: generateModelSource(table, columns)
17327
+ });
17328
+ }
17329
+ return out;
17330
+ }
17331
+ var init_introspect_db = __esm(() => {
17332
+ init_config();
17333
+ init_src2();
17334
+ });
17335
+
16868
17336
  // src/actions/make-model.ts
16869
17337
  import { existsSync as existsSync14, mkdirSync as mkdirSync5, writeFileSync as writeFileSync10 } from "fs";
16870
17338
  import { dirname as dirname5, join as join7 } from "path";
@@ -17807,9 +18275,51 @@ var init_migrate_generate = __esm(() => {
17807
18275
  });
17808
18276
 
17809
18277
  // src/actions/migrate-rollback.ts
17810
- import { existsSync as existsSync17, unlinkSync as unlinkSync2 } from "fs";
18278
+ import { existsSync as existsSync17, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
17811
18279
  import { dirname as dirname7, join as join10 } from "path";
17812
18280
  import process26 from "process";
18281
+ function splitSqlStatements2(sql) {
18282
+ const out = [];
18283
+ let buf = "";
18284
+ let inString = false;
18285
+ const lines = sql.split(`
18286
+ `).filter((l) => !/^\s*--/.test(l));
18287
+ const text = lines.join(`
18288
+ `);
18289
+ for (let i = 0;i < text.length; i++) {
18290
+ const ch = text[i];
18291
+ if (ch === "'")
18292
+ inString = !inString;
18293
+ if (ch === ";" && !inString) {
18294
+ if (buf.trim())
18295
+ out.push(buf.trim());
18296
+ buf = "";
18297
+ } else {
18298
+ buf += ch;
18299
+ }
18300
+ }
18301
+ if (buf.trim())
18302
+ out.push(buf.trim());
18303
+ return out;
18304
+ }
18305
+ function deriveDownStatements(forwardSql, dialect = config5.dialect) {
18306
+ const q = (id) => dialect === "mysql" ? `\`${id}\`` : `"${id}"`;
18307
+ const down = [];
18308
+ const skipped = [];
18309
+ for (const stmt of splitSqlStatements2(forwardSql)) {
18310
+ let m;
18311
+ if (m = /^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`']?(\w+)["`']?/i.exec(stmt)) {
18312
+ down.push(`DROP TABLE IF EXISTS ${q(m[1])}`);
18313
+ } else if (m = /^ALTER\s+TABLE\s+["`']?(\w+)["`']?\s+ADD\s+(?:COLUMN\s+)?["`']?(\w+)["`']?/i.exec(stmt)) {
18314
+ down.push(`ALTER TABLE ${q(m[1])} DROP COLUMN ${q(m[2])}`);
18315
+ } else if (m = /^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`']?(\w+)["`']?(?:\s+ON\s+["`']?(\w+)["`']?)?/i.exec(stmt)) {
18316
+ down.push(dialect === "mysql" && m[2] ? `DROP INDEX ${q(m[1])} ON ${q(m[2])}` : `DROP INDEX IF EXISTS ${q(m[1])}`);
18317
+ } else {
18318
+ skipped.push(stmt);
18319
+ }
18320
+ }
18321
+ return { down: down.reverse(), skipped };
18322
+ }
17813
18323
  function findWorkspaceRoot2(startPath) {
17814
18324
  let currentPath = startPath;
17815
18325
  while (currentPath !== dirname7(currentPath)) {
@@ -17828,6 +18338,7 @@ function getSqlDirectory2(workspaceRoot) {
17828
18338
  }
17829
18339
  async function migrateRollback(options = {}) {
17830
18340
  const steps = options.steps || 1;
18341
+ const reverseSchema = options.reverseSchema !== false;
17831
18342
  console.log("-- Rolling back migrations");
17832
18343
  console.log(`-- Steps: ${steps}`);
17833
18344
  console.log();
@@ -17857,13 +18368,25 @@ async function migrateRollback(options = {}) {
17857
18368
  console.log(` - ${migration.migration}`);
17858
18369
  }
17859
18370
  console.log();
18371
+ const sqlDir = getSqlDirectory2();
18372
+ let reversedAny = false;
17860
18373
  for (const migration of migrationsToRollback) {
17861
18374
  try {
18375
+ const filePath = join10(sqlDir, migration.migration);
18376
+ if (reverseSchema && existsSync17(filePath)) {
18377
+ const forwardSql = readFileSync4(filePath, "utf8");
18378
+ const { down, skipped } = deriveDownStatements(forwardSql);
18379
+ for (const stmt of down) {
18380
+ await qb.unsafe(stmt);
18381
+ console.log(`-- \u21A9 ${stmt}`);
18382
+ reversedAny = true;
18383
+ }
18384
+ if (skipped.length > 0)
18385
+ console.log(`-- \u26A0\uFE0F ${skipped.length} statement(s) in ${migration.migration} could not be auto-reversed (data/complex DDL) \u2014 reverse manually.`);
18386
+ }
17862
18387
  const deleteSql = `DELETE FROM migrations WHERE migration = $1`;
17863
18388
  await qb.unsafe(deleteSql, [migration.migration]);
17864
18389
  console.log(`-- \u2713 Removed migration record: ${migration.migration}`);
17865
- const sqlDir = getSqlDirectory2();
17866
- const filePath = join10(sqlDir, migration.migration);
17867
18390
  if (existsSync17(filePath)) {
17868
18391
  unlinkSync2(filePath);
17869
18392
  console.log(`-- \uD83D\uDDD1\uFE0F Deleted migration file: ${migration.migration}`);
@@ -17874,17 +18397,18 @@ async function migrateRollback(options = {}) {
17874
18397
  }
17875
18398
  }
17876
18399
  console.log();
17877
- console.log("-- \u26A0\uFE0F Important: Rollback only removes migration records.");
17878
- console.log("-- To reverse schema changes:");
17879
- console.log("-- 1. Revert your model changes");
17880
- console.log("-- 2. Run `qb migrate:fresh` to rebuild the database");
17881
- console.log("-- or manually write and execute reverse SQL");
18400
+ if (reverseSchema) {
18401
+ console.log(reversedAny ? "-- \u2713 Reverse DDL executed for the rolled-back migration(s)." : "-- \u26A0\uFE0F No reversible DDL found; only migration records were removed.");
18402
+ } else {
18403
+ console.log("-- \u26A0\uFE0F reverseSchema disabled: only migration records were removed.");
18404
+ }
17882
18405
  } catch (err) {
17883
18406
  console.error("-- Rollback failed:", err);
17884
18407
  throw err;
17885
18408
  }
17886
18409
  }
17887
18410
  var init_migrate_rollback = __esm(() => {
18411
+ init_config();
17888
18412
  init_src2();
17889
18413
  });
17890
18414
 
@@ -18119,7 +18643,7 @@ var init_ping = __esm(() => {
18119
18643
  });
18120
18644
 
18121
18645
  // src/actions/query-explain-all.ts
18122
- import { readdirSync as readdirSync10, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
18646
+ import { readdirSync as readdirSync10, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
18123
18647
  import { extname as extname3, join as join13 } from "path";
18124
18648
  async function queryExplainAll(path, options = {}) {
18125
18649
  const results = [];
@@ -18145,7 +18669,7 @@ async function queryExplainAll(path, options = {}) {
18145
18669
  }
18146
18670
  for (const file2 of files) {
18147
18671
  try {
18148
- const query = readFileSync4(file2, "utf8").trim();
18672
+ const query = readFileSync5(file2, "utf8").trim();
18149
18673
  if (!query) {
18150
18674
  if (options.verbose) {
18151
18675
  console.log(`\u2298 ${file2}: empty file`);
@@ -18182,7 +18706,7 @@ async function queryExplainAll(path, options = {}) {
18182
18706
  } catch (error) {
18183
18707
  results.push({
18184
18708
  file: file2,
18185
- query: readFileSync4(file2, "utf8").trim(),
18709
+ query: readFileSync5(file2, "utf8").trim(),
18186
18710
  plan: [],
18187
18711
  error: error.message
18188
18712
  });
@@ -18796,6 +19320,7 @@ var init_actions = __esm(() => {
18796
19320
  init_file();
18797
19321
  init_inspect();
18798
19322
  init_introspect();
19323
+ init_introspect_db();
18799
19324
  init_make_model();
18800
19325
  init_migrate();
18801
19326
  init_migrate_generate();
@@ -23415,6 +23940,14 @@ function getDatabase() {
23415
23940
  return exec.sqliteDb;
23416
23941
  throw new Error(`[bun-query-builder] getDatabase() is only available for the sqlite dialect; ` + `the configured dialect is '${exec.dialect}'. Use the async model API instead.`);
23417
23942
  }
23943
+ function softDeletesEnabled(definition) {
23944
+ const t2 = definition.traits;
23945
+ return Boolean(t2?.useSoftDeletes || t2?.softDeletable);
23946
+ }
23947
+ function timestampsEnabled(definition) {
23948
+ const t2 = definition.traits;
23949
+ return Boolean(t2?.useTimestamps || t2?.timestampable);
23950
+ }
23418
23951
  function collectBelongsToManyKeys(definition) {
23419
23952
  const keys = new Set;
23420
23953
  const rel = definition.belongsToMany;
@@ -23584,7 +24117,7 @@ class ModelInstance {
23584
24117
  if (changeKeys.length > 0) {
23585
24118
  const sets = changeKeys.map((k2) => `${k2} = ?`).join(", ");
23586
24119
  const values = [...Object.values(changes), this._attributes[pk]];
23587
- if (this._definition.traits?.useTimestamps) {
24120
+ if (timestampsEnabled(this._definition)) {
23588
24121
  const now = formatNow();
23589
24122
  await exec.run(`UPDATE ${this._definition.table} SET ${sets}, updated_at = ? WHERE ${pk} = ?`, [...Object.values(changes), now, this._attributes[pk]]);
23590
24123
  } else {
@@ -23596,11 +24129,13 @@ class ModelInstance {
23596
24129
  const attrs = this._definition.attributes;
23597
24130
  const data = {};
23598
24131
  for (const [key, attr] of Object.entries(attrs)) {
23599
- if (attr.fillable && this._attributes[key] !== undefined) {
24132
+ if (attr.guarded)
24133
+ continue;
24134
+ if (this._attributes[key] !== undefined) {
23600
24135
  data[key] = this._attributes[key];
23601
24136
  }
23602
24137
  }
23603
- if (this._definition.traits?.useTimestamps) {
24138
+ if (timestampsEnabled(this._definition)) {
23604
24139
  const now = formatNow();
23605
24140
  data.created_at = now;
23606
24141
  data.updated_at = now;
@@ -23646,14 +24181,31 @@ class ModelInstance {
23646
24181
  if (!pkValue)
23647
24182
  throw new Error("Cannot delete a model without a primary key");
23648
24183
  await hooks?.beforeDelete?.(this);
23649
- if (this._definition.traits?.useSoftDeletes) {
23650
- await exec.run(`UPDATE ${this._definition.table} SET deleted_at = ? WHERE ${pk} = ?`, [formatNow(), pkValue]);
24184
+ if (softDeletesEnabled(this._definition)) {
24185
+ const now = formatNow();
24186
+ await exec.run(`UPDATE ${this._definition.table} SET deleted_at = ? WHERE ${pk} = ?`, [now, pkValue]);
24187
+ this._attributes[SOFT_DELETE_COLUMN] = now;
23651
24188
  } else {
23652
24189
  await exec.run(`DELETE FROM ${this._definition.table} WHERE ${pk} = ?`, [pkValue]);
23653
24190
  }
23654
24191
  await hooks?.afterDelete?.(this);
23655
24192
  return true;
23656
24193
  }
24194
+ async restore() {
24195
+ if (!softDeletesEnabled(this._definition))
24196
+ throw new Error(`[orm] restore() requires soft deletes on '${this._definition.name}'`);
24197
+ const pk = this._definition.primaryKey || "id";
24198
+ const pkValue = this._attributes[pk];
24199
+ if (!pkValue)
24200
+ throw new Error("Cannot restore a model without a primary key");
24201
+ await getExecutor().run(`UPDATE ${this._definition.table} SET ${SOFT_DELETE_COLUMN} = ? WHERE ${pk} = ?`, [null, pkValue]);
24202
+ this._attributes[SOFT_DELETE_COLUMN] = null;
24203
+ this._original = null;
24204
+ return this;
24205
+ }
24206
+ trashed() {
24207
+ return this._attributes[SOFT_DELETE_COLUMN] != null;
24208
+ }
23657
24209
  async refresh() {
23658
24210
  const exec = getExecutor();
23659
24211
  const pk = this._definition.primaryKey || "id";
@@ -23958,9 +24510,23 @@ class BelongsToManyRelationBuilder {
23958
24510
  this._offset = n2;
23959
24511
  return this;
23960
24512
  }
24513
+ relatedSelectColumns() {
24514
+ const cols = new Set([this.relatedPk, ...Object.keys(this._relatedDef.attributes ?? {})]);
24515
+ const t2 = this._relatedDef.traits;
24516
+ if (t2?.useTimestamps || t2?.timestampable) {
24517
+ cols.add("created_at");
24518
+ cols.add("updated_at");
24519
+ }
24520
+ if (t2?.useSoftDeletes || t2?.softDeletable)
24521
+ cols.add("deleted_at");
24522
+ if (t2?.useUuid)
24523
+ cols.add("uuid");
24524
+ return [...cols];
24525
+ }
23961
24526
  buildSelect() {
23962
24527
  const params = [];
23963
- let sql2 = `SELECT ${this.relatedTable}.*, ${this.pivotTable}.* FROM ${this.relatedTable}`;
24528
+ const relatedSelect = this.relatedSelectColumns().map((c2) => `${this.relatedTable}.${c2} AS ${BTM_RELATED_ALIAS}${c2}`).join(", ");
24529
+ let sql2 = `SELECT ${relatedSelect}, ${this.pivotTable}.* FROM ${this.relatedTable}`;
23964
24530
  sql2 += ` INNER JOIN ${this.pivotTable} ON ${this.pivotTable}.${this.fkRelated} = ${this.relatedTable}.${this.relatedPk}`;
23965
24531
  sql2 += ` WHERE ${this.pivotTable}.${this.fkParent} = ?`;
23966
24532
  params.push(this.parentId);
@@ -23981,25 +24547,17 @@ class BelongsToManyRelationBuilder {
23981
24547
  return { sql: sql2, params };
23982
24548
  }
23983
24549
  hydrateRows(rows) {
23984
- const relatedAttrs = new Set(Object.keys(this._relatedDef.attributes ?? {}));
23985
- relatedAttrs.add(this.relatedPk);
23986
- relatedAttrs.add("created_at");
23987
- relatedAttrs.add("updated_at");
23988
- relatedAttrs.add("deleted_at");
23989
24550
  const fkParent = this.fkParent;
23990
24551
  const fkRelated = this.fkRelated;
23991
24552
  return rows.map((raw) => {
23992
24553
  const relatedRow = {};
23993
24554
  const pivotExtras = {};
23994
24555
  for (const [k2, v2] of Object.entries(raw)) {
23995
- if (k2 === fkParent || k2 === fkRelated)
23996
- continue;
23997
- if (relatedAttrs.has(k2))
23998
- relatedRow[k2] = v2;
23999
- else
24556
+ if (k2.startsWith(BTM_RELATED_ALIAS))
24557
+ relatedRow[k2.slice(BTM_RELATED_ALIAS.length)] = v2;
24558
+ else if (k2 !== fkParent && k2 !== fkRelated)
24000
24559
  pivotExtras[k2] = v2;
24001
24560
  }
24002
- relatedRow[this.relatedPk] = raw[this.relatedPk];
24003
24561
  const inst = new ModelInstance(this._relatedDef, relatedRow);
24004
24562
  inst.pivot = pivotExtras;
24005
24563
  return inst;
@@ -24146,9 +24704,18 @@ class ModelQueryBuilder {
24146
24704
  _offset;
24147
24705
  _select = ["*"];
24148
24706
  _withRelations = [];
24707
+ _trashed = "exclude";
24149
24708
  constructor(definition) {
24150
24709
  this._definition = definition;
24151
24710
  }
24711
+ withTrashed() {
24712
+ this._trashed = "include";
24713
+ return this;
24714
+ }
24715
+ onlyTrashed() {
24716
+ this._trashed = "only";
24717
+ return this;
24718
+ }
24152
24719
  where(column, operatorOrValue, value) {
24153
24720
  if (value === undefined) {
24154
24721
  this._wheres.push({ column, operator: "=", value: operatorOrValue, boolean: "and" });
@@ -24326,11 +24893,24 @@ class ModelQueryBuilder {
24326
24893
  }
24327
24894
  return clauses.join(" ");
24328
24895
  }
24896
+ softDeleteClause() {
24897
+ if (this._trashed === "include" || !softDeletesEnabled(this._definition))
24898
+ return "";
24899
+ return this._trashed === "only" ? `${SOFT_DELETE_COLUMN} IS NOT NULL` : `${SOFT_DELETE_COLUMN} IS NULL`;
24900
+ }
24901
+ composeWhere(params) {
24902
+ const userClause = this._wheres.length > 0 ? this.buildWhereClauses(params) : "";
24903
+ const sd = this.softDeleteClause();
24904
+ if (userClause && sd)
24905
+ return `(${userClause}) AND ${sd}`;
24906
+ return userClause || sd;
24907
+ }
24329
24908
  buildQuery() {
24330
24909
  const params = [];
24331
24910
  let sql2 = `SELECT ${this._select.join(", ")} FROM ${this._definition.table}`;
24332
- if (this._wheres.length > 0) {
24333
- sql2 += ` WHERE ${this.buildWhereClauses(params)}`;
24911
+ const whereBody = this.composeWhere(params);
24912
+ if (whereBody) {
24913
+ sql2 += ` WHERE ${whereBody}`;
24334
24914
  }
24335
24915
  if (this._orderBy.length > 0) {
24336
24916
  sql2 += ` ORDER BY ${this._orderBy.map((o2) => `${o2.column} ${o2.direction.toUpperCase()}`).join(", ")}`;
@@ -24354,6 +24934,8 @@ class ModelQueryBuilder {
24354
24934
  let rel = relationCache.get(cacheKey);
24355
24935
  if (rel === undefined) {
24356
24936
  rel = resolveRelation(this._definition, relationName);
24937
+ if (relationCache.size >= RELATION_CACHE_MAX)
24938
+ relationCache.clear();
24357
24939
  relationCache.set(cacheKey, rel);
24358
24940
  }
24359
24941
  if (!rel)
@@ -24390,8 +24972,13 @@ class ModelQueryBuilder {
24390
24972
  }
24391
24973
  }
24392
24974
  if (rel.type === "belongsTo") {
24393
- const fkValues = instances.map((i2) => i2._attributes[rel.foreignKey]).filter((v2) => v2 != null);
24394
- const uniqueFkValues = [...new Set(fkValues)];
24975
+ const fkSet = new Set;
24976
+ for (const i2 of instances) {
24977
+ const v2 = i2._attributes[rel.foreignKey];
24978
+ if (v2 != null)
24979
+ fkSet.add(v2);
24980
+ }
24981
+ const uniqueFkValues = [...fkSet];
24395
24982
  if (uniqueFkValues.length === 0)
24396
24983
  continue;
24397
24984
  const placeholders = uniqueFkValues.map(() => "?").join(", ");
@@ -24421,7 +25008,13 @@ class ModelQueryBuilder {
24421
25008
  instance.setRelation(relationName, []);
24422
25009
  continue;
24423
25010
  }
24424
- const relatedIds = [...new Set(pivotRows.map((p2) => p2[rel.pivotFkRelated]))].filter((v2) => v2 != null);
25011
+ const relatedIdSet = new Set;
25012
+ for (const p2 of pivotRows) {
25013
+ const v2 = p2[rel.pivotFkRelated];
25014
+ if (v2 != null)
25015
+ relatedIdSet.add(v2);
25016
+ }
25017
+ const relatedIds = [...relatedIdSet];
24425
25018
  const relatedModelDef = getModelFromRegistry(rel.relatedModelName);
24426
25019
  const relDef = relatedModelDef?.getDefinition?.() || relatedModelDef?.definition || this._definition;
24427
25020
  const relatedPk = relDef?.primaryKey || "id";
@@ -24492,8 +25085,9 @@ class ModelQueryBuilder {
24492
25085
  const exec = getExecutor();
24493
25086
  const params = [];
24494
25087
  let sql2 = `SELECT COUNT(*) as count FROM ${this._definition.table}`;
24495
- if (this._wheres.length > 0) {
24496
- sql2 += ` WHERE ${this.buildWhereClauses(params)}`;
25088
+ const whereBody = this.composeWhere(params);
25089
+ if (whereBody) {
25090
+ sql2 += ` WHERE ${whereBody}`;
24497
25091
  }
24498
25092
  const row = await exec.get(sql2, params);
24499
25093
  return Number(row?.count ?? 0);
@@ -24518,7 +25112,7 @@ class ModelQueryBuilder {
24518
25112
  const exec = getExecutor();
24519
25113
  const params = [amount];
24520
25114
  let sql2 = `UPDATE ${this._definition.table} SET ${column} = ${column} + ?`;
24521
- if (this._definition.traits?.useTimestamps) {
25115
+ if (timestampsEnabled(this._definition)) {
24522
25116
  sql2 += `, updated_at = ?`;
24523
25117
  params.push(formatNow());
24524
25118
  }
@@ -24539,6 +25133,7 @@ class ModelQueryBuilder {
24539
25133
  builder._orderBy = [...this._orderBy];
24540
25134
  builder._select = [...this._select];
24541
25135
  builder._withRelations = [...this._withRelations];
25136
+ builder._trashed = this._trashed;
24542
25137
  builder._limit = size;
24543
25138
  builder._offset = page * size;
24544
25139
  const results = await builder.get();
@@ -24575,8 +25170,9 @@ class ModelQueryBuilder {
24575
25170
  const exec = getExecutor();
24576
25171
  const params = [];
24577
25172
  let sql2 = `SELECT ${column} FROM ${this._definition.table}`;
24578
- if (this._wheres.length > 0) {
24579
- sql2 += ` WHERE ${this.buildWhereClauses(params)}`;
25173
+ const whereBody = this.composeWhere(params);
25174
+ if (whereBody) {
25175
+ sql2 += ` WHERE ${whereBody}`;
24580
25176
  }
24581
25177
  if (this._orderBy.length > 0) {
24582
25178
  sql2 += ` ORDER BY ${this._orderBy.map((o2) => `${o2.column} ${o2.direction.toUpperCase()}`).join(", ")}`;
@@ -24593,8 +25189,9 @@ class ModelQueryBuilder {
24593
25189
  const exec = getExecutor();
24594
25190
  const params = [];
24595
25191
  let sql2 = `SELECT ${fn}(${column}) as v FROM ${this._definition.table}`;
24596
- if (this._wheres.length > 0) {
24597
- sql2 += ` WHERE ${this.buildWhereClauses(params)}`;
25192
+ const whereBody = this.composeWhere(params);
25193
+ if (whereBody) {
25194
+ sql2 += ` WHERE ${whereBody}`;
24598
25195
  }
24599
25196
  const row = await exec.get(sql2, params);
24600
25197
  return row?.v == null ? null : Number(row.v);
@@ -24625,10 +25222,10 @@ class ModelQueryBuilder {
24625
25222
  const entries = Object.entries(data);
24626
25223
  const sets = entries.map(([k2]) => `${k2} = ?`).join(", ");
24627
25224
  const params = entries.map(([, v2]) => v2);
24628
- if (this._definition.traits?.useTimestamps) {
25225
+ if (timestampsEnabled(this._definition)) {
24629
25226
  params.push(formatNow());
24630
25227
  }
24631
- let sql2 = `UPDATE ${this._definition.table} SET ${sets}${this._definition.traits?.useTimestamps ? ", updated_at = ?" : ""}`;
25228
+ let sql2 = `UPDATE ${this._definition.table} SET ${sets}${timestampsEnabled(this._definition) ? ", updated_at = ?" : ""}`;
24632
25229
  if (this._wheres.length > 0) {
24633
25230
  sql2 += ` WHERE ${this.buildWhereClauses(params)}`;
24634
25231
  }
@@ -24674,10 +25271,13 @@ function createModel(definition) {
24674
25271
  limit: (count) => new ModelQueryBuilder(definition).limit(count),
24675
25272
  take: (count) => new ModelQueryBuilder(definition).take(count),
24676
25273
  skip: (count) => new ModelQueryBuilder(definition).skip(count),
25274
+ withTrashed: () => new ModelQueryBuilder(definition).withTrashed(),
25275
+ onlyTrashed: () => new ModelQueryBuilder(definition).onlyTrashed(),
24677
25276
  async find(id) {
24678
25277
  const exec = getExecutor();
24679
25278
  const pk = definition.primaryKey || "id";
24680
- const row = await exec.get(`SELECT * FROM ${definition.table} WHERE ${pk} = ?`, [id]);
25279
+ const sd = softDeletesEnabled(definition) ? ` AND ${SOFT_DELETE_COLUMN} IS NULL` : "";
25280
+ const row = await exec.get(`SELECT * FROM ${definition.table} WHERE ${pk} = ?${sd}`, [id]);
24681
25281
  return row ? new ModelInstance(definition, row) : undefined;
24682
25282
  },
24683
25283
  async findOrFail(id) {
@@ -24689,7 +25289,8 @@ function createModel(definition) {
24689
25289
  async findMany(ids) {
24690
25290
  const exec = getExecutor();
24691
25291
  const pk = definition.primaryKey || "id";
24692
- const rows = await exec.all(`SELECT * FROM ${definition.table} WHERE ${pk} IN (${ids.map(() => "?").join(", ")})`, ids);
25292
+ const sd = softDeletesEnabled(definition) ? ` AND ${SOFT_DELETE_COLUMN} IS NULL` : "";
25293
+ const rows = await exec.all(`SELECT * FROM ${definition.table} WHERE ${pk} IN (${ids.map(() => "?").join(", ")})${sd}`, ids);
24693
25294
  return rows.map((row) => new ModelInstance(definition, row));
24694
25295
  },
24695
25296
  all: () => new ModelQueryBuilder(definition).get(),
@@ -24804,10 +25405,10 @@ async function createTableFromModel(definition) {
24804
25405
  }
24805
25406
  columns.push(colDef);
24806
25407
  }
24807
- if (definition.traits?.useTimestamps) {
25408
+ if (timestampsEnabled(definition)) {
24808
25409
  columns.push("created_at TEXT", "updated_at TEXT");
24809
25410
  }
24810
- if (definition.traits?.useSoftDeletes) {
25411
+ if (softDeletesEnabled(definition)) {
24811
25412
  columns.push("deleted_at TEXT");
24812
25413
  }
24813
25414
  await exec.run(`CREATE TABLE IF NOT EXISTS ${definition.table} (${columns.join(", ")})`, []);
@@ -24852,7 +25453,7 @@ async function seedModel(definition, count, faker) {
24852
25453
  if (attr.factory)
24853
25454
  data[name] = attr.factory(faker);
24854
25455
  }
24855
- if (definition.traits?.useTimestamps) {
25456
+ if (timestampsEnabled(definition)) {
24856
25457
  const now = formatNow();
24857
25458
  data.created_at = now;
24858
25459
  data.updated_at = now;
@@ -24863,7 +25464,7 @@ async function seedModel(definition, count, faker) {
24863
25464
  await exec.run(`INSERT INTO ${definition.table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, Object.values(data));
24864
25465
  }
24865
25466
  }
24866
- var SAFE_SQL_IDENTIFIER, _getModel = null, globalDb = null, _executor = null, _executorForDb = null, _executorDialect = null, _executorDatabase = null, snakeCaseCache, tableNameCache, relationCache;
25467
+ 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;
24867
25468
  var init_orm = __esm(() => {
24868
25469
  init_config();
24869
25470
  init_db();
@@ -26348,6 +26949,23 @@ async function loadModels(options) {
26348
26949
  }
26349
26950
  var init_loader = () => {};
26350
26951
 
26952
+ // src/relation-utils.ts
26953
+ function normalizeRelationEntry(entry) {
26954
+ if (typeof entry === "string")
26955
+ return { model: entry };
26956
+ if (entry && typeof entry === "object" && typeof entry.model === "string") {
26957
+ const e2 = entry;
26958
+ return { model: e2.model, foreignKey: e2.foreignKey, onDelete: e2.onDelete };
26959
+ }
26960
+ return null;
26961
+ }
26962
+ function normalizeRelationList(rel) {
26963
+ if (!rel)
26964
+ return [];
26965
+ const entries = Array.isArray(rel) ? rel : typeof rel === "object" ? Object.values(rel) : [];
26966
+ return entries.map(normalizeRelationEntry).filter((x2) => x2 !== null);
26967
+ }
26968
+
26351
26969
  // src/meta.ts
26352
26970
  function buildSchemaMeta(models) {
26353
26971
  const modelToTable = {};
@@ -26365,13 +26983,24 @@ function buildSchemaMeta(models) {
26365
26983
  const toRecord = (v2) => {
26366
26984
  if (!v2)
26367
26985
  return {};
26986
+ const rec = {};
26368
26987
  if (Array.isArray(v2)) {
26369
- const rec = {};
26370
- for (const relName of v2)
26371
- rec[relName] = relName;
26988
+ for (const item of v2) {
26989
+ const n2 = normalizeRelationEntry(item);
26990
+ if (n2)
26991
+ rec[n2.model] = n2.model;
26992
+ }
26372
26993
  return rec;
26373
26994
  }
26374
- return v2;
26995
+ if (typeof v2 === "object") {
26996
+ for (const [key, val] of Object.entries(v2)) {
26997
+ const n2 = normalizeRelationEntry(val);
26998
+ if (n2)
26999
+ rec[key] = n2.model;
27000
+ }
27001
+ return rec;
27002
+ }
27003
+ return {};
26375
27004
  };
26376
27005
  const toBelongsToManyRecord = (v2) => {
26377
27006
  if (!v2)
@@ -26418,6 +27047,7 @@ function buildSchemaMeta(models) {
26418
27047
  }
26419
27048
  return { modelToTable, tableToModel, primaryKeys, relations, scopes: scopesByTable, models };
26420
27049
  }
27050
+ var init_meta = () => {};
26421
27051
 
26422
27052
  // src/migrations.ts
26423
27053
  import { existsSync as existsSync21, mkdirSync as mkdirSync9, writeFileSync as writeFileSync14 } from "fs";
@@ -26580,15 +27210,6 @@ function detectTypeFromValidationRule(rule) {
26580
27210
  }
26581
27211
  return;
26582
27212
  }
26583
- function normalizeBelongsTo(belongsTo) {
26584
- if (!belongsTo)
26585
- return [];
26586
- if (Array.isArray(belongsTo))
26587
- return belongsTo;
26588
- if (typeof belongsTo === "object")
26589
- return Object.values(belongsTo);
26590
- return [];
26591
- }
26592
27213
  function buildMigrationPlan2(models, options) {
26593
27214
  const meta = buildSchemaMeta(models);
26594
27215
  const tables = [];
@@ -26677,12 +27298,12 @@ function buildMigrationPlan2(models, options) {
26677
27298
  }
26678
27299
  columns.push(col);
26679
27300
  }
26680
- const belongsToModels = normalizeBelongsTo(model.belongsTo);
26681
- for (const relatedModel of belongsToModels) {
26682
- const fkColumnName = `${snakeCase(relatedModel)}_id`;
27301
+ const belongsToRelations = normalizeRelationList(model.belongsTo);
27302
+ for (const rel of belongsToRelations) {
27303
+ const fkColumnName = rel.foreignKey ?? `${snakeCase(rel.model)}_id`;
26683
27304
  if (columns.some((c2) => c2.name === fkColumnName))
26684
27305
  continue;
26685
- const refTable = meta.modelToTable[relatedModel];
27306
+ const refTable = meta.modelToTable[rel.model];
26686
27307
  if (!refTable)
26687
27308
  continue;
26688
27309
  const refPk = meta.primaryKeys[refTable] ?? "id";
@@ -26693,7 +27314,7 @@ function buildMigrationPlan2(models, options) {
26693
27314
  isUnique: false,
26694
27315
  isNullable: true,
26695
27316
  hasDefault: false,
26696
- references: { table: refTable, column: refPk }
27317
+ references: { table: refTable, column: refPk, onDelete: rel.onDelete }
26697
27318
  });
26698
27319
  }
26699
27320
  const traits = model.traits;
@@ -27010,6 +27631,13 @@ function columnsAreDifferent(col1, col2) {
27010
27631
  }
27011
27632
  return false;
27012
27633
  }
27634
+ function referencesAreDifferent(r1, r2) {
27635
+ if (Boolean(r1) !== Boolean(r2))
27636
+ return true;
27637
+ if (!r1 || !r2)
27638
+ return false;
27639
+ return r1.table !== r2.table || r1.column !== r2.column || r1.onDelete !== r2.onDelete || r1.onUpdate !== r2.onUpdate;
27640
+ }
27013
27641
  function mapIndexesByKey(indexes) {
27014
27642
  const map = {};
27015
27643
  for (const i2 of indexes) {
@@ -27153,6 +27781,13 @@ function generateDiffSql(previous, next) {
27153
27781
  info2(`-- Detected column type change: ${curr.table}.${colName} (${prevCol.type} -> ${currCol.type})`);
27154
27782
  hasChanges = true;
27155
27783
  }
27784
+ if (referencesAreDifferent(prevCol.references, currCol.references) && currCol.references) {
27785
+ const addFkStatement = driver.addForeignKey(curr.table, currCol.name, currCol.references.table, currCol.references.column, currCol.references.onDelete, currCol.references.onUpdate);
27786
+ tableChanges.push(addFkStatement);
27787
+ chunks.push(addFkStatement);
27788
+ info2(`-- Detected foreign-key change: ${curr.table}.${colName} -> ${currCol.references.table}(${currCol.references.column})${currCol.references.onDelete ? ` ON DELETE ${currCol.references.onDelete}` : ""}`);
27789
+ hasChanges = true;
27790
+ }
27156
27791
  }
27157
27792
  }
27158
27793
  for (const colName of Object.keys(currCols)) {
@@ -27206,6 +27841,7 @@ var migrationCounter = 0, migrationsCreatedCount = 0, migrationsUpdatedCount = 0
27206
27841
  var init_migrations = __esm(() => {
27207
27842
  init_config();
27208
27843
  init_drivers();
27844
+ init_meta();
27209
27845
  });
27210
27846
 
27211
27847
  // src/schema.ts
@@ -27240,6 +27876,7 @@ var init_src2 = __esm(() => {
27240
27876
  init_dynamodb_tooling_adapter();
27241
27877
  init_dynamodb();
27242
27878
  init_loader();
27879
+ init_meta();
27243
27880
  init_migrations();
27244
27881
  init_orm();
27245
27882
  });
@@ -29283,7 +29920,7 @@ function getPrefix() {
29283
29920
  }
29284
29921
  var prefix = getPrefix();
29285
29922
  // package.json
29286
- var version2 = "0.1.25";
29923
+ var version2 = "0.1.27";
29287
29924
 
29288
29925
  // bin/cli.ts
29289
29926
  init_actions();
@@ -29309,6 +29946,15 @@ var cli = new CLI("query-builder");
29309
29946
  cli.command("introspect <dir>", "Load models and print inferred schema").option("--verbose", "Enable verbose logging").example("query-builder introspect ./app/Models --verbose").action(async (dir, _options) => {
29310
29947
  await introspect(dir, _options);
29311
29948
  });
29949
+ cli.command("introspect:db", "Reverse-introspect the live database into defineModel() source (#1047)").option("--table <table>", "Limit to a single table (repeatable)").example("query-builder introspect:db > app/Models/generated.ts").action(async (_options) => {
29950
+ const t2 = _options?.table;
29951
+ const tables = t2 ? Array.isArray(t2) ? t2 : [t2] : undefined;
29952
+ const models = await introspectDatabase({ tables });
29953
+ console.log(`import { defineModel } from 'bun-query-builder'
29954
+
29955
+ ${models.map((m2) => m2.source).join(`
29956
+ `)}`);
29957
+ });
29312
29958
  cli.command("sql <dir> <table>", "Build a sample query for a table").option("--limit <n>", "Limit rows", { default: 10 }).example("query-builder sql ./app/Models users --limit 5").action(async (dir, table, opts) => {
29313
29959
  await sql(dir, table, opts);
29314
29960
  });