bun-query-builder 0.1.63 → 0.2.2

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
@@ -11641,9 +11641,18 @@ var init_dist = __esm(() => {
11641
11641
  });
11642
11642
 
11643
11643
  // src/config.ts
11644
+ import process19 from "process";
11644
11645
  function isMysqlLike(dialect = config5.dialect) {
11645
11646
  return dialect === "mysql" || dialect === "singlestore";
11646
11647
  }
11648
+ function resolveDialect(explicit) {
11649
+ if (explicit)
11650
+ return explicit;
11651
+ const fromEnv = process19.env.DB_DIALECT || process19.env.DB_CONNECTION;
11652
+ if (fromEnv)
11653
+ return fromEnv;
11654
+ return config5.dialect || "postgres";
11655
+ }
11647
11656
  function getPlaceholder(index) {
11648
11657
  if (config5.dialect === "postgres") {
11649
11658
  return `$${index}`;
@@ -11802,7 +11811,7 @@ var {SQL } = globalThis.Bun;
11802
11811
  import { Database } from "bun:sqlite";
11803
11812
  import { mkdirSync as mkdirSync5 } from "fs";
11804
11813
  import { dirname as dirname5 } from "path";
11805
- import process19 from "process";
11814
+ import process21 from "process";
11806
11815
 
11807
11816
  class SQLiteWrapper {
11808
11817
  db;
@@ -12080,9 +12089,13 @@ function sqliteFileFromUrl(url) {
12080
12089
  const path = stripped.startsWith("///") ? stripped.slice(2) : stripped.startsWith("//") ? stripped.slice(2) : stripped;
12081
12090
  return path || ":memory:";
12082
12091
  }
12092
+ function resolveDatabaseName(dbConfig = config5.database) {
12093
+ const raw = process21.env.DB_DATABASE || dbConfig?.database || "";
12094
+ return String(raw).replace(/^['"]|['"]$/g, "");
12095
+ }
12083
12096
  function createConnectionString(dialect, dbConfig) {
12084
- if ((dialect === "postgres" || dialect === "mysql" || dialect === "singlestore") && process19.env.DB_CONNECTION === dialect) {
12085
- const e = process19.env;
12097
+ if ((dialect === "postgres" || dialect === "mysql" || dialect === "singlestore") && process21.env.DB_CONNECTION === dialect) {
12098
+ const e = process21.env;
12086
12099
  const envDb = (e.DB_DATABASE || dbConfig.database || "").replace(/^['"]|['"]$/g, "");
12087
12100
  const envUser = e.DB_USERNAME || dbConfig.username;
12088
12101
  const envPass = e.DB_PASSWORD ?? dbConfig.password ?? "";
@@ -12096,7 +12109,7 @@ function createConnectionString(dialect, dbConfig) {
12096
12109
  return dialect === "sqlite" ? sqliteFileFromUrl(dbConfig.url) : dbConfig.url;
12097
12110
  }
12098
12111
  const { database, username, password, host = "localhost", port, ssl } = dbConfig;
12099
- const sslQ = ssl || process19.env.DB_SSL === "true" || process19.env.DB_SSL === "1" ? "?ssl=true" : "";
12112
+ const sslQ = ssl || process21.env.DB_SSL === "true" || process21.env.DB_SSL === "1" ? "?ssl=true" : "";
12100
12113
  switch (dialect) {
12101
12114
  case "postgres":
12102
12115
  return `postgres://${username}:${password}@${host}${port ? `:${port}` : ""}/${database}${sslQ}`;
@@ -12370,6 +12383,17 @@ var init_benchmark = __esm(() => {
12370
12383
  });
12371
12384
 
12372
12385
  // src/inflect.ts
12386
+ function toTableName(modelName) {
12387
+ const snake = String(modelName).replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
12388
+ if (snake.endsWith("y") && !/[aeou]y$/.test(snake))
12389
+ return `${snake.slice(0, -1)}ies`;
12390
+ if (snake.endsWith("s") || snake.endsWith("x") || snake.endsWith("ch") || snake.endsWith("sh"))
12391
+ return `${snake}es`;
12392
+ return `${snake}s`;
12393
+ }
12394
+ function tableNameFor(model) {
12395
+ return typeof model?.table === "string" && model.table.length > 0 ? model.table : toTableName(String(model?.name ?? ""));
12396
+ }
12373
12397
  function stripTrailingS(name) {
12374
12398
  return name.endsWith("s") ? name.slice(0, -1) : name;
12375
12399
  }
@@ -12484,6 +12508,24 @@ var init_pivot = __esm(() => {
12484
12508
  function isRawExpression(expr) {
12485
12509
  return typeof expr === "object" && expr !== null && "raw" in expr && typeof expr.raw === "string";
12486
12510
  }
12511
+ function isBoundSqlExpression(expr) {
12512
+ return typeof expr === "object" && expr !== null && "sql" in expr && typeof expr.sql === "string" && (!("parameters" in expr) || Array.isArray(expr.parameters));
12513
+ }
12514
+ function renderBoundSqlExpression(expression, startIndex) {
12515
+ const parameters = expression.parameters ?? [];
12516
+ if (config5.dialect !== "postgres" || parameters.length === 0)
12517
+ return { text: expression.sql, parameters };
12518
+ let parameterIndex = 0;
12519
+ const text = expression.sql.replace(/\?/g, (placeholder) => {
12520
+ if (parameterIndex >= parameters.length)
12521
+ return placeholder;
12522
+ return getPlaceholder(startIndex + parameterIndex++);
12523
+ });
12524
+ if (parameterIndex !== parameters.length) {
12525
+ throw new TypeError(`[query-builder] SQL expression declares ${parameters.length} parameters but contains only ${parameterIndex} placeholders`);
12526
+ }
12527
+ return { text, parameters };
12528
+ }
12487
12529
  function raw(strings, ...values) {
12488
12530
  if (typeof strings === "string")
12489
12531
  return { raw: strings };
@@ -12957,6 +12999,31 @@ function createQueryBuilder(state) {
12957
12999
  throw err;
12958
13000
  });
12959
13001
  }
13002
+ function mutationCount(result) {
13003
+ if (typeof result === "number")
13004
+ return Number.isFinite(result) ? result : 0;
13005
+ if (typeof result === "bigint")
13006
+ return Number(result);
13007
+ if (!result || typeof result !== "object")
13008
+ return 0;
13009
+ const record = result;
13010
+ for (const key of [
13011
+ "changes",
13012
+ "affectedRows",
13013
+ "count",
13014
+ "rowCount",
13015
+ "numAffectedRows",
13016
+ "numUpdatedRows",
13017
+ "numDeletedRows",
13018
+ "numInsertedOrUpdatedRows"
13019
+ ]) {
13020
+ if (record[key] !== undefined && record[key] !== null)
13021
+ return mutationCount(record[key]);
13022
+ }
13023
+ if (Array.isArray(result))
13024
+ return result.reduce((total, item) => total + mutationCount(item), 0);
13025
+ return 0;
13026
+ }
12960
13027
  function makeExecutableQuery(q, text) {
12961
13028
  const sqlText = text ?? computeSqlText(q);
12962
13029
  return {
@@ -14052,7 +14119,16 @@ function createQueryBuilder(state) {
14052
14119
  built = null;
14053
14120
  return this;
14054
14121
  }
14055
- return this;
14122
+ if (typeof expr === "function") {
14123
+ throw new TypeError("where(callback) is not supported. This builder has no expression callback, and " + "ignoring one silently would leave the query matching every row the filter was " + "meant to exclude. Use whereNested(subquery), whereAny(...), or the orWhere* " + "helpers to build a grouped condition.");
14124
+ }
14125
+ if (expr === undefined || expr === null) {
14126
+ throw new TypeError("where() was called with no condition.");
14127
+ }
14128
+ if (typeof expr === "string") {
14129
+ throw new TypeError(`where(${JSON.stringify(expr)}) is missing an operator and a value. Pass ` + "where(column, operator, value), or whereRaw() for a raw fragment.");
14130
+ }
14131
+ throw new TypeError(`where() does not understand a condition of type ${typeof expr}.`);
14056
14132
  },
14057
14133
  whereNull(column) {
14058
14134
  validateIdentifier(String(column), "whereNull(column)");
@@ -15657,8 +15733,17 @@ function createQueryBuilder(state) {
15657
15733
  const setClauses = Array.from({ length: len });
15658
15734
  for (let i = 0;i < len; i++) {
15659
15735
  const key = keys[i];
15660
- setClauses[i] = `${quoteId(key)} = ${getPlaceholder(i + 1)}`;
15661
- params.push(values[key]);
15736
+ const value = values[key];
15737
+ if (isRawExpression(value)) {
15738
+ setClauses[i] = `${quoteId(key)} = ${value.raw}`;
15739
+ } else if (isBoundSqlExpression(value)) {
15740
+ const expression = renderBoundSqlExpression(value, params.length + 1);
15741
+ setClauses[i] = `${quoteId(key)} = ${expression.text}`;
15742
+ params.push(...expression.parameters);
15743
+ } else {
15744
+ setClauses[i] = `${quoteId(key)} = ${getPlaceholder(params.length + 1)}`;
15745
+ params.push(value);
15746
+ }
15662
15747
  }
15663
15748
  sqlText = `${sqlText} SET ${setClauses.join(", ")}`;
15664
15749
  built = _sql.unsafe(sqlText, params);
@@ -15666,17 +15751,29 @@ function createQueryBuilder(state) {
15666
15751
  },
15667
15752
  where(expr, op, value) {
15668
15753
  const getWhereKeyword = () => SQL_PATTERNS.WHERE.test(sqlText) ? "AND" : "WHERE";
15669
- if (typeof expr === "string" && op !== undefined) {
15754
+ if (op !== undefined && (typeof expr === "string" || isRawExpression(expr) || isBoundSqlExpression(expr))) {
15755
+ const safeOperator = assertSafeWhereOperator(op, "updateTable.where");
15756
+ let left;
15757
+ if (typeof expr === "string") {
15758
+ left = quoteId(expr);
15759
+ } else if (isRawExpression(expr)) {
15760
+ left = expr.raw;
15761
+ } else {
15762
+ const expression = renderBoundSqlExpression(expr, params.length + 1);
15763
+ left = expression.text;
15764
+ params.push(...expression.parameters);
15765
+ }
15670
15766
  const paramIndex = params.length + 1;
15671
- sqlText = `${sqlText} ${getWhereKeyword()} ${quoteId(expr)} ${op} ${getPlaceholder(paramIndex)}`;
15767
+ sqlText = `${sqlText} ${getWhereKeyword()} ${left} ${safeOperator} ${getPlaceholder(paramIndex)}`;
15672
15768
  params.push(value);
15673
15769
  built = _sql.unsafe(sqlText, params);
15674
15770
  return this;
15675
15771
  }
15676
15772
  if (Array.isArray(expr)) {
15677
15773
  const [col, op2, val] = expr;
15774
+ const safeOperator = assertSafeWhereOperator(op2, "updateTable.where");
15678
15775
  const paramIndex = params.length + 1;
15679
- sqlText = `${sqlText} ${getWhereKeyword()} ${quoteId(String(col))} ${String(op2)} ${getPlaceholder(paramIndex)}`;
15776
+ sqlText = `${sqlText} ${getWhereKeyword()} ${quoteId(String(col))} ${safeOperator} ${getPlaceholder(paramIndex)}`;
15680
15777
  params.push(val);
15681
15778
  built = _sql.unsafe(sqlText, params);
15682
15779
  return this;
@@ -15735,15 +15832,16 @@ function createQueryBuilder(state) {
15735
15832
  }
15736
15833
  return makeExecutableQuery(built, sqlText);
15737
15834
  },
15738
- execute() {
15739
- return runWithHooks(built, "update");
15835
+ async execute() {
15836
+ const result = await runWithHooks(built, "update");
15837
+ return mutationCount(result);
15740
15838
  },
15741
15839
  async executeTakeFirst() {
15742
- const result = await runWithHooks(built, "update");
15840
+ const result = mutationCount(await runWithHooks(built, "update"));
15743
15841
  return { numUpdatedRows: result };
15744
15842
  },
15745
15843
  async executeTakeFirstOrThrow() {
15746
- const result = await runWithHooks(built, "update");
15844
+ const result = mutationCount(await runWithHooks(built, "update"));
15747
15845
  if (result === 0)
15748
15846
  throw new Error("No rows updated");
15749
15847
  return { numUpdatedRows: result };
@@ -15864,18 +15962,18 @@ function createQueryBuilder(state) {
15864
15962
  } catch (err) {
15865
15963
  throw err;
15866
15964
  }
15867
- const result = await runWithHooks(ensureDelBuilt(), "delete");
15965
+ const result = mutationCount(await runWithHooks(ensureDelBuilt(), "delete"));
15868
15966
  try {
15869
15967
  await config5.hooks?.afterDelete?.({ table: String(table), where: whereCondition, result });
15870
15968
  } catch {}
15871
15969
  return result;
15872
15970
  },
15873
15971
  async executeTakeFirst() {
15874
- const result = await runWithHooks(ensureDelBuilt(), "delete");
15972
+ const result = mutationCount(await runWithHooks(ensureDelBuilt(), "delete"));
15875
15973
  return { numDeletedRows: result };
15876
15974
  },
15877
15975
  async executeTakeFirstOrThrow() {
15878
- const result = await runWithHooks(ensureDelBuilt(), "delete");
15976
+ const result = mutationCount(await runWithHooks(ensureDelBuilt(), "delete"));
15879
15977
  if (result === 0)
15880
15978
  throw new Error("No rows deleted");
15881
15979
  return { numDeletedRows: result };
@@ -16590,7 +16688,7 @@ var init_cache = __esm(() => {
16590
16688
  });
16591
16689
 
16592
16690
  // src/actions/console.ts
16593
- import process21 from "process";
16691
+ import process22 from "process";
16594
16692
  import { createInterface } from "readline";
16595
16693
  async function startConsole() {
16596
16694
  console.log("-- Query Builder Interactive Console");
@@ -16599,8 +16697,8 @@ async function startConsole() {
16599
16697
  console.log();
16600
16698
  const qb = createQueryBuilder();
16601
16699
  const rl = createInterface({
16602
- input: process21.stdin,
16603
- output: process21.stdout,
16700
+ input: process22.stdin,
16701
+ output: process22.stdout,
16604
16702
  prompt: "qb> "
16605
16703
  });
16606
16704
  let multilineBuffer = "";
@@ -16651,7 +16749,7 @@ Tips:
16651
16749
  if (cmd === ".exit" || cmd === ".quit") {
16652
16750
  console.log("Goodbye!");
16653
16751
  rl.close();
16654
- process21.exit(0);
16752
+ process22.exit(0);
16655
16753
  } else if (cmd === ".help") {
16656
16754
  console.log(helpText);
16657
16755
  } else if (cmd === ".clear") {
@@ -16722,7 +16820,7 @@ Tips:
16722
16820
  rl.on("close", () => {
16723
16821
  console.log(`
16724
16822
  Goodbye!`);
16725
- process21.exit(0);
16823
+ process22.exit(0);
16726
16824
  });
16727
16825
  rl.prompt();
16728
16826
  }
@@ -17100,9 +17198,8 @@ var init_db_info = __esm(() => {
17100
17198
  });
17101
17199
 
17102
17200
  // src/actions/db-optimize.ts
17103
- import process22 from "process";
17104
17201
  async function dbOptimize(options = {}) {
17105
- const dialect = options.dialect || process22.env.DB_DIALECT || config5.dialect || "postgres";
17202
+ const dialect = resolveDialect(options.dialect);
17106
17203
  const aggressive = options.aggressive || false;
17107
17204
  if (options.verbose) {
17108
17205
  console.log(`Optimizing ${dialect} database${aggressive ? " (aggressive mode)" : ""}...`);
@@ -17142,7 +17239,7 @@ async function dbOptimize(options = {}) {
17142
17239
  await bunSql`ANALYZE TABLE ${bunSql(table)}`;
17143
17240
  }
17144
17241
  } else {
17145
- const dbName = process22.env.DB_NAME || "test";
17242
+ const dbName = resolveDatabaseName();
17146
17243
  const tables = await bunSql`
17147
17244
  SELECT table_name
17148
17245
  FROM information_schema.tables
@@ -17195,9 +17292,8 @@ var init_db_optimize = __esm(() => {
17195
17292
  });
17196
17293
 
17197
17294
  // src/actions/db-wipe.ts
17198
- import process23 from "process";
17199
17295
  async function dbWipe(options = {}) {
17200
- const dialect = options.dialect || process23.env.DB_DIALECT || config5.dialect || "postgres";
17296
+ const dialect = resolveDialect(options.dialect);
17201
17297
  if (options.verbose) {
17202
17298
  console.log(`Wiping all tables from ${dialect} database...`);
17203
17299
  }
@@ -17211,7 +17307,7 @@ async function dbWipe(options = {}) {
17211
17307
  `;
17212
17308
  tables = result.map((row) => row.tablename);
17213
17309
  } else if (isMysqlLike(dialect)) {
17214
- const dbName = process23.env.DB_NAME || "test";
17310
+ const dbName = resolveDatabaseName();
17215
17311
  const result = await bunSql`
17216
17312
  SELECT table_name
17217
17313
  FROM information_schema.tables
@@ -17648,6 +17744,8 @@ function sqlTypeToNormalized(rawType, dialect, ctx) {
17648
17744
  return "decimal";
17649
17745
  if (base === "date")
17650
17746
  return "date";
17747
+ if (base === "timestamptz" || /\bwith time zone\b/.test(t))
17748
+ return "timestamptz";
17651
17749
  if (/^(?:timestamp|datetime)/.test(base))
17652
17750
  return "datetime";
17653
17751
  if (/^time/.test(base))
@@ -17658,10 +17756,6 @@ function sqlTypeToNormalized(rawType, dialect, ctx) {
17658
17756
  return "enum";
17659
17757
  return "string";
17660
17758
  }
17661
- function stripIndexPrefix(physicalName, table) {
17662
- const prefix = `${table}_`;
17663
- return physicalName.startsWith(prefix) ? physicalName.slice(prefix.length) : physicalName;
17664
- }
17665
17759
  function asAction(raw2) {
17666
17760
  const v = String(raw2 ?? "").toLowerCase().trim();
17667
17761
  if (v === "cascade" || v === "restrict" || v === "set null")
@@ -17697,7 +17791,7 @@ async function buildSqliteTable(qb, table) {
17697
17791
  const isUnique = Number(idx.unique) === 1;
17698
17792
  if (isUnique && columns2.length === 1)
17699
17793
  uniqueColumns.add(columns2[0]);
17700
- indexes.push({ name: stripIndexPrefix(physName, table), columns: columns2, type: isUnique ? "unique" : "index" });
17794
+ indexes.push({ name: physName, columns: columns2, type: isUnique ? "unique" : "index" });
17701
17795
  }
17702
17796
  const columns = colRows.map((r) => {
17703
17797
  const name = String(r.name);
@@ -17734,7 +17828,31 @@ function normalizeRawDefault(raw2) {
17734
17828
  return raw2;
17735
17829
  return String(raw2);
17736
17830
  }
17737
- async function buildPgTable(qb, table) {
17831
+ async function pgEnumLabels(qb) {
17832
+ const rows = await qb.unsafe(`SELECT t.typname AS type_name, e.enumlabel AS label
17833
+ FROM pg_type t
17834
+ JOIN pg_enum e ON e.enumtypid = t.oid
17835
+ JOIN pg_namespace n ON n.oid = t.typnamespace
17836
+ WHERE n.nspname = 'public'
17837
+ ORDER BY t.typname, e.enumsortorder`);
17838
+ const labels = new Map;
17839
+ for (const row of rows) {
17840
+ const name = String(row.type_name);
17841
+ const existing = labels.get(name);
17842
+ if (existing)
17843
+ existing.push(String(row.label));
17844
+ else
17845
+ labels.set(name, [String(row.label)]);
17846
+ }
17847
+ return labels;
17848
+ }
17849
+ function pgColumnType(dataType, udtName, enums) {
17850
+ const enumValues = enums.get(udtName);
17851
+ const type = sqlTypeToNormalized(String(dataType || udtName || ""), "postgres", { enumValues });
17852
+ return enumValues ? { type, enumValues, enumTypeName: udtName } : { type };
17853
+ }
17854
+ async function buildPgTable(qb, table, enumLabels) {
17855
+ const enums = enumLabels ?? await pgEnumLabels(qb);
17738
17856
  const colRows = await qb.unsafe(`SELECT c.column_name, c.data_type, c.udt_name, c.is_nullable, c.column_default, c.character_maximum_length,
17739
17857
  (SELECT COUNT(*) FROM information_schema.table_constraints tc
17740
17858
  JOIN information_schema.key_column_usage k ON k.constraint_name = tc.constraint_name
@@ -17782,7 +17900,7 @@ async function buildPgTable(qb, table) {
17782
17900
  const hasDefault = rawDefault != null && !isSerial;
17783
17901
  return {
17784
17902
  name,
17785
- type: sqlTypeToNormalized(String(r.data_type ?? r.udt_name ?? ""), "postgres"),
17903
+ ...pgColumnType(String(r.data_type ?? ""), r.udt_name == null ? "" : String(r.udt_name), enums),
17786
17904
  isPrimaryKey,
17787
17905
  isUnique: uniqueColumns.has(name),
17788
17906
  isNullable: isPrimaryKey ? false : String(r.is_nullable).toUpperCase() === "YES",
@@ -17868,7 +17986,7 @@ function groupPhysicalIndexes(table, rows) {
17868
17986
  if (entry.isUnique && entry.columns.length === 1)
17869
17987
  uniqueColumns.add(entry.columns[0]);
17870
17988
  indexes.push({
17871
- name: stripIndexPrefix(physName, table),
17989
+ name: physName,
17872
17990
  columns: entry.columns,
17873
17991
  type: entry.isUnique ? "unique" : "index",
17874
17992
  where: entry.where
@@ -17881,13 +17999,14 @@ async function buildPlanFromDatabase(dialect, opts = {}) {
17881
17999
  const qb = createQueryBuilder();
17882
18000
  const allTables = opts.tables?.length ? opts.tables : await listTables(qb, d);
17883
18001
  const tables = allTables.filter((t) => t !== "migrations");
18002
+ const enumLabels = d === "postgres" ? await pgEnumLabels(qb) : undefined;
17884
18003
  const out = [];
17885
18004
  for (const table of tables) {
17886
18005
  let plan;
17887
18006
  if (d === "sqlite")
17888
18007
  plan = await buildSqliteTable(qb, table);
17889
18008
  else if (d === "postgres")
17890
- plan = await buildPgTable(qb, table);
18009
+ plan = await buildPgTable(qb, table, enumLabels);
17891
18010
  else
17892
18011
  plan = await buildMysqlTable(qb, table);
17893
18012
  if (plan.columns.length > 0)
@@ -17903,7 +18022,7 @@ var init_introspect_db = __esm(() => {
17903
18022
  // src/actions/make-model.ts
17904
18023
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
17905
18024
  import { dirname as dirname7, join as join7 } from "path";
17906
- import process24 from "process";
18025
+ import process23 from "process";
17907
18026
  function findWorkspaceRoot(startPath) {
17908
18027
  let currentPath = startPath;
17909
18028
  while (currentPath !== dirname7(currentPath)) {
@@ -17912,10 +18031,10 @@ function findWorkspaceRoot(startPath) {
17912
18031
  }
17913
18032
  currentPath = dirname7(currentPath);
17914
18033
  }
17915
- return process24.cwd();
18034
+ return process23.cwd();
17916
18035
  }
17917
18036
  async function makeModel(name, options = {}) {
17918
- const workspaceRoot = findWorkspaceRoot(process24.cwd());
18037
+ const workspaceRoot = findWorkspaceRoot(process23.cwd());
17919
18038
  const modelsDir = options.dir || join7(workspaceRoot, "app/Models");
17920
18039
  if (!existsSync14(modelsDir)) {
17921
18040
  mkdirSync7(modelsDir, { recursive: true });
@@ -17958,6 +18077,12 @@ export default model
17958
18077
  }
17959
18078
  var init_make_model = () => {};
17960
18079
 
18080
+ // src/drivers/index-name.ts
18081
+ function qualifiedIndexName(tableName, indexName) {
18082
+ const prefix = `${tableName}_`;
18083
+ return indexName.startsWith(prefix) ? indexName : `${prefix}${indexName}`;
18084
+ }
18085
+
17961
18086
  // src/drivers/mysql.ts
17962
18087
  class MySQLDriver {
17963
18088
  quoteIdentifier(id) {
@@ -18044,7 +18169,7 @@ class MySQLDriver {
18044
18169
  throw new Error(`[migrations] Partial indexes (CompositeIndex.where) are not supported on MySQL. Index '${index.name}' on table '${tableName}' uses WHERE clause: ${index.where}`);
18045
18170
  }
18046
18171
  const kind = index.type === "unique" ? "UNIQUE " : "";
18047
- const idxName = `${tableName}_${index.name}`;
18172
+ const idxName = qualifiedIndexName(tableName, index.name);
18048
18173
  const columns = index.columns.map((c) => this.quoteIdentifier(c)).join(", ");
18049
18174
  return `CREATE ${kind}INDEX ${this.quoteIdentifier(idxName)} ON ${this.quoteIdentifier(tableName)} (${columns});`;
18050
18175
  }
@@ -18097,7 +18222,7 @@ class MySQLDriver {
18097
18222
  return `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP COLUMN ${this.quoteIdentifier(columnName)};`;
18098
18223
  }
18099
18224
  dropIndex(tableName, indexName) {
18100
- const fullIndexName = `${tableName}_${indexName}`;
18225
+ const fullIndexName = qualifiedIndexName(tableName, indexName);
18101
18226
  return `DROP INDEX ${this.quoteIdentifier(fullIndexName)} ON ${this.quoteIdentifier(tableName)};`;
18102
18227
  }
18103
18228
  dropEnumType(_enumTypeName) {
@@ -18111,7 +18236,7 @@ class MySQLDriver {
18111
18236
  )`;
18112
18237
  }
18113
18238
  getExecutedMigrationsQuery() {
18114
- return "SELECT migration FROM migrations ORDER BY executed_at";
18239
+ return "SELECT migration, executed_at FROM migrations ORDER BY executed_at";
18115
18240
  }
18116
18241
  recordMigrationQuery() {
18117
18242
  return "INSERT INTO migrations (migration) VALUES (?)";
@@ -18144,6 +18269,7 @@ class MySQLDriver {
18144
18269
  return parts.join(" ");
18145
18270
  }
18146
18271
  }
18272
+ var init_mysql = () => {};
18147
18273
 
18148
18274
  // src/drivers/postgres.ts
18149
18275
  class PostgresDriver {
@@ -18178,7 +18304,7 @@ class PostgresDriver {
18178
18304
  return "jsonb";
18179
18305
  case "enum":
18180
18306
  if (column.enumValues && column.enumValues.length > 0) {
18181
- return this.quoteIdentifier(column.enumTypeName ?? `${column.name}_type`);
18307
+ return column.enumTypeName ? this.quoteIdentifier(column.enumTypeName) : "text";
18182
18308
  }
18183
18309
  return "text";
18184
18310
  default:
@@ -18232,7 +18358,7 @@ class PostgresDriver {
18232
18358
  }
18233
18359
  createIndex(tableName, index) {
18234
18360
  const kind = index.type === "unique" ? "UNIQUE " : "";
18235
- const idxName = `${tableName}_${index.name}`;
18361
+ const idxName = qualifiedIndexName(tableName, index.name);
18236
18362
  const columns = index.columns.map((c) => this.quoteIdentifier(c)).join(", ");
18237
18363
  const where = index.where ? ` WHERE ${index.where}` : "";
18238
18364
  return `CREATE ${kind}INDEX IF NOT EXISTS ${this.quoteIdentifier(idxName)} ON ${this.quoteIdentifier(tableName)} (${columns})${where};`;
@@ -18260,7 +18386,17 @@ class PostgresDriver {
18260
18386
  }
18261
18387
  modifyColumn(tableName, column) {
18262
18388
  const typeSql = this.getColumnType(column);
18263
- return `ALTER TABLE ${this.quoteIdentifier(tableName)} ALTER COLUMN ${this.quoteIdentifier(column.name)} TYPE ${typeSql} USING ${this.quoteIdentifier(column.name)}::${typeSql};`;
18389
+ const table = this.quoteIdentifier(tableName);
18390
+ const name = this.quoteIdentifier(column.name);
18391
+ const statements = [
18392
+ `ALTER TABLE ${table} ALTER COLUMN ${name} TYPE ${typeSql} USING ${name}::${typeSql};`
18393
+ ];
18394
+ if (!column.isPrimaryKey)
18395
+ statements.push(`ALTER TABLE ${table} ALTER COLUMN ${name} ${column.isNullable ? "DROP NOT NULL" : "SET NOT NULL"};`);
18396
+ const defaultValue = this.getDefaultValue(column);
18397
+ statements.push(defaultValue ? `ALTER TABLE ${table} ALTER COLUMN ${name} SET ${defaultValue};` : `ALTER TABLE ${table} ALTER COLUMN ${name} DROP DEFAULT;`);
18398
+ return statements.join(`
18399
+ `);
18264
18400
  }
18265
18401
  renameColumn(tableName, from, to) {
18266
18402
  return `ALTER TABLE ${this.quoteIdentifier(tableName)} RENAME COLUMN ${this.quoteIdentifier(from)} TO ${this.quoteIdentifier(to)};`;
@@ -18278,7 +18414,7 @@ class PostgresDriver {
18278
18414
  return `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP COLUMN ${this.quoteIdentifier(columnName)};`;
18279
18415
  }
18280
18416
  dropIndex(tableName, indexName) {
18281
- const fullIndexName = `${tableName}_${indexName}`;
18417
+ const fullIndexName = qualifiedIndexName(tableName, indexName);
18282
18418
  return `DROP INDEX IF EXISTS ${this.quoteIdentifier(fullIndexName)};`;
18283
18419
  }
18284
18420
  dropEnumType(enumTypeName) {
@@ -18292,7 +18428,7 @@ class PostgresDriver {
18292
18428
  )`;
18293
18429
  }
18294
18430
  getExecutedMigrationsQuery() {
18295
- return "SELECT migration FROM migrations ORDER BY executed_at";
18431
+ return "SELECT migration, executed_at FROM migrations ORDER BY executed_at";
18296
18432
  }
18297
18433
  recordMigrationQuery() {
18298
18434
  return "INSERT INTO migrations (migration) VALUES ($1)";
@@ -18321,10 +18457,12 @@ class PostgresDriver {
18321
18457
  return parts.join(" ");
18322
18458
  }
18323
18459
  }
18460
+ var init_postgres = () => {};
18324
18461
 
18325
18462
  // src/drivers/singlestore.ts
18326
18463
  var SingleStoreDriver;
18327
18464
  var init_singlestore = __esm(() => {
18465
+ init_mysql();
18328
18466
  SingleStoreDriver = class SingleStoreDriver extends MySQLDriver {
18329
18467
  createTable(table) {
18330
18468
  const columns = table.columns.map((c) => this.renderColumn(c)).join(`,
@@ -18458,7 +18596,7 @@ class SQLiteDriver {
18458
18596
  }
18459
18597
  createIndex(tableName, index) {
18460
18598
  const kind = index.type === "unique" ? "UNIQUE " : "";
18461
- const idxName = `${tableName}_${index.name}`;
18599
+ const idxName = qualifiedIndexName(tableName, index.name);
18462
18600
  const columns = index.columns.map((c) => this.quoteIdentifier(c)).join(", ");
18463
18601
  const where = index.where ? ` WHERE ${index.where}` : "";
18464
18602
  return `CREATE ${kind}INDEX IF NOT EXISTS ${this.quoteIdentifier(idxName)} ON ${this.quoteIdentifier(tableName)} (${columns})${where};`;
@@ -18523,7 +18661,7 @@ class SQLiteDriver {
18523
18661
  return `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP COLUMN ${this.quoteIdentifier(columnName)};`;
18524
18662
  }
18525
18663
  dropIndex(tableName, indexName) {
18526
- const fullIndexName = `${tableName}_${indexName}`;
18664
+ const fullIndexName = qualifiedIndexName(tableName, indexName);
18527
18665
  return `DROP INDEX IF EXISTS ${this.quoteIdentifier(fullIndexName)};`;
18528
18666
  }
18529
18667
  dropEnumType(_enumTypeName) {
@@ -18537,7 +18675,7 @@ class SQLiteDriver {
18537
18675
  )`;
18538
18676
  }
18539
18677
  getExecutedMigrationsQuery() {
18540
- return "SELECT migration FROM migrations ORDER BY executed_at";
18678
+ return "SELECT migration, executed_at FROM migrations ORDER BY executed_at";
18541
18679
  }
18542
18680
  recordMigrationQuery() {
18543
18681
  return "INSERT INTO migrations (migration) VALUES (?)";
@@ -18569,6 +18707,7 @@ class SQLiteDriver {
18569
18707
  return parts.join(" ");
18570
18708
  }
18571
18709
  }
18710
+ var init_sqlite = () => {};
18572
18711
 
18573
18712
  // src/drivers/index.ts
18574
18713
  function getDialectDriver(dialect) {
@@ -18586,10 +18725,34 @@ function getDialectDriver(dialect) {
18586
18725
  }
18587
18726
  }
18588
18727
  var init_drivers = __esm(() => {
18728
+ init_mysql();
18729
+ init_postgres();
18589
18730
  init_singlestore();
18731
+ init_sqlite();
18732
+ init_mysql();
18733
+ init_postgres();
18590
18734
  init_singlestore();
18735
+ init_sqlite();
18591
18736
  });
18592
18737
 
18738
+ // src/workspace.ts
18739
+ import { existsSync as existsSync16 } from "fs";
18740
+ import { dirname as dirname8, join as join9 } from "path";
18741
+ import process24 from "process";
18742
+ function findWorkspaceRoot2(startPath = process24.cwd()) {
18743
+ let currentPath = startPath;
18744
+ while (currentPath !== dirname8(currentPath)) {
18745
+ if (existsSync16(join9(currentPath, "package.json")))
18746
+ return currentPath;
18747
+ currentPath = dirname8(currentPath);
18748
+ }
18749
+ return process24.cwd();
18750
+ }
18751
+ function getSqlDirectory(workspaceRoot) {
18752
+ return join9(workspaceRoot ?? findWorkspaceRoot2(), "database", "migrations");
18753
+ }
18754
+ var init_workspace = () => {};
18755
+
18593
18756
  // src/actions/migrate.ts
18594
18757
  var exports_migrate = {};
18595
18758
  __export(exports_migrate, {
@@ -18601,23 +18764,48 @@ __export(exports_migrate, {
18601
18764
  copyModelsToGenerated: () => copyModelsToGenerated,
18602
18765
  clearGeneratedDirectory: () => clearGeneratedDirectory
18603
18766
  });
18604
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, mkdtempSync, readdirSync as readdirSync6, readFileSync as readFileSync3, rmSync, unlinkSync, writeFileSync as writeFileSync11 } from "fs";
18767
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, mkdtempSync, readdirSync as readdirSync6, readFileSync as readFileSync3, rmSync, unlinkSync, writeFileSync as writeFileSync11 } from "fs";
18605
18768
  import { tmpdir } from "os";
18606
- import { join as join9 } from "path";
18769
+ import { join as join10 } from "path";
18607
18770
  import process25 from "process";
18771
+ async function settleAppliedFiles(qb, dialect, files) {
18772
+ if (files.length === 0)
18773
+ return;
18774
+ const sqlDir = ensureSqlDirectory(getWorkspaceRoot());
18775
+ try {
18776
+ await createMigrationsTable(qb, dialect);
18777
+ for (const file2 of files) {
18778
+ await recordMigration(qb, file2, dialect);
18779
+ info(`-- \u2713 Recorded applied migration: ${file2}`);
18780
+ }
18781
+ } catch (err) {
18782
+ info(`-- Could not record applied migrations (the schema change itself succeeded): ${err instanceof Error ? err.message : String(err)}`);
18783
+ }
18784
+ }
18785
+ function isGeneratedMigration(sqlDir, file2) {
18786
+ try {
18787
+ return isGeneratedMigrationSql(readFileSync3(join10(sqlDir, file2), "utf8"));
18788
+ } catch {
18789
+ return false;
18790
+ }
18791
+ }
18792
+ function isAlreadyAppliedError(err) {
18793
+ const message = err instanceof Error ? err.message : String(err);
18794
+ return /duplicate column name/i.test(message) || /duplicate key name/i.test(message) || /\balready exists\b/i.test(message);
18795
+ }
18608
18796
  function info(message) {
18609
18797
  if (config5.verbose)
18610
18798
  console.log(message);
18611
18799
  }
18612
18800
  function getSnapshotDir(workspaceRoot) {
18613
- return join9(workspaceRoot, config5.snapshotDir || ".qb");
18801
+ return join10(workspaceRoot, config5.snapshotDir || ".qb");
18614
18802
  }
18615
18803
  function getSnapshotPath(workspaceRoot, dialect) {
18616
- return join9(getSnapshotDir(workspaceRoot), `model-snapshot.${dialect}.json`);
18804
+ return join10(getSnapshotDir(workspaceRoot), `model-snapshot.${dialect}.json`);
18617
18805
  }
18618
18806
  function loadPlanSnapshot(workspaceRoot, dialect) {
18619
18807
  const snapshotPath = getSnapshotPath(workspaceRoot, dialect);
18620
- if (!existsSync16(snapshotPath)) {
18808
+ if (!existsSync17(snapshotPath)) {
18621
18809
  return;
18622
18810
  }
18623
18811
  try {
@@ -18640,7 +18828,7 @@ function savePlanSnapshot(workspaceRoot, dialect, plan) {
18640
18828
  const snapshotPath = getSnapshotPath(workspaceRoot, dialect);
18641
18829
  const snapshotDir = getSnapshotDir(workspaceRoot);
18642
18830
  const hash = hashMigrationPlan(plan);
18643
- if (existsSync16(snapshotPath)) {
18831
+ if (existsSync17(snapshotPath)) {
18644
18832
  try {
18645
18833
  const current = JSON.parse(readFileSync3(snapshotPath, "utf8"));
18646
18834
  if (current?.hash === hash && current?.dialect === dialect && current?.plan) {
@@ -18649,7 +18837,7 @@ function savePlanSnapshot(workspaceRoot, dialect, plan) {
18649
18837
  }
18650
18838
  } catch {}
18651
18839
  }
18652
- if (!existsSync16(snapshotDir)) {
18840
+ if (!existsSync17(snapshotDir)) {
18653
18841
  mkdirSync8(snapshotDir, { recursive: true });
18654
18842
  info(`-- Created snapshot directory: ${snapshotDir}`);
18655
18843
  }
@@ -18667,11 +18855,11 @@ function saveMigrationSnapshot(plan, options = {}) {
18667
18855
  savePlanSnapshot(options.workspaceRoot ?? getWorkspaceRoot(), dialect, plan);
18668
18856
  }
18669
18857
  function getWorkspaceRoot() {
18670
- return process25.cwd();
18858
+ return findWorkspaceRoot2();
18671
18859
  }
18672
18860
  function ensureSqlDirectory(workspaceRoot) {
18673
18861
  const sqlDir = getSqlDirectory(workspaceRoot);
18674
- if (!existsSync16(sqlDir)) {
18862
+ if (!existsSync17(sqlDir)) {
18675
18863
  mkdirSync8(sqlDir, { recursive: true });
18676
18864
  info(`-- Created SQL directory: ${sqlDir}`);
18677
18865
  }
@@ -18680,21 +18868,22 @@ function ensureSqlDirectory(workspaceRoot) {
18680
18868
  async function generateMigration(dir, opts = {}) {
18681
18869
  await getConfig();
18682
18870
  if (!dir) {
18683
- dir = join9(process25.cwd(), "app/Models");
18871
+ dir = join10(process25.cwd(), "app/Models");
18684
18872
  }
18685
18873
  const dialect = opts.dialect || config5.dialect || "postgres";
18686
18874
  const workspaceRoot = getWorkspaceRoot();
18687
18875
  const models = await loadModels({ modelsDir: dir });
18688
18876
  const plan = buildMigrationPlan2(models, { dialect });
18689
18877
  let previous;
18878
+ let reconciledFromDatabase = false;
18690
18879
  if (!opts.full) {
18691
18880
  previous = loadPlanSnapshot(workspaceRoot, dialect);
18692
18881
  if (previous) {
18693
18882
  info("-- Comparing with stored model snapshot");
18694
18883
  } else {
18695
- const defaultStatePath = join9(dir, `.qb-migrations.${dialect}.json`);
18884
+ const defaultStatePath = join10(dir, `.qb-migrations.${dialect}.json`);
18696
18885
  const statePath = String(opts.state || defaultStatePath);
18697
- if (existsSync16(statePath)) {
18886
+ if (existsSync17(statePath)) {
18698
18887
  try {
18699
18888
  const raw2 = readFileSync3(statePath, "utf8");
18700
18889
  const parsed = JSON.parse(raw2);
@@ -18711,6 +18900,7 @@ async function generateMigration(dir, opts = {}) {
18711
18900
  const scoped = livePlan.tables.filter((t) => modelTables.has(t.table));
18712
18901
  if (scoped.length > 0) {
18713
18902
  previous = { dialect: livePlan.dialect, tables: scoped };
18903
+ reconciledFromDatabase = true;
18714
18904
  info(`-- No snapshot; reconciling against live database (${scoped.length} matching table${scoped.length === 1 ? "" : "s"})`);
18715
18905
  }
18716
18906
  } catch (err) {
@@ -18724,25 +18914,34 @@ async function generateMigration(dir, opts = {}) {
18724
18914
  } else {
18725
18915
  info("-- Full migration requested, ignoring any previous state");
18726
18916
  }
18727
- const diff = opts.full ? { statements: generateSql(plan, { dryRun: opts.dryRun }), operations: plan.tables.map((t) => ({ kind: "create_table", table: t.table, destructive: false, sql: "" })) } : generateDiffOperations(previous, plan, { applyRenames: opts.applyRenames, dryRun: opts.dryRun });
18917
+ const diff = opts.full ? { statements: generateSql(plan, { dryRun: opts.dryRun }), operations: plan.tables.map((t) => ({ kind: "create_table", table: t.table, destructive: false, sql: "" })) } : generateDiffOperations(previous, plan, {
18918
+ applyRenames: opts.applyRenames,
18919
+ dryRun: opts.dryRun,
18920
+ preserveUnknownColumns: reconciledFromDatabase
18921
+ });
18728
18922
  const sqlStatements = diff.statements;
18729
18923
  const operations = diff.operations;
18730
18924
  const sql = sqlStatements.join(`
18731
18925
  `);
18732
18926
  const hasChanges = sqlStatements.some((stmt) => /\b(?:CREATE|ALTER|DROP)\b/i.test(stmt));
18733
18927
  if (opts.apply) {
18734
- const dirPath = mkdtempSync(join9(tmpdir(), "qb-migrate-"));
18735
- const filePath = join9(dirPath, "migration.sql");
18928
+ const dirPath = mkdtempSync(join10(tmpdir(), "qb-migrate-"));
18929
+ const filePath = join10(dirPath, "migration.sql");
18736
18930
  try {
18737
18931
  if (hasChanges) {
18738
18932
  writeFileSync11(filePath, sql);
18933
+ const qb = createQueryBuilder();
18934
+ await qb.file(filePath);
18739
18935
  info("-- Migration applied");
18936
+ await settleAppliedFiles(qb, dialect, lastCreatedMigrationFiles());
18740
18937
  } else {
18741
18938
  info("-- No changes; nothing to apply");
18742
18939
  }
18743
18940
  } catch (err) {
18744
18941
  console.error("-- Migration failed:", err);
18745
18942
  throw err;
18943
+ } finally {
18944
+ rmSync(dirPath, { recursive: true, force: true });
18746
18945
  }
18747
18946
  }
18748
18947
  if (!opts.dryRun)
@@ -18751,9 +18950,6 @@ async function generateMigration(dir, opts = {}) {
18751
18950
  }
18752
18951
  async function executeMigration(dir) {
18753
18952
  await getConfig();
18754
- if (!dir) {
18755
- dir = join9(process25.cwd(), "app/Models");
18756
- }
18757
18953
  const workspaceRoot = getWorkspaceRoot();
18758
18954
  const sqlDir = ensureSqlDirectory(workspaceRoot);
18759
18955
  const dialect = config5.dialect || "postgres";
@@ -18768,42 +18964,25 @@ async function executeMigration(dir) {
18768
18964
  const qb = createQueryBuilder();
18769
18965
  await createMigrationsTable(qb, dialect);
18770
18966
  const executedMigrations = await getExecutedMigrations(qb, dialect);
18771
- const permanentMigrations = [];
18772
- const transientMigrations = [];
18773
- for (const file2 of scriptFiles) {
18774
- if (file2.includes("alter-") && file2.includes("-table")) {
18775
- transientMigrations.push(file2);
18776
- } else if (!executedMigrations.includes(file2)) {
18777
- permanentMigrations.push(file2);
18778
- }
18779
- }
18780
- const totalPending = permanentMigrations.length + transientMigrations.length;
18781
- if (totalPending === 0) {
18967
+ const pending = scriptFiles.filter((file2) => !executedMigrations.includes(file2));
18968
+ if (pending.length === 0) {
18782
18969
  info("-- No pending migrations to execute");
18783
18970
  return true;
18784
18971
  }
18785
- info(`-- Executing ${totalPending} migrations (${permanentMigrations.length} permanent, ${transientMigrations.length} transient)`);
18786
- for (const file2 of permanentMigrations) {
18787
- const filePath = join9(sqlDir, file2);
18972
+ info(`-- Executing ${pending.length} migration${pending.length === 1 ? "" : "s"}`);
18973
+ for (const file2 of pending) {
18974
+ const filePath = join10(sqlDir, file2);
18788
18975
  info(`-- Executing: ${file2}`);
18789
18976
  try {
18790
18977
  await qb.file(filePath);
18791
18978
  await recordMigration(qb, file2, dialect);
18792
18979
  info(`-- \u2713 Migration ${file2} executed and recorded`);
18793
18980
  } catch (err) {
18794
- console.error(`-- \u2717 Migration ${file2} failed:`, err);
18795
- throw err;
18796
- }
18797
- }
18798
- for (const file2 of transientMigrations) {
18799
- const filePath = join9(sqlDir, file2);
18800
- info(`-- Executing: ${file2} (transient)`);
18801
- try {
18802
- await qb.file(filePath);
18803
- info(`-- \u2713 Migration ${file2} executed (not recorded)`);
18804
- unlinkSync(filePath);
18805
- info(`-- \uD83D\uDDD1\uFE0F Deleted transient migration: ${file2}`);
18806
- } catch (err) {
18981
+ if (isGeneratedMigration(sqlDir, file2) && isAlreadyAppliedError(err)) {
18982
+ await recordMigration(qb, file2, dialect);
18983
+ info(`-- \u2713 Migration ${file2} was already applied; recorded it (${err instanceof Error ? err.message : String(err)})`);
18984
+ continue;
18985
+ }
18807
18986
  console.error(`-- \u2717 Migration ${file2} failed:`, err);
18808
18987
  throw err;
18809
18988
  }
@@ -18818,7 +18997,7 @@ async function executeMigration(dir) {
18818
18997
  async function resetDatabase(dir, opts = {}) {
18819
18998
  await getConfig();
18820
18999
  if (!dir) {
18821
- dir = join9(process25.cwd(), "app/Models");
19000
+ dir = join10(process25.cwd(), "app/Models");
18822
19001
  }
18823
19002
  const dialect = opts.dialect || config5.dialect || "postgres";
18824
19003
  const driver = getDialectDriver(dialect);
@@ -18915,42 +19094,50 @@ async function resetDatabase(dir, opts = {}) {
18915
19094
  }
18916
19095
  async function deleteMigrationFiles(dir, workspaceRoot, opts = {}) {
18917
19096
  if (!dir) {
18918
- dir = join9(process25.cwd(), "app/Models");
19097
+ dir = join10(process25.cwd(), "app/Models");
18919
19098
  }
18920
19099
  if (!workspaceRoot) {
18921
19100
  workspaceRoot = getWorkspaceRoot();
18922
19101
  }
18923
19102
  const dialect = String(opts.dialect || config5.dialect || "postgres");
18924
19103
  const snapshotPath = getSnapshotPath(workspaceRoot, dialect);
18925
- if (existsSync16(snapshotPath)) {
19104
+ if (existsSync17(snapshotPath)) {
18926
19105
  unlinkSync(snapshotPath);
18927
19106
  info(`-- Removed model snapshot file: ${snapshotPath}`);
18928
19107
  }
18929
- const defaultStatePath = join9(dir, `.qb-migrations.${dialect}.json`);
19108
+ const defaultStatePath = join10(dir, `.qb-migrations.${dialect}.json`);
18930
19109
  const statePath = String(opts.state || defaultStatePath);
18931
- if (existsSync16(statePath)) {
19110
+ if (existsSync17(statePath)) {
18932
19111
  unlinkSync(statePath);
18933
19112
  info(`-- Removed legacy migration state file: ${statePath}`);
18934
19113
  }
18935
- const sqlDir = getSqlDirectory(workspaceRoot);
18936
- if (existsSync16(sqlDir)) {
18937
- const sqlFiles = readdirSync6(sqlDir);
18938
- const migrationFiles = sqlFiles.filter((file2) => file2.endsWith(".sql"));
18939
- for (const file2 of migrationFiles) {
18940
- const filePath = join9(sqlDir, file2);
18941
- unlinkSync(filePath);
18942
- info(`-- Removed migration file: ${file2}`);
19114
+ removeGeneratedMigrationFiles(getSqlDirectory(workspaceRoot));
19115
+ }
19116
+ function removeGeneratedMigrationFiles(sqlDir) {
19117
+ if (!existsSync17(sqlDir))
19118
+ return;
19119
+ let removed = 0;
19120
+ const kept = [];
19121
+ for (const file2 of readdirSync6(sqlDir).filter((f) => f.endsWith(".sql"))) {
19122
+ if (!isGeneratedMigration(sqlDir, file2)) {
19123
+ kept.push(file2);
19124
+ continue;
18943
19125
  }
18944
- info(`-- Cleaned up ${migrationFiles.length} migration files from migrations directory`);
19126
+ unlinkSync(join10(sqlDir, file2));
19127
+ info(`-- Removed generated migration file: ${file2}`);
19128
+ removed += 1;
18945
19129
  }
19130
+ info(`-- Cleaned up ${removed} generated migration file${removed === 1 ? "" : "s"} from migrations directory`);
19131
+ if (kept.length > 0)
19132
+ info(`-- Kept ${kept.length} hand-written migration${kept.length === 1 ? "" : "s"}: ${kept.slice(0, 5).join(", ")}${kept.length > 5 ? `, +${kept.length - 5} more` : ""}`);
18946
19133
  }
18947
19134
  async function copyModelsToGenerated(_dir, _workspaceRoot) {}
18948
19135
  async function clearGeneratedDirectory(workspaceRoot) {
18949
19136
  if (!workspaceRoot) {
18950
19137
  workspaceRoot = getWorkspaceRoot();
18951
19138
  }
18952
- const generatedDir = join9(workspaceRoot, "generated");
18953
- if (existsSync16(generatedDir)) {
19139
+ const generatedDir = join10(workspaceRoot, "generated");
19140
+ if (existsSync17(generatedDir)) {
18954
19141
  try {
18955
19142
  rmSync(generatedDir, { recursive: true, force: true });
18956
19143
  info("-- Cleared generated directory");
@@ -18959,12 +19146,6 @@ async function clearGeneratedDirectory(workspaceRoot) {
18959
19146
  }
18960
19147
  }
18961
19148
  }
18962
- function getSqlDirectory(workspaceRoot) {
18963
- if (!workspaceRoot) {
18964
- workspaceRoot = getWorkspaceRoot();
18965
- }
18966
- return join9(workspaceRoot, "database", "migrations");
18967
- }
18968
19149
  async function createMigrationsTable(qb, dialect) {
18969
19150
  const driver = getDialectDriver(dialect);
18970
19151
  const createTableSql = driver.createMigrationsTable();
@@ -19003,6 +19184,7 @@ var init_migrate = __esm(() => {
19003
19184
  init_drivers();
19004
19185
  init_src2();
19005
19186
  init_introspect_db();
19187
+ init_workspace();
19006
19188
  });
19007
19189
 
19008
19190
  // src/actions/migrate-generate.ts
@@ -19014,28 +19196,49 @@ var init_migrate_generate = __esm(() => {
19014
19196
  });
19015
19197
 
19016
19198
  // src/actions/migrate-rollback.ts
19017
- import { existsSync as existsSync17, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
19018
- import { dirname as dirname8, join as join10 } from "path";
19019
- import process26 from "process";
19199
+ import { existsSync as existsSync18, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
19200
+ import { join as join11 } from "path";
19020
19201
  function splitSqlStatements2(sql) {
19021
19202
  const out = [];
19022
19203
  let buf = "";
19023
- let inString = false;
19024
- const lines = sql.split(`
19025
- `).filter((l) => !/^\s*--/.test(l));
19026
- const text = lines.join(`
19027
- `);
19028
- for (let i = 0;i < text.length; i++) {
19029
- const ch = text[i];
19030
- if (ch === "'")
19031
- inString = !inString;
19032
- if (ch === ";" && !inString) {
19204
+ let quote = null;
19205
+ for (let i = 0;i < sql.length; i++) {
19206
+ const ch = sql[i];
19207
+ if (quote) {
19208
+ buf += ch;
19209
+ if (ch === quote) {
19210
+ if (sql[i + 1] === quote) {
19211
+ buf += sql[i + 1];
19212
+ i++;
19213
+ } else {
19214
+ quote = null;
19215
+ }
19216
+ }
19217
+ continue;
19218
+ }
19219
+ if (ch === "'" || ch === '"' || ch === "`") {
19220
+ quote = ch;
19221
+ buf += ch;
19222
+ continue;
19223
+ }
19224
+ if (ch === "-" && sql[i + 1] === "-") {
19225
+ const newline = sql.indexOf(`
19226
+ `, i);
19227
+ i = newline === -1 ? sql.length : newline;
19228
+ continue;
19229
+ }
19230
+ if (ch === "/" && sql[i + 1] === "*") {
19231
+ const close = sql.indexOf("*/", i + 2);
19232
+ i = close === -1 ? sql.length : close + 1;
19233
+ continue;
19234
+ }
19235
+ if (ch === ";") {
19033
19236
  if (buf.trim())
19034
19237
  out.push(buf.trim());
19035
19238
  buf = "";
19036
- } else {
19037
- buf += ch;
19239
+ continue;
19038
19240
  }
19241
+ buf += ch;
19039
19242
  }
19040
19243
  if (buf.trim())
19041
19244
  out.push(buf.trim());
@@ -19049,8 +19252,20 @@ function deriveDownStatements(forwardSql, dialect = config5.dialect) {
19049
19252
  let m;
19050
19253
  if (m = /^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`']?(\w+)["`']?/i.exec(stmt)) {
19051
19254
  down.push(`DROP TABLE IF EXISTS ${q(m[1])}`);
19052
- } else if (m = /^ALTER\s+TABLE\s+["`']?(\w+)["`']?\s+ADD\s+(?:COLUMN\s+)?["`']?(\w+)["`']?/i.exec(stmt)) {
19053
- down.push(`ALTER TABLE ${q(m[1])} DROP COLUMN ${q(m[2])}`);
19255
+ } else if (m = /^ALTER\s+TABLE\s+["`']?(\w+)["`']?\s+ADD\s+CONSTRAINT\s+["`']?(\w+)["`']?([\s\S]*)$/i.exec(stmt)) {
19256
+ if (dialect === "sqlite") {
19257
+ skipped.push(stmt);
19258
+ } else if (isMysqlLike(dialect) && /\bFOREIGN\s+KEY\b/i.test(m[3] ?? "")) {
19259
+ down.push(`ALTER TABLE ${q(m[1])} DROP FOREIGN KEY ${q(m[2])}`);
19260
+ } else {
19261
+ down.push(`ALTER TABLE ${q(m[1])} DROP CONSTRAINT ${q(m[2])}`);
19262
+ }
19263
+ } else if (m = /^ALTER\s+TABLE\s+["`']?(\w+)["`']?\s+ADD\s+(?:COLUMN\s+["`']?(\w+)["`']?|["`'](\w+)["`']|(\w+))/i.exec(stmt)) {
19264
+ const column = m[2] ?? m[3] ?? m[4];
19265
+ if (m[4] && /^(?:PRIMARY|UNIQUE|INDEX|KEY|FOREIGN|CHECK|FULLTEXT|SPATIAL)$/i.test(m[4]))
19266
+ skipped.push(stmt);
19267
+ else
19268
+ down.push(`ALTER TABLE ${q(m[1])} DROP COLUMN ${q(column)}`);
19054
19269
  } else if (m = /^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`']?(\w+)["`']?(?:\s+ON\s+["`']?(\w+)["`']?)?/i.exec(stmt)) {
19055
19270
  down.push(isMysqlLike(dialect) && m[2] ? `DROP INDEX ${q(m[1])} ON ${q(m[2])}` : `DROP INDEX IF EXISTS ${q(m[1])}`);
19056
19271
  } else {
@@ -19059,22 +19274,6 @@ function deriveDownStatements(forwardSql, dialect = config5.dialect) {
19059
19274
  }
19060
19275
  return { down: down.reverse(), skipped };
19061
19276
  }
19062
- function findWorkspaceRoot2(startPath) {
19063
- let currentPath = startPath;
19064
- while (currentPath !== dirname8(currentPath)) {
19065
- if (existsSync17(join10(currentPath, "package.json"))) {
19066
- return currentPath;
19067
- }
19068
- currentPath = dirname8(currentPath);
19069
- }
19070
- return process26.cwd();
19071
- }
19072
- function getSqlDirectory2(workspaceRoot) {
19073
- if (!workspaceRoot) {
19074
- workspaceRoot = findWorkspaceRoot2(process26.cwd());
19075
- }
19076
- return join10(workspaceRoot, "database", "migrations");
19077
- }
19078
19277
  async function migrateRollback(options = {}) {
19079
19278
  const steps = options.steps || 1;
19080
19279
  const reverseSchema = options.reverseSchema !== false;
@@ -19107,12 +19306,12 @@ async function migrateRollback(options = {}) {
19107
19306
  console.log(` - ${migration.migration}`);
19108
19307
  }
19109
19308
  console.log();
19110
- const sqlDir = getSqlDirectory2();
19309
+ const sqlDir = getSqlDirectory();
19111
19310
  let reversedAny = false;
19112
19311
  for (const migration of migrationsToRollback) {
19113
19312
  try {
19114
- const filePath = join10(sqlDir, migration.migration);
19115
- if (reverseSchema && existsSync17(filePath)) {
19313
+ const filePath = join11(sqlDir, migration.migration);
19314
+ if (reverseSchema && existsSync18(filePath)) {
19116
19315
  const forwardSql = readFileSync4(filePath, "utf8");
19117
19316
  const { down, skipped } = deriveDownStatements(forwardSql);
19118
19317
  for (const stmt of down) {
@@ -19123,10 +19322,10 @@ async function migrateRollback(options = {}) {
19123
19322
  if (skipped.length > 0)
19124
19323
  console.log(`-- \u26A0\uFE0F ${skipped.length} statement(s) in ${migration.migration} could not be auto-reversed (data/complex DDL) \u2014 reverse manually.`);
19125
19324
  }
19126
- const deleteSql = `DELETE FROM migrations WHERE migration = $1`;
19325
+ const deleteSql = `DELETE FROM migrations WHERE migration = ${getPlaceholder(1)}`;
19127
19326
  await qb.unsafe(deleteSql, [migration.migration]);
19128
19327
  console.log(`-- \u2713 Removed migration record: ${migration.migration}`);
19129
- if (existsSync17(filePath)) {
19328
+ if (options.deleteFiles && existsSync18(filePath)) {
19130
19329
  unlinkSync2(filePath);
19131
19330
  console.log(`-- \uD83D\uDDD1\uFE0F Deleted migration file: ${migration.migration}`);
19132
19331
  }
@@ -19149,37 +19348,20 @@ async function migrateRollback(options = {}) {
19149
19348
  var init_migrate_rollback = __esm(() => {
19150
19349
  init_config();
19151
19350
  init_src2();
19351
+ init_workspace();
19152
19352
  });
19153
19353
 
19154
19354
  // src/actions/migrate-status.ts
19155
- import { existsSync as existsSync18, readdirSync as readdirSync8 } from "fs";
19156
- import { dirname as dirname9, join as join11 } from "path";
19157
- import process27 from "process";
19158
- function findWorkspaceRoot3(startPath) {
19159
- let currentPath = startPath;
19160
- while (currentPath !== dirname9(currentPath)) {
19161
- if (existsSync18(join11(currentPath, "package.json"))) {
19162
- return currentPath;
19163
- }
19164
- currentPath = dirname9(currentPath);
19165
- }
19166
- return process27.cwd();
19167
- }
19168
- function getSqlDirectory3(workspaceRoot) {
19169
- if (!workspaceRoot) {
19170
- workspaceRoot = findWorkspaceRoot3(process27.cwd());
19171
- }
19172
- return join11(workspaceRoot, "database", "migrations");
19173
- }
19355
+ import { existsSync as existsSync19, readdirSync as readdirSync8 } from "fs";
19174
19356
  async function migrateStatus() {
19175
19357
  const dialect = config5.dialect || "postgres";
19176
19358
  const driver = getDialectDriver(dialect);
19177
- const sqlDir = getSqlDirectory3();
19359
+ const sqlDir = getSqlDirectory();
19178
19360
  console.log("-- Migration Status");
19179
19361
  console.log(`-- Dialect: ${dialect}`);
19180
19362
  console.log(`-- SQL directory: ${sqlDir}`);
19181
19363
  console.log();
19182
- if (!existsSync18(sqlDir)) {
19364
+ if (!existsSync19(sqlDir)) {
19183
19365
  console.log("-- No SQL directory found. No migrations have been created yet.");
19184
19366
  return [];
19185
19367
  }
@@ -19200,27 +19382,16 @@ async function migrateStatus() {
19200
19382
  console.log();
19201
19383
  }
19202
19384
  const executedMap = new Map(executedMigrations.map((m) => [m.migration, m.executed_at]));
19203
- const statuses = migrationFiles.map((file2) => {
19204
- if (file2.includes("alter-") && file2.includes("-table")) {
19205
- return {
19206
- file: file2,
19207
- status: "transient"
19208
- };
19209
- }
19210
- const executedAt = executedMap.get(file2);
19211
- return {
19212
- file: file2,
19213
- status: executedAt ? "executed" : "pending",
19214
- executedAt
19215
- };
19216
- });
19385
+ const statuses = migrationFiles.map((file2) => ({
19386
+ file: file2,
19387
+ status: executedMap.has(file2) ? "executed" : "pending",
19388
+ executedAt: executedMap.get(file2)
19389
+ }));
19217
19390
  const executed = statuses.filter((s) => s.status === "executed");
19218
19391
  const pending = statuses.filter((s) => s.status === "pending");
19219
- const transient = statuses.filter((s) => s.status === "transient");
19220
19392
  console.log(`-- Total migrations: ${migrationFiles.length}`);
19221
19393
  console.log(`-- Executed: ${executed.length}`);
19222
19394
  console.log(`-- Pending: ${pending.length}`);
19223
- console.log(`-- Transient: ${transient.length}`);
19224
19395
  console.log();
19225
19396
  if (executed.length > 0) {
19226
19397
  console.log("\u2713 Executed Migrations:");
@@ -19236,13 +19407,6 @@ async function migrateStatus() {
19236
19407
  }
19237
19408
  console.log();
19238
19409
  }
19239
- if (transient.length > 0) {
19240
- console.log("\u26A1 Transient Migrations (ALTER TABLE - not tracked):");
19241
- for (const migration of transient) {
19242
- console.log(` - ${migration.file}`);
19243
- }
19244
- console.log();
19245
- }
19246
19410
  return statuses;
19247
19411
  } catch (err) {
19248
19412
  console.error("-- Failed to get migration status:", err);
@@ -19256,14 +19420,15 @@ var init_migrate_status = __esm(() => {
19256
19420
  init_config();
19257
19421
  init_drivers();
19258
19422
  init_src2();
19423
+ init_workspace();
19259
19424
  });
19260
19425
 
19261
19426
  // src/actions/model-show.ts
19262
19427
  import { readdirSync as readdirSync9 } from "fs";
19263
19428
  import { extname as extname2, join as join12 } from "path";
19264
- import process28 from "process";
19429
+ import process26 from "process";
19265
19430
  async function modelShow(modelName, options = {}) {
19266
- const dir = options.dir || join12(process28.cwd(), "app/Models");
19431
+ const dir = options.dir || join12(process26.cwd(), "app/Models");
19267
19432
  try {
19268
19433
  const files = readdirSync9(dir);
19269
19434
  const modelFile = files.find((f) => {
@@ -19477,9 +19642,9 @@ var init_query_explain_all = __esm(() => {
19477
19642
  // src/actions/relation-diagram.ts
19478
19643
  import { writeFileSync as writeFileSync12 } from "fs";
19479
19644
  import { join as join14 } from "path";
19480
- import process29 from "process";
19645
+ import process27 from "process";
19481
19646
  async function relationDiagram(options = {}) {
19482
- const dir = options.dir || join14(process29.cwd(), "app/Models");
19647
+ const dir = options.dir || join14(process27.cwd(), "app/Models");
19483
19648
  const format = options.format || "mermaid";
19484
19649
  try {
19485
19650
  const models = await loadModels({ modelsDir: dir });
@@ -19599,25 +19764,25 @@ var init_relation_diagram = __esm(() => {
19599
19764
  });
19600
19765
 
19601
19766
  // src/actions/seed.ts
19602
- import { existsSync as existsSync19, mkdirSync as mkdirSync9, readdirSync as readdirSync11, writeFileSync as writeFileSync13 } from "fs";
19767
+ import { existsSync as existsSync20, mkdirSync as mkdirSync9, readdirSync as readdirSync11, writeFileSync as writeFileSync13 } from "fs";
19603
19768
  import { dirname as dirname10, join as join15 } from "path";
19604
- import process30 from "process";
19605
- function findWorkspaceRoot4(startPath) {
19769
+ import process28 from "process";
19770
+ function findWorkspaceRoot3(startPath) {
19606
19771
  let currentPath = startPath;
19607
19772
  while (currentPath !== dirname10(currentPath)) {
19608
- if (existsSync19(join15(currentPath, "package.json"))) {
19773
+ if (existsSync20(join15(currentPath, "package.json"))) {
19609
19774
  return currentPath;
19610
19775
  }
19611
19776
  currentPath = dirname10(currentPath);
19612
19777
  }
19613
- return process30.cwd();
19778
+ return process28.cwd();
19614
19779
  }
19615
19780
  async function loadSeeders(seedersDir) {
19616
- if (!existsSync19(seedersDir)) {
19781
+ if (!existsSync20(seedersDir)) {
19617
19782
  console.log(`-- Seeders directory not found: ${seedersDir}`);
19618
19783
  return [];
19619
19784
  }
19620
- const files = readdirSync11(seedersDir);
19785
+ const files = readdirSync11(seedersDir).sort((a, b) => a.localeCompare(b));
19621
19786
  const seederFiles = files.filter((file2) => (file2.endsWith(".ts") || file2.endsWith(".js")) && file2 !== "index.ts" && file2 !== "index.js");
19622
19787
  const seeders = [];
19623
19788
  for (const file2 of seederFiles) {
@@ -19630,6 +19795,7 @@ async function loadSeeders(seedersDir) {
19630
19795
  if (instance && typeof instance.run === "function") {
19631
19796
  seeders.push({
19632
19797
  name: file2.replace(/\.(ts|js)$/, ""),
19798
+ className: String(SeederClass.name || ""),
19633
19799
  instance
19634
19800
  });
19635
19801
  }
@@ -19642,7 +19808,7 @@ async function loadSeeders(seedersDir) {
19642
19808
  return seeders;
19643
19809
  }
19644
19810
  async function runSeeders(config6 = {}) {
19645
- const workspaceRoot = findWorkspaceRoot4(process30.cwd());
19811
+ const workspaceRoot = findWorkspaceRoot3(process28.cwd());
19646
19812
  const seedersDir = config6.seedersDir || join15(workspaceRoot, "database/seeders");
19647
19813
  const verbose = config6.verbose ?? true;
19648
19814
  if (verbose) {
@@ -19677,17 +19843,18 @@ async function runSeeders(config6 = {}) {
19677
19843
  }
19678
19844
  }
19679
19845
  async function runSeeder(className, options = {}) {
19680
- const workspaceRoot = findWorkspaceRoot4(process30.cwd());
19846
+ const workspaceRoot = findWorkspaceRoot3(process28.cwd());
19681
19847
  const seedersDir = join15(workspaceRoot, "database/seeders");
19682
19848
  const verbose = options.verbose ?? true;
19683
19849
  if (verbose) {
19684
19850
  console.log(`-- Running seeder: ${className}`);
19685
19851
  }
19686
19852
  const seeders = await loadSeeders(seedersDir);
19687
- const seeder = seeders.find((s) => s.name === className);
19853
+ const seeder = seeders.find((s) => s.className === className) ?? seeders.find((s) => s.name === className);
19688
19854
  if (!seeder) {
19689
- console.error(`-- Seeder not found: ${className}`);
19690
- throw new Error(`Seeder not found: ${className}`);
19855
+ const known = seeders.map((s) => s.className || s.name).join(", ") || "none";
19856
+ console.error(`-- Seeder not found: ${className}. Available: ${known}`);
19857
+ throw new Error(`Seeder not found: ${className}. Available: ${known}`);
19691
19858
  }
19692
19859
  const qb = createQueryBuilder();
19693
19860
  try {
@@ -19701,9 +19868,9 @@ async function runSeeder(className, options = {}) {
19701
19868
  }
19702
19869
  }
19703
19870
  async function makeSeeder(name) {
19704
- const workspaceRoot = findWorkspaceRoot4(process30.cwd());
19871
+ const workspaceRoot = findWorkspaceRoot3(process28.cwd());
19705
19872
  const seedersDir = join15(workspaceRoot, "database/seeders");
19706
- if (!existsSync19(seedersDir)) {
19873
+ if (!existsSync20(seedersDir)) {
19707
19874
  mkdirSync9(seedersDir, { recursive: true });
19708
19875
  console.log(`-- Created seeders directory: ${seedersDir}`);
19709
19876
  }
@@ -19711,7 +19878,7 @@ async function makeSeeder(name) {
19711
19878
  const className = `${baseName}Seeder`;
19712
19879
  const fileName = `${className}.ts`;
19713
19880
  const filePath = join15(seedersDir, fileName);
19714
- if (existsSync19(filePath)) {
19881
+ if (existsSync20(filePath)) {
19715
19882
  console.error(`-- Seeder already exists: ${filePath}`);
19716
19883
  throw new Error(`Seeder already exists: ${filePath}`);
19717
19884
  }
@@ -19765,7 +19932,7 @@ export default class ${className} extends Seeder {
19765
19932
  console.log(`-- \u2713 Created seeder: ${filePath}`);
19766
19933
  }
19767
19934
  async function freshDatabase(options = {}) {
19768
- const workspaceRoot = findWorkspaceRoot4(process30.cwd());
19935
+ const workspaceRoot = findWorkspaceRoot3(process28.cwd());
19769
19936
  const modelsDir = options.modelsDir || join15(workspaceRoot, "app/Models");
19770
19937
  const seedersDir = options.seedersDir || join15(workspaceRoot, "database/seeders");
19771
19938
  const verbose = options.verbose ?? true;
@@ -19825,22 +19992,22 @@ var init_unsafe = __esm(() => {
19825
19992
  });
19826
19993
 
19827
19994
  // src/actions/validate.ts
19828
- import { existsSync as existsSync20 } from "fs";
19995
+ import { existsSync as existsSync21 } from "fs";
19829
19996
  import { dirname as dirname11, join as join16 } from "path";
19830
- import process31 from "process";
19831
- function findWorkspaceRoot5(startPath) {
19997
+ import process29 from "process";
19998
+ function findWorkspaceRoot4(startPath) {
19832
19999
  let currentPath = startPath;
19833
20000
  while (currentPath !== dirname11(currentPath)) {
19834
- if (existsSync20(join16(currentPath, "package.json"))) {
20001
+ if (existsSync21(join16(currentPath, "package.json"))) {
19835
20002
  return currentPath;
19836
20003
  }
19837
20004
  currentPath = dirname11(currentPath);
19838
20005
  }
19839
- return process31.cwd();
20006
+ return process29.cwd();
19840
20007
  }
19841
20008
  async function validateSchema(dir) {
19842
20009
  if (!dir) {
19843
- dir = join16(findWorkspaceRoot5(process31.cwd()), "app/Models");
20010
+ dir = join16(findWorkspaceRoot4(process29.cwd()), "app/Models");
19844
20011
  }
19845
20012
  const dialect = config5.dialect || "postgres";
19846
20013
  console.log("-- Validating Schema");
@@ -25091,18 +25258,11 @@ function toSnakeCase(str) {
25091
25258
  snakeCaseCache.set(str, cached);
25092
25259
  return cached;
25093
25260
  }
25094
- function toTableName(modelName) {
25261
+ function toTableName2(modelName) {
25095
25262
  let cached = tableNameCache.get(modelName);
25096
25263
  if (cached !== undefined)
25097
25264
  return cached;
25098
- const snake = toSnakeCase(modelName);
25099
- if (snake.endsWith("y") && !snake.endsWith("ay") && !snake.endsWith("ey") && !snake.endsWith("oy") && !snake.endsWith("uy")) {
25100
- cached = snake.slice(0, -1) + "ies";
25101
- } else if (snake.endsWith("s") || snake.endsWith("x") || snake.endsWith("ch") || snake.endsWith("sh")) {
25102
- cached = `${snake}es`;
25103
- } else {
25104
- cached = `${snake}s`;
25105
- }
25265
+ cached = toTableName(modelName);
25106
25266
  tableNameCache.set(modelName, cached);
25107
25267
  return cached;
25108
25268
  }
@@ -25187,14 +25347,14 @@ function resolveRelation(definition, relationName) {
25187
25347
  const hasManyModel = findModelName(definition.hasMany);
25188
25348
  if (hasManyModel) {
25189
25349
  const relatedModel = getModelFromRegistry(hasManyModel);
25190
- const relatedTable = relatedModel?.getTable?.() || toTableName(hasManyModel);
25350
+ const relatedTable = relatedModel?.getTable?.() || toTableName2(hasManyModel);
25191
25351
  const foreignKey = toSnakeCase(parentName) + "_id";
25192
25352
  return { type: "hasMany", relatedModelName: hasManyModel, relatedTable, foreignKey, localKey: parentPk };
25193
25353
  }
25194
25354
  const hasOneModel = findModelName(definition.hasOne);
25195
25355
  if (hasOneModel) {
25196
25356
  const relatedModel = getModelFromRegistry(hasOneModel);
25197
- const relatedTable = relatedModel?.getTable?.() || toTableName(hasOneModel);
25357
+ const relatedTable = relatedModel?.getTable?.() || toTableName2(hasOneModel);
25198
25358
  const foreignKey = toSnakeCase(parentName) + "_id";
25199
25359
  return { type: "hasOne", relatedModelName: hasOneModel, relatedTable, foreignKey, localKey: parentPk };
25200
25360
  }
@@ -25202,7 +25362,7 @@ function resolveRelation(definition, relationName) {
25202
25362
  if (belongsToEntry) {
25203
25363
  const belongsToModel = belongsToEntry.model;
25204
25364
  const relatedModel = getModelFromRegistry(belongsToModel);
25205
- const relatedTable = relatedModel?.getTable?.() || toTableName(belongsToModel);
25365
+ const relatedTable = relatedModel?.getTable?.() || toTableName2(belongsToModel);
25206
25366
  const relatedPk = relatedModel?.getDefinition?.()?.primaryKey || "id";
25207
25367
  const foreignKey = belongsToEntry.foreignKey || `${toSnakeCase(belongsToModel)}_id`;
25208
25368
  return { type: "belongsTo", relatedModelName: belongsToModel, relatedTable, foreignKey, localKey: relatedPk };
@@ -25212,7 +25372,7 @@ function resolveRelation(definition, relationName) {
25212
25372
  const isConfig = typeof found.entry === "object";
25213
25373
  const relatedModelName = isConfig ? found.entry.model : found.entry;
25214
25374
  const relatedModel = getModelFromRegistry(relatedModelName);
25215
- const relatedTable = relatedModel?.getTable?.() || toTableName(relatedModelName);
25375
+ const relatedTable = relatedModel?.getTable?.() || toTableName2(relatedModelName);
25216
25376
  const config6 = isConfig ? found.entry : null;
25217
25377
  let pivotTable;
25218
25378
  let pivotModelName;
@@ -25224,7 +25384,7 @@ function resolveRelation(definition, relationName) {
25224
25384
  if (!throughModel) {
25225
25385
  throw new Error(`[orm] belongsToMany relation '${relationName}' on '${parentName}' references unknown through model '${config6.through}'. Make sure '${config6.through}' is registered.`);
25226
25386
  }
25227
- pivotTable = throughModel.getTable?.() || throughModel.getDefinition?.()?.table || toTableName(config6.through);
25387
+ pivotTable = throughModel.getTable?.() || throughModel.getDefinition?.()?.table || toTableName2(config6.through);
25228
25388
  } else {
25229
25389
  const a2 = singularizeWord(parentTable);
25230
25390
  const b2 = singularizeWord(relatedTable);
@@ -25275,10 +25435,10 @@ function resolveRelation(definition, relationName) {
25275
25435
  return null;
25276
25436
  const throughModel = getModelFromRegistry(throughModelName);
25277
25437
  const throughDef = throughModel?.getDefinition?.() || throughModel?.definition;
25278
- const throughTable = throughModel?.getTable?.() || throughDef?.table || toTableName(throughModelName);
25438
+ const throughTable = throughModel?.getTable?.() || throughDef?.table || toTableName2(throughModelName);
25279
25439
  const throughPk = throughDef?.primaryKey || "id";
25280
25440
  const targetModel = getModelFromRegistry(targetModelName);
25281
- const targetTable = targetModel?.getTable?.() || toTableName(targetModelName);
25441
+ const targetTable = targetModel?.getTable?.() || toTableName2(targetModelName);
25282
25442
  return {
25283
25443
  type,
25284
25444
  relatedModelName: targetModelName,
@@ -27875,7 +28035,7 @@ function buildDatabaseSchema(models) {
27875
28035
  for (const modelName of Object.keys(models)) {
27876
28036
  const rawModel = models[modelName];
27877
28037
  const m2 = rawModel.definition ?? rawModel.getDefinition?.() ?? rawModel;
27878
- const table = m2.table || `${String(m2.name).toLowerCase()}s`;
28038
+ const table = tableNameFor(m2);
27879
28039
  const attrs = m2.attributes ?? {};
27880
28040
  const columns = {};
27881
28041
  for (const key of Object.keys(attrs))
@@ -27887,30 +28047,53 @@ function buildDatabaseSchema(models) {
27887
28047
  }
27888
28048
  return schema;
27889
28049
  }
28050
+ var init_factory = () => {};
27890
28051
 
27891
28052
  // src/loader.ts
27892
- import { readdirSync as readdirSync12, statSync as statSync5 } from "fs";
27893
- import { basename, extname as extname4 } from "path";
27894
- import process33 from "process";
27895
- async function loadModels(options) {
27896
- const cwd = options.cwd ?? process33.cwd();
27897
- const dir = options.modelsDir.startsWith("/") ? options.modelsDir : `${cwd}/${options.modelsDir}`;
27898
- const result = {};
27899
- const entries = readdirSync12(dir);
28053
+ import { readdirSync as readdirSync12 } from "fs";
28054
+ import { basename, extname as extname4, isAbsolute as isAbsolute3, join as join17 } from "path";
28055
+ import process30 from "process";
28056
+ function isModelFile(name) {
28057
+ const ext = extname4(name);
28058
+ if (!MODEL_EXTENSIONS.has(ext))
28059
+ return false;
28060
+ const base = basename(name, ext);
28061
+ if (base.startsWith("_") || base === "index")
28062
+ return false;
28063
+ return !base.endsWith(".d");
28064
+ }
28065
+ function isModelDefinition(def) {
28066
+ if (!def || typeof def !== "object")
28067
+ return false;
28068
+ const d2 = def;
28069
+ return typeof d2.name === "string" || typeof d2.table === "string";
28070
+ }
28071
+ function collectModelFiles(dir) {
28072
+ const files = [];
28073
+ const entries = readdirSync12(dir, { withFileTypes: true }).sort((a2, b2) => a2.name.localeCompare(b2.name));
27900
28074
  for (const entry of entries) {
27901
- const full = `${dir}/${entry}`;
27902
- const st = statSync5(full);
27903
- if (st.isDirectory())
27904
- continue;
27905
- const ext = extname4(full);
27906
- if (![".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"].includes(ext))
28075
+ const full = join17(dir, entry.name);
28076
+ if (entry.isDirectory()) {
28077
+ files.push(...collectModelFiles(full));
27907
28078
  continue;
28079
+ }
28080
+ if (isModelFile(entry.name))
28081
+ files.push(full);
28082
+ }
28083
+ return files;
28084
+ }
28085
+ async function loadModels(options) {
28086
+ const cwd = options.cwd ?? process30.cwd();
28087
+ const dir = isAbsolute3(options.modelsDir) ? options.modelsDir : join17(cwd, options.modelsDir);
28088
+ const result = {};
28089
+ for (const full of collectModelFiles(dir)) {
27908
28090
  const cacheBuster = `?t=${Date.now()}-${Math.random().toString(36).slice(2)}`;
27909
28091
  const mod = await import(`${full}${cacheBuster}`);
27910
28092
  const rawDef = mod.default ?? mod;
27911
28093
  const def = rawDef.definition ?? rawDef.getDefinition?.() ?? rawDef;
27912
- const fileName = basename(entry, ext);
27913
- const name = def.name ?? fileName;
28094
+ if (!isModelDefinition(def))
28095
+ continue;
28096
+ const name = def.name ?? basename(full, extname4(full));
27914
28097
  result[name] = {
27915
28098
  ...def,
27916
28099
  name
@@ -27918,7 +28101,10 @@ async function loadModels(options) {
27918
28101
  }
27919
28102
  return result;
27920
28103
  }
27921
- var init_loader = () => {};
28104
+ var MODEL_EXTENSIONS;
28105
+ var init_loader = __esm(() => {
28106
+ MODEL_EXTENSIONS = new Set([".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"]);
28107
+ });
27922
28108
 
27923
28109
  // src/meta.ts
27924
28110
  function buildSchemaMeta(models) {
@@ -27930,7 +28116,7 @@ function buildSchemaMeta(models) {
27930
28116
  for (const name of Object.keys(models)) {
27931
28117
  const rawModel = models[name];
27932
28118
  const m2 = rawModel.definition ?? rawModel.getDefinition?.() ?? rawModel;
27933
- const table = m2.table || `${String(m2.name).toLowerCase()}s`;
28119
+ const table = tableNameFor(m2);
27934
28120
  modelToTable[name] = table;
27935
28121
  tableToModel[table] = name;
27936
28122
  primaryKeys[table] = m2.primaryKey ?? "id";
@@ -28004,9 +28190,9 @@ function buildSchemaMeta(models) {
28004
28190
  var init_meta = () => {};
28005
28191
 
28006
28192
  // src/migrations.ts
28007
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, writeFileSync as writeFileSync14 } from "fs";
28008
- import { dirname as dirname12, join as join17 } from "path";
28009
- import process35 from "process";
28193
+ import { existsSync as existsSync23, mkdirSync as mkdirSync10, readdirSync as readdirSync13, readFileSync as readFileSync6, writeFileSync as writeFileSync14 } from "fs";
28194
+ import { dirname as dirname12, join as join18 } from "path";
28195
+ import process31 from "process";
28010
28196
  function info2(message) {
28011
28197
  if (config5.verbose)
28012
28198
  console.log(message);
@@ -28017,36 +28203,80 @@ function snakeCase(str) {
28017
28203
  function singularizeTable(table) {
28018
28204
  return singularizerFor(config5.relations?.singularizeStrategy)(table);
28019
28205
  }
28020
- function findWorkspaceRoot6(startPath) {
28206
+ function lastCreatedMigrationFiles() {
28207
+ return [...createdMigrationFiles];
28208
+ }
28209
+ function findWorkspaceRoot5(startPath) {
28021
28210
  let currentPath = startPath;
28022
28211
  while (currentPath !== dirname12(currentPath)) {
28023
- if (existsSync21(join17(currentPath, "package.json"))) {
28212
+ if (existsSync23(join18(currentPath, "package.json"))) {
28024
28213
  return currentPath;
28025
28214
  }
28026
28215
  currentPath = dirname12(currentPath);
28027
28216
  }
28028
- return process35.cwd();
28217
+ return process31.cwd();
28029
28218
  }
28030
28219
  function ensureSqlDirectory2() {
28031
- const workspaceRoot = findWorkspaceRoot6(process35.cwd());
28032
- const sqlDir = join17(workspaceRoot, "database", "migrations");
28033
- if (!existsSync21(sqlDir)) {
28220
+ const workspaceRoot = findWorkspaceRoot5(process31.cwd());
28221
+ const sqlDir = join18(workspaceRoot, "database", "migrations");
28222
+ if (!existsSync23(sqlDir)) {
28034
28223
  mkdirSync10(sqlDir, { recursive: true });
28035
28224
  info2(`-- Created SQL directory: ${sqlDir}`);
28036
28225
  }
28037
28226
  return sqlDir;
28038
28227
  }
28228
+ function isGeneratedMigrationSql(sql2) {
28229
+ return sql2.trimStart().startsWith(GENERATED_MARKER);
28230
+ }
28231
+ function migrationBody(sql2) {
28232
+ return sql2.split(`
28233
+ `).filter((line) => !line.trimStart().startsWith("--")).join(`
28234
+ `).replace(/\s+/g, " ").trim().replace(/;+$/, "");
28235
+ }
28236
+ function existingMigrationBodies(sqlDir) {
28237
+ const bodies = new Set;
28238
+ let files;
28239
+ try {
28240
+ files = readdirSync13(sqlDir).filter((f2) => f2.endsWith(".sql"));
28241
+ } catch {
28242
+ return bodies;
28243
+ }
28244
+ for (const file2 of files) {
28245
+ try {
28246
+ bodies.add(migrationBody(readFileSync6(join18(sqlDir, file2), "utf8")));
28247
+ } catch {}
28248
+ }
28249
+ return bodies;
28250
+ }
28039
28251
  function createMigrationFile(statement, fileName) {
28040
28252
  if (!statement)
28041
28253
  return false;
28042
28254
  const sqlDir = ensureSqlDirectory2();
28255
+ if (existingCorpusBodies === null)
28256
+ existingCorpusBodies = existingMigrationBodies(sqlDir);
28257
+ const body = migrationBody(statement);
28258
+ if (body.length > 0 && existingCorpusBodies.has(body)) {
28259
+ info2(`-- Migration already in the corpus, not writing it again: ${fileName}`);
28260
+ return false;
28261
+ }
28043
28262
  migrationCounter++;
28044
- const sequence = useDeterministicNames ? String(migrationCounter).padStart(10, "0") : String(Math.floor(Date.now() / 1000) + migrationCounter);
28045
- const fullFileName = `${sequence}-${fileName}.sql`;
28046
- const filePath = join17(sqlDir, fullFileName);
28047
- writeFileSync14(filePath, statement);
28263
+ let sequence = useDeterministicNames ? String(migrationCounter).padStart(10, "0") : String(Math.floor(Date.now() / 1000) + migrationCounter);
28264
+ let fullFileName = `${sequence}-${fileName}.sql`;
28265
+ if (!useDeterministicNames) {
28266
+ let next = Number(sequence);
28267
+ while (existsSync23(join18(sqlDir, fullFileName))) {
28268
+ next += 1;
28269
+ sequence = String(next);
28270
+ fullFileName = `${sequence}-${fileName}.sql`;
28271
+ }
28272
+ }
28273
+ const filePath = join18(sqlDir, fullFileName);
28274
+ writeFileSync14(filePath, `${GENERATED_MARKER}
28275
+ ${statement}`);
28048
28276
  info2(`-- Migration file created: ${fullFileName}`);
28049
28277
  migrationsCreatedCount++;
28278
+ createdMigrationFiles.push(fullFileName);
28279
+ existingCorpusBodies?.add(body);
28050
28280
  return true;
28051
28281
  }
28052
28282
  function guessTypeFromName(columnName) {
@@ -28221,7 +28451,7 @@ function buildMigrationPlan2(models, options) {
28221
28451
  const tables = [];
28222
28452
  for (const modelName of Object.keys(models)) {
28223
28453
  const model = models[modelName];
28224
- const table = model.table || `${String(model.name).toLowerCase()}s`;
28454
+ const table = tableNameFor(model);
28225
28455
  const primaryKey = model.primaryKey ?? "id";
28226
28456
  const _autoIncrement = model.autoIncrement !== false;
28227
28457
  const attrs = model.attributes ?? {};
@@ -28237,6 +28467,23 @@ function buildMigrationPlan2(models, options) {
28237
28467
  hasDefault: false
28238
28468
  });
28239
28469
  }
28470
+ const declaredFkTargets = new Map;
28471
+ const declaresBelongsTo = normalizeRelationList(model.belongsTo).length > 0;
28472
+ const columnNames = new Set(Object.keys(attrs).map((name) => snakeCase(name)));
28473
+ const polymorphicColumns = new Set;
28474
+ for (const column of columnNames) {
28475
+ if (!column.endsWith("_type"))
28476
+ continue;
28477
+ const idColumn = `${column.slice(0, -"_type".length)}_id`;
28478
+ if (columnNames.has(idColumn))
28479
+ polymorphicColumns.add(idColumn);
28480
+ }
28481
+ for (const rel of normalizeRelationList(model.belongsTo)) {
28482
+ const column = rel.foreignKey ?? `${snakeCase(rel.model)}_id`;
28483
+ const table2 = meta.modelToTable[rel.model];
28484
+ if (table2)
28485
+ declaredFkTargets.set(column, table2);
28486
+ }
28240
28487
  for (const attrName of Object.keys(attrs)) {
28241
28488
  const attr = attrs[attrName];
28242
28489
  const columnName = snakeCase(attrName);
@@ -28284,7 +28531,7 @@ function buildMigrationPlan2(models, options) {
28284
28531
  enumValues,
28285
28532
  maxLength: typeof maxLen === "number" && maxLen > 0 && maxLen <= 255 ? maxLen : undefined
28286
28533
  };
28287
- if (columnName.endsWith("_id") && attr.foreignKey !== false) {
28534
+ if (columnName.endsWith("_id") && attr.foreignKey !== false && !polymorphicColumns.has(columnName)) {
28288
28535
  if (typeof attr.foreignKey === "object" && attr.foreignKey !== null) {
28289
28536
  const fkConfig = attr.foreignKey;
28290
28537
  col.references = {
@@ -28296,9 +28543,10 @@ function buildMigrationPlan2(models, options) {
28296
28543
  if (fkConfig.nullable !== undefined)
28297
28544
  col.isNullable = fkConfig.nullable;
28298
28545
  } else {
28546
+ const declared = declaredFkTargets.get(columnName);
28299
28547
  const base = columnName.replace(/_id$/, "");
28300
28548
  const maybeModel = base.replace(/(^|_)([a-z])/g, (_2, __, c2) => c2.toUpperCase());
28301
- const refTable = meta.modelToTable[maybeModel];
28549
+ const refTable = declared ?? (declaresBelongsTo ? undefined : meta.modelToTable[maybeModel]);
28302
28550
  if (refTable) {
28303
28551
  const refPk = meta.primaryKeys[refTable] ?? "id";
28304
28552
  col.references = { table: refTable, column: refPk };
@@ -28313,9 +28561,7 @@ function buildMigrationPlan2(models, options) {
28313
28561
  if (columns.some((c2) => c2.name === fkColumnName))
28314
28562
  continue;
28315
28563
  const refTable = meta.modelToTable[rel.model];
28316
- if (!refTable)
28317
- continue;
28318
- const refPk = meta.primaryKeys[refTable] ?? "id";
28564
+ const refPk = refTable ? meta.primaryKeys[refTable] ?? "id" : undefined;
28319
28565
  columns.push({
28320
28566
  name: fkColumnName,
28321
28567
  type: "bigint",
@@ -28323,7 +28569,7 @@ function buildMigrationPlan2(models, options) {
28323
28569
  isUnique: false,
28324
28570
  isNullable: true,
28325
28571
  hasDefault: false,
28326
- references: { table: refTable, column: refPk, onDelete: rel.onDelete }
28572
+ references: refTable && refPk && !polymorphicColumns.has(fkColumnName) ? { table: refTable, column: refPk, onDelete: rel.onDelete } : undefined
28327
28573
  });
28328
28574
  }
28329
28575
  const traits = model.traits;
@@ -28404,7 +28650,7 @@ function buildMigrationPlan2(models, options) {
28404
28650
  for (const modelName of Object.keys(models)) {
28405
28651
  const model = models[modelName];
28406
28652
  const traits = model?.traits ?? {};
28407
- const modelTable = model.table || `${String(model.name).toLowerCase()}s`;
28653
+ const modelTable = tableNameFor(model);
28408
28654
  const singular = singularizeTable(modelTable);
28409
28655
  if (traits.likeable) {
28410
28656
  const opts = typeof traits.likeable === "object" ? traits.likeable : {};
@@ -28424,7 +28670,7 @@ function buildMigrationPlan2(models, options) {
28424
28670
  }
28425
28671
  if (traits.taggable) {
28426
28672
  ensureTable({
28427
- table: "taggable",
28673
+ table: "taggables",
28428
28674
  columns: [
28429
28675
  { name: "id", type: "bigint", isPrimaryKey: true, isUnique: false, isNullable: false, hasDefault: false },
28430
28676
  { name: "name", type: "string", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false },
@@ -28432,37 +28678,61 @@ function buildMigrationPlan2(models, options) {
28432
28678
  { name: "description", type: "text", isPrimaryKey: false, isUnique: false, isNullable: true, hasDefault: false },
28433
28679
  { name: "order", type: "integer", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: true, defaultValue: 0 },
28434
28680
  { name: "is_active", type: "boolean", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: true, defaultValue: true },
28681
+ { name: "taggable_id", type: "bigint", isPrimaryKey: false, isUnique: false, isNullable: true, hasDefault: false },
28682
+ { name: "taggable_type", type: "string", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false },
28683
+ { name: "created_at", type: "datetime", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: true, defaultValue: "CURRENT_TIMESTAMP" },
28684
+ { name: "updated_at", type: "datetime", isPrimaryKey: false, isUnique: false, isNullable: true, hasDefault: false }
28685
+ ],
28686
+ indexes: [
28687
+ { name: "taggables_target_index", columns: ["taggable_id", "taggable_type"], type: "index" },
28688
+ { name: "taggables_type_slug_unique", columns: ["taggable_type", "slug"], type: "unique" }
28689
+ ]
28690
+ });
28691
+ ensureTable({
28692
+ table: "taggable_models",
28693
+ columns: [
28694
+ { name: "id", type: "bigint", isPrimaryKey: true, isUnique: false, isNullable: false, hasDefault: false },
28695
+ { name: "tag_id", type: "bigint", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false, references: { table: "taggables", column: "id" } },
28435
28696
  { name: "taggable_id", type: "bigint", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false },
28436
28697
  { name: "taggable_type", type: "string", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false },
28437
28698
  { name: "created_at", type: "datetime", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: true, defaultValue: "CURRENT_TIMESTAMP" },
28438
28699
  { name: "updated_at", type: "datetime", isPrimaryKey: false, isUnique: false, isNullable: true, hasDefault: false }
28439
28700
  ],
28440
- indexes: [{ name: "taggable_target_index", columns: ["taggable_id", "taggable_type"], type: "index" }]
28701
+ indexes: [
28702
+ { name: "taggable_models_owner_unique", columns: ["tag_id", "taggable_id", "taggable_type"], type: "unique" },
28703
+ { name: "taggable_models_target_index", columns: ["taggable_id", "taggable_type"], type: "index" }
28704
+ ]
28441
28705
  });
28442
28706
  }
28443
28707
  if (traits.categorizable) {
28444
28708
  ensureTable({
28445
- table: "categorizable",
28709
+ table: "categorizables",
28446
28710
  columns: [
28447
28711
  { name: "id", type: "bigint", isPrimaryKey: true, isUnique: false, isNullable: false, hasDefault: false },
28448
28712
  { name: "name", type: "string", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false },
28449
28713
  { name: "slug", type: "string", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false },
28450
28714
  { name: "description", type: "text", isPrimaryKey: false, isUnique: false, isNullable: true, hasDefault: false },
28715
+ { name: "is_active", type: "boolean", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: true, defaultValue: true },
28716
+ { name: "categorizable_type", type: "string", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false },
28451
28717
  { name: "created_at", type: "datetime", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: true, defaultValue: "CURRENT_TIMESTAMP" },
28452
28718
  { name: "updated_at", type: "datetime", isPrimaryKey: false, isUnique: false, isNullable: true, hasDefault: false }
28453
28719
  ],
28454
- indexes: [{ name: "categorizable_slug_unique", columns: ["slug"], type: "unique" }]
28720
+ indexes: [{ name: "categorizables_type_slug_unique", columns: ["categorizable_type", "slug"], type: "unique" }]
28455
28721
  });
28456
28722
  ensureTable({
28457
28723
  table: "categorizable_models",
28458
28724
  columns: [
28459
28725
  { name: "id", type: "bigint", isPrimaryKey: true, isUnique: false, isNullable: false, hasDefault: false },
28460
- { name: "category_id", type: "bigint", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false, references: { table: "categorizable", column: "id" } },
28726
+ { name: "category_id", type: "bigint", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false, references: { table: "categorizables", column: "id" } },
28461
28727
  { name: "categorizable_id", type: "bigint", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false },
28462
28728
  { name: "categorizable_type", type: "string", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: false },
28463
- { name: "created_at", type: "datetime", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: true, defaultValue: "CURRENT_TIMESTAMP" }
28729
+ { name: "created_at", type: "datetime", isPrimaryKey: false, isUnique: false, isNullable: false, hasDefault: true, defaultValue: "CURRENT_TIMESTAMP" },
28730
+ { name: "updated_at", type: "datetime", isPrimaryKey: false, isUnique: false, isNullable: true, hasDefault: false }
28464
28731
  ],
28465
- indexes: [{ name: "categorizable_models_target_index", columns: ["categorizable_id", "categorizable_type"], type: "index" }]
28732
+ indexes: [
28733
+ { name: "categorizable_models_owner_unique", columns: ["category_id", "categorizable_id", "categorizable_type"], type: "unique" },
28734
+ { name: "categorizable_models_target_index", columns: ["categorizable_id", "categorizable_type"], type: "index" }
28735
+ ]
28466
28736
  });
28467
28737
  }
28468
28738
  }
@@ -28471,7 +28741,7 @@ function buildMigrationPlan2(models, options) {
28471
28741
  const btm = model?.belongsToMany;
28472
28742
  if (!btm || typeof btm !== "object" || Array.isArray(btm))
28473
28743
  continue;
28474
- const parentTable = model.table || `${String(model.name).toLowerCase()}s`;
28744
+ const parentTable = tableNameFor(model);
28475
28745
  const parentSingular = singularizeTable(parentTable);
28476
28746
  for (const [relKey, value] of Object.entries(btm)) {
28477
28747
  const cfg = value;
@@ -28532,6 +28802,8 @@ function generateSql(plan, opts = {}) {
28532
28802
  migrationCounter = 0;
28533
28803
  migrationsCreatedCount = 0;
28534
28804
  migrationsUpdatedCount = 0;
28805
+ createdMigrationFiles = [];
28806
+ existingCorpusBodies = null;
28535
28807
  useDeterministicNames = true;
28536
28808
  const emit = opts.dryRun ? () => false : createMigrationFile;
28537
28809
  const statements = [];
@@ -28793,12 +29065,13 @@ function referencesAreDifferent(r1, r2) {
28793
29065
  const act = (a2) => (a2 ?? "no action").toLowerCase();
28794
29066
  return r1.table !== r2.table || r1.column !== r2.column || act(r1.onDelete) !== act(r2.onDelete) || act(r1.onUpdate) !== act(r2.onUpdate);
28795
29067
  }
28796
- function mapIndexesByKey(indexes) {
29068
+ function indexKey(index, table) {
29069
+ return `${index.type}:${qualifiedIndexName(table, index.name)}:${index.columns.join(",")}`;
29070
+ }
29071
+ function mapIndexesByKey(indexes, table) {
28797
29072
  const map = {};
28798
- for (const i2 of indexes) {
28799
- const key = `${i2.type}:${i2.name}:${i2.columns.join(",")}`;
28800
- map[key] = i2;
28801
- }
29073
+ for (const i2 of indexes)
29074
+ map[indexKey(i2, table)] = i2;
28802
29075
  return map;
28803
29076
  }
28804
29077
  function generateDiffOperations(previous, next, opts = {}) {
@@ -28813,10 +29086,13 @@ function generateDiffOperations(previous, next, opts = {}) {
28813
29086
  return { statements: statements2, operations: operations2 };
28814
29087
  }
28815
29088
  const applyRenames = opts.applyRenames !== false;
29089
+ const preserveUnknownColumns = opts.preserveUnknownColumns === true;
28816
29090
  const emit = opts.dryRun ? () => false : createMigrationFile;
28817
29091
  migrationCounter = 0;
28818
29092
  migrationsCreatedCount = 0;
28819
29093
  migrationsUpdatedCount = 0;
29094
+ createdMigrationFiles = [];
29095
+ existingCorpusBodies = null;
28820
29096
  useDeterministicNames = false;
28821
29097
  const chunks = [];
28822
29098
  const operations = [];
@@ -28895,42 +29171,52 @@ function generateDiffOperations(previous, next, opts = {}) {
28895
29171
  const prevCols = mapColumnsByName(prev.columns);
28896
29172
  const currCols = mapColumnsByName(curr.columns);
28897
29173
  for (const colName of Object.keys(currCols)) {
28898
- if (!prevCols[colName]) {
28899
- const c2 = currCols[colName];
28900
- if (c2.type === "enum" && c2.enumValues && c2.enumValues.length > 0) {
28901
- const enumTypeName = `${curr.table}_${c2.name}_type`;
28902
- c2.enumTypeName = enumTypeName;
28903
- if (!enumTypes.has(enumTypeName)) {
28904
- const createEnumStatement = driver.createEnumType(enumTypeName, c2.enumValues);
28905
- if (createEnumStatement) {
28906
- chunks.push(createEnumStatement);
28907
- operations.push({ kind: "create_enum", table: curr.table, column: c2.name, destructive: false, sql: createEnumStatement });
28908
- emit(createEnumStatement, `create-${enumTypeName}-enum`);
28909
- }
28910
- enumTypes.add(enumTypeName);
28911
- }
29174
+ const c2 = currCols[colName];
29175
+ if (c2.type !== "enum" || !c2.enumValues || c2.enumValues.length === 0)
29176
+ continue;
29177
+ const enumTypeName = `${curr.table}_${c2.name}_type`;
29178
+ c2.enumTypeName = enumTypeName;
29179
+ if (prevCols[colName])
29180
+ continue;
29181
+ if (!enumTypes.has(enumTypeName)) {
29182
+ const createEnumStatement = driver.createEnumType(enumTypeName, c2.enumValues);
29183
+ if (createEnumStatement) {
29184
+ chunks.push(createEnumStatement);
29185
+ operations.push({ kind: "create_enum", table: curr.table, column: c2.name, destructive: false, sql: createEnumStatement });
29186
+ emit(createEnumStatement, `create-${enumTypeName}-enum`);
28912
29187
  }
29188
+ enumTypes.add(enumTypeName);
28913
29189
  }
28914
29190
  }
28915
29191
  }
28916
29192
  for (const tableName of Object.keys(nextTables)) {
28917
29193
  const prev = prevTables[tableName];
28918
- const curr = nextTables[tableName];
29194
+ let curr = nextTables[tableName];
28919
29195
  if (!prev)
28920
29196
  continue;
28921
29197
  const prevCols = mapColumnsByName(prev.columns);
28922
- const currCols = mapColumnsByName(curr.columns);
28923
- const prevIdx = mapIndexesByKey(prev.indexes);
28924
- const currIdx = mapIndexesByKey(curr.indexes);
29198
+ const prevIdx = mapIndexesByKey(prev.indexes, prev.table);
29199
+ let currCols = mapColumnsByName(curr.columns);
29200
+ let currIdx = mapIndexesByKey(curr.indexes, curr.table);
28925
29201
  let renames = [];
28926
29202
  let removedCols = Object.keys(prevCols).filter((n2) => !currCols[n2]);
28927
29203
  let addedCols = Object.keys(currCols).filter((n2) => !prevCols[n2]);
28928
- if (applyRenames) {
29204
+ if (applyRenames && !preserveUnknownColumns) {
28929
29205
  const detected = detectColumnRenames(prevCols, currCols, dialect);
28930
29206
  renames = detected.renames;
28931
29207
  removedCols = detected.removed;
28932
29208
  addedCols = detected.added;
28933
29209
  }
29210
+ if (preserveUnknownColumns && removedCols.length > 0) {
29211
+ const preserved = removedCols.map((name) => prevCols[name]);
29212
+ const preservedNames = new Set(removedCols);
29213
+ const keptIndexes = prev.indexes.filter((idx) => idx.columns.some((c2) => preservedNames.has(c2)) && !currIdx[indexKey(idx)]);
29214
+ info2(`-- Keeping ${preserved.length} column(s) on "${curr.table}" no model declares: ${removedCols.join(", ")} \u2014 reconciled from the database, where an undeclared column is out of scope rather than removed`);
29215
+ curr = { ...curr, columns: [...curr.columns, ...preserved], indexes: [...curr.indexes, ...keptIndexes] };
29216
+ currCols = mapColumnsByName(curr.columns);
29217
+ currIdx = mapIndexesByKey(curr.indexes, curr.table);
29218
+ removedCols = [];
29219
+ }
28934
29220
  const modifiedCols = Object.keys(currCols).filter((n2) => prevCols[n2] && columnsAreDifferent(prevCols[n2], currCols[n2], dialect));
28935
29221
  const fkChangedCols = Object.keys(currCols).filter((n2) => prevCols[n2] && referencesAreDifferent(prevCols[n2].references, currCols[n2].references));
28936
29222
  if (dialect === "sqlite") {
@@ -29078,11 +29364,13 @@ function generateDiffOperations(previous, next, opts = {}) {
29078
29364
  function generateDiffSql(previous, next, opts = {}) {
29079
29365
  return generateDiffOperations(previous, next, opts).statements;
29080
29366
  }
29081
- var migrationCounter = 0, migrationsCreatedCount = 0, migrationsUpdatedCount = 0, useDeterministicNames = true;
29367
+ var migrationCounter = 0, migrationsCreatedCount = 0, migrationsUpdatedCount = 0, useDeterministicNames = true, createdMigrationFiles, existingCorpusBodies = null, GENERATED_MARKER = "-- qb:generated";
29082
29368
  var init_migrations = __esm(() => {
29083
29369
  init_config();
29084
29370
  init_drivers();
29371
+ init_sqlite();
29085
29372
  init_meta();
29373
+ createdMigrationFiles = [];
29086
29374
  });
29087
29375
 
29088
29376
  // src/schema.ts
@@ -29116,6 +29404,7 @@ var init_src2 = __esm(() => {
29116
29404
  init_dynamodb_single_table();
29117
29405
  init_dynamodb_tooling_adapter();
29118
29406
  init_dynamodb();
29407
+ init_factory();
29119
29408
  init_loader();
29120
29409
  init_meta();
29121
29410
  init_migrations();
@@ -29125,11 +29414,11 @@ var init_src2 = __esm(() => {
29125
29414
  });
29126
29415
 
29127
29416
  // ../../node_modules/@stacksjs/clapp/dist/index.js
29128
- import { existsSync as existsSync23 } from "fs";
29417
+ import { existsSync as existsSync25 } from "fs";
29129
29418
  import fs from "fs/promises";
29130
29419
  import os from "os";
29131
29420
  import path from "path";
29132
- import process36 from "process";
29421
+ import process33 from "process";
29133
29422
  import { EventEmitter } from "events";
29134
29423
  import process52 from "process";
29135
29424
  import process210 from "process";
@@ -29153,10 +29442,10 @@ class Telemetry {
29153
29442
  this.configPath = path.join(configDir, "telemetry.json");
29154
29443
  }
29155
29444
  async isEnabled() {
29156
- if (process36.env.DO_NOT_TRACK === "1" || process36.env.DO_NOT_TRACK === "true") {
29445
+ if (process33.env.DO_NOT_TRACK === "1" || process33.env.DO_NOT_TRACK === "true") {
29157
29446
  return false;
29158
29447
  }
29159
- if (process36.env.NO_TELEMETRY === "1" || process36.env.NO_TELEMETRY === "true") {
29448
+ if (process33.env.NO_TELEMETRY === "1" || process33.env.NO_TELEMETRY === "true") {
29160
29449
  return false;
29161
29450
  }
29162
29451
  const config6 = await this.loadConfig();
@@ -29185,7 +29474,7 @@ class Telemetry {
29185
29474
  ...data,
29186
29475
  timestamp: Date.now(),
29187
29476
  platform: os.platform(),
29188
- nodeVersion: process36.version
29477
+ nodeVersion: process33.version
29189
29478
  };
29190
29479
  this.events.push(telemetryEvent);
29191
29480
  if (this.events.length >= 10) {
@@ -29241,7 +29530,7 @@ class Telemetry {
29241
29530
  const config6 = await this.loadConfig();
29242
29531
  return {
29243
29532
  enabled: config6.enabled,
29244
- doNotTrack: process36.env.DO_NOT_TRACK === "1" || process36.env.DO_NOT_TRACK === "true",
29533
+ doNotTrack: process33.env.DO_NOT_TRACK === "1" || process33.env.DO_NOT_TRACK === "true",
29245
29534
  eventsQueued: this.events.length,
29246
29535
  lastSent: config6.lastSent
29247
29536
  };
@@ -29251,7 +29540,7 @@ class Telemetry {
29251
29540
  return this.config;
29252
29541
  }
29253
29542
  try {
29254
- if (existsSync23(this.configPath)) {
29543
+ if (existsSync25(this.configPath)) {
29255
29544
  const data = await fs.readFile(this.configPath, "utf-8");
29256
29545
  this.config = JSON.parse(data);
29257
29546
  return this.config;
@@ -31163,7 +31452,7 @@ function getPrefix() {
31163
31452
  }
31164
31453
  var prefix = getPrefix();
31165
31454
  // package.json
31166
- var version2 = "0.1.63";
31455
+ var version2 = "0.2.2";
31167
31456
 
31168
31457
  // bin/cli.ts
31169
31458
  init_actions();
@@ -31218,7 +31507,7 @@ cli.command("unsafe <sql>", "Execute an unsafe SQL string (one statement with pa
31218
31507
  cli.command("file <path>", "Execute a SQL file (supports $1, $2 params)").option("--params <json>", "JSON array of parameters").example("query-builder file ./migrations/seed.sql").example('query-builder file ./reports/top.sql --params "[30]"').action(async (path2, opts) => {
31219
31508
  await file(path2, opts);
31220
31509
  });
31221
- cli.command("migrate <dir>", "Generate SQL migrations from models").option("--dialect <d>", "Dialect (postgres|mysql|sqlite)", { default: "postgres" }).option("--state <path>", "Path to migration state file (defaults to <dir>/.qb-migrations.<dialect>.json)").option("--apply", "Execute the generated SQL against the database").option("--full", "Force full migration SQL instead of incremental diff").example("query-builder migrate ./app/Models --dialect postgres").action(async (dir, opts) => {
31510
+ cli.command("migrate <dir>", "Generate SQL migrations from models").option("--dialect <d>", "Dialect (postgres|mysql|sqlite) \u2014 defaults to the configured dialect").option("--state <path>", "Path to migration state file (defaults to <dir>/.qb-migrations.<dialect>.json)").option("--apply", "Execute the generated SQL against the database").option("--full", "Force full migration SQL instead of incremental diff").example("query-builder migrate ./app/Models --dialect postgres").action(async (dir, opts) => {
31222
31511
  try {
31223
31512
  await generateMigration(dir, {
31224
31513
  dialect: opts.dialect,
@@ -31262,7 +31551,7 @@ cli.command("relation:diagram", "Generate relationship diagram from models").opt
31262
31551
  proc.default.exitCode = 1;
31263
31552
  }
31264
31553
  });
31265
- cli.command("migrate:fresh <dir>", "Reset database and run all migrations").option("--dialect <d>", "Dialect (postgres|mysql|sqlite)", { default: "postgres" }).option("--state <path>", "Path to migration state file (defaults to <dir>/.qb-migrations.<dialect>.json)").option("--full", "Force full migration SQL instead of incremental diff").example("query-builder migrate:fresh ./app/Models --dialect postgres").action(async (dir, opts) => {
31554
+ cli.command("migrate:fresh <dir>", "Reset database and run all migrations").option("--dialect <d>", "Dialect (postgres|mysql|sqlite) \u2014 defaults to the configured dialect").option("--state <path>", "Path to migration state file (defaults to <dir>/.qb-migrations.<dialect>.json)").option("--full", "Force full migration SQL instead of incremental diff").example("query-builder migrate:fresh ./app/Models --dialect postgres").action(async (dir, opts) => {
31266
31555
  try {
31267
31556
  console.log("-- Resetting database...");
31268
31557
  await resetDatabase(dir, {
@@ -31283,7 +31572,7 @@ cli.command("migrate:fresh <dir>", "Reset database and run all migrations").opti
31283
31572
  proc.default.exitCode = 1;
31284
31573
  }
31285
31574
  });
31286
- cli.command("reset <dir>", "Drop all tables and reset database").option("--dialect <d>", "Dialect (postgres|mysql|sqlite)", { default: "postgres" }).option("--state <path>", "Path to migration state file (defaults to <dir>/.qb-migrations.<dialect>.json)").example("query-builder reset ./app/Models --dialect postgres").action(async (dir, opts) => {
31575
+ cli.command("reset <dir>", "Drop all tables and reset database").option("--dialect <d>", "Dialect (postgres|mysql|sqlite) \u2014 defaults to the configured dialect").option("--state <path>", "Path to migration state file (defaults to <dir>/.qb-migrations.<dialect>.json)").example("query-builder reset ./app/Models --dialect postgres").action(async (dir, opts) => {
31287
31576
  try {
31288
31577
  await resetDatabase(dir, {
31289
31578
  dialect: opts.dialect,
@@ -31387,16 +31676,20 @@ cli.command("migrate:list", "List all migrations (alias for migrate:status)").ex
31387
31676
  proc.default.exitCode = 1;
31388
31677
  }
31389
31678
  });
31390
- cli.command("migrate:rollback", "Rollback migrations").option("--steps <n>", "Number of migrations to rollback", { default: 1 }).example("query-builder migrate:rollback").example("query-builder migrate:rollback --steps 2").action(async (opts) => {
31679
+ cli.command("migrate:rollback", "Rollback migrations").option("--steps <n>", "Number of migrations to rollback", { default: 1 }).option("--no-reverse-schema", "Only remove the migration records; leave the schema alone").option("--delete-files", "Also delete the rolled-back migration files from disk").example("query-builder migrate:rollback").example("query-builder migrate:rollback --steps 2").example("query-builder migrate:rollback --no-reverse-schema").action(async (opts) => {
31391
31680
  try {
31392
- await migrateRollback({ steps: Number(opts.steps) });
31681
+ await migrateRollback({
31682
+ steps: Number(opts.steps),
31683
+ reverseSchema: opts.reverseSchema,
31684
+ deleteFiles: Boolean(opts.deleteFiles)
31685
+ });
31393
31686
  } catch (err) {
31394
31687
  console.error("-- Rollback failed:", err);
31395
31688
  const proc = await import("process");
31396
31689
  proc.default.exitCode = 1;
31397
31690
  }
31398
31691
  });
31399
- cli.command("migrate:generate [dir]", "Generate migration from model changes").option("--dialect <d>", "Dialect (postgres|mysql|sqlite)", { default: "postgres" }).option("--state <path>", "Path to migration state file").option("--apply", "Execute the generated SQL").option("--full", "Force full migration SQL").example("query-builder migrate:generate").example("query-builder migrate:generate ./app/Models --dialect postgres").action(async (dir, opts) => {
31692
+ cli.command("migrate:generate [dir]", "Generate migration from model changes").option("--dialect <d>", "Dialect (postgres|mysql|sqlite) \u2014 defaults to the configured dialect").option("--state <path>", "Path to migration state file").option("--apply", "Execute the generated SQL").option("--full", "Force full migration SQL").example("query-builder migrate:generate").example("query-builder migrate:generate ./app/Models --dialect postgres").action(async (dir, opts) => {
31400
31693
  try {
31401
31694
  await migrateGenerate(dir, {
31402
31695
  dialect: opts?.dialect,
@@ -31428,7 +31721,7 @@ cli.command("db:stats", "Show database statistics (alias for db:info)").example(
31428
31721
  proc.default.exitCode = 1;
31429
31722
  }
31430
31723
  });
31431
- cli.command("db:wipe", "Drop all tables from the database").option("--dialect <d>", "Dialect (postgres|mysql|sqlite)", { default: "postgres" }).option("--force", "Skip confirmation prompt").option("--verbose", "Enable verbose output").example("query-builder db:wipe").example("query-builder db:wipe --force").action(async (opts) => {
31724
+ cli.command("db:wipe", "Drop all tables from the database").option("--dialect <d>", "Dialect (postgres|mysql|sqlite) \u2014 defaults to the configured dialect").option("--force", "Skip confirmation prompt").option("--verbose", "Enable verbose output").example("query-builder db:wipe").example("query-builder db:wipe --force").action(async (opts) => {
31432
31725
  try {
31433
31726
  await dbWipe({
31434
31727
  dialect: opts.dialect,
@@ -31441,7 +31734,7 @@ cli.command("db:wipe", "Drop all tables from the database").option("--dialect <d
31441
31734
  proc.default.exitCode = 1;
31442
31735
  }
31443
31736
  });
31444
- cli.command("db:optimize", "Optimize database tables (VACUUM, ANALYZE, OPTIMIZE)").option("--dialect <d>", "Dialect (postgres|mysql|sqlite)", { default: "postgres" }).option("--aggressive", "Use aggressive optimization (VACUUM FULL for postgres)").option("--tables <tables>", "Comma-separated list of tables to optimize").option("--verbose", "Enable verbose output").example("query-builder db:optimize").example("query-builder db:optimize --aggressive").example("query-builder db:optimize --tables users,posts").action(async (opts) => {
31737
+ cli.command("db:optimize", "Optimize database tables (VACUUM, ANALYZE, OPTIMIZE)").option("--dialect <d>", "Dialect (postgres|mysql|sqlite) \u2014 defaults to the configured dialect").option("--aggressive", "Use aggressive optimization (VACUUM FULL for postgres)").option("--tables <tables>", "Comma-separated list of tables to optimize").option("--verbose", "Enable verbose output").example("query-builder db:optimize").example("query-builder db:optimize --aggressive").example("query-builder db:optimize --tables users,posts").action(async (opts) => {
31445
31738
  try {
31446
31739
  await dbOptimize({
31447
31740
  dialect: opts.dialect,