@happyvertical/smrt-cli 0.42.7 → 0.43.1

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/AGENTS.md CHANGED
@@ -43,6 +43,18 @@ migrations are manifest-driven through registered objects and project manifests.
43
43
 
44
44
  ## `db:migrate` on PostgreSQL
45
45
 
46
+ New tables are created in deterministic foreign-key dependency order. Mutual
47
+ cycles are created without the cyclic clauses first, then receive named
48
+ constraints after both tables exist. When a same-package constraint is missing
49
+ on an existing table, `db:migrate` probes the exact child/parent columns for
50
+ orphans before `ADD ... NOT VALID` and `VALIDATE CONSTRAINT`; orphaned data or a
51
+ failed probe stays manual with detector/repair SQL.
52
+
53
+ `db:migrate --dry-run` and deprecated `db:setup --dry-run` print the same
54
+ engine-specific dependency plan used for execution, including exact table DDL
55
+ and deferred PostgreSQL cycle constraints; cached per-class DDL is not a valid
56
+ preview.
57
+
46
58
  `db:migrate` always bounds a PostgreSQL batch with `SET LOCAL lock_timeout` and
47
59
  `SET LOCAL statement_timeout` inside its transaction, from
48
60
  `migrations.postgres.lockTimeout` / `.statementTimeout` (defaults `30s` / `60s`;
@@ -105,6 +117,13 @@ not from the schema definition.
105
117
 
106
118
  ## `db:migrate` on SQLite: type changes rebuild the table
107
119
 
120
+ SQLite can enforce generated same-package constraints on new tables, including
121
+ cycles, but adding one to an existing table requires an explicit rebuild.
122
+ DuckDB reports unsupported constraint shapes or `ALTER ADD CONSTRAINT` as
123
+ manual/refused work, never as a successful no-op. Generated same-package
124
+ constraints retain `ON UPDATE CASCADE`, which DuckDB/JSON cannot enforce, so
125
+ those engines report an actionable refusal rather than silently stripping it.
126
+
108
127
  SQLite has no `ALTER COLUMN ... TYPE`, so a type-bucket change (the common one
109
128
  being a numeric default edited `0` → `0.0`) is applied as the documented table
110
129
  rebuild — stage, copy, drop, rename, replay indexes and triggers — planned by
@@ -5,7 +5,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFile
5
5
  import * as path from "node:path";
6
6
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { ObjectRegistry, SchemaComparer, checkLiveSchemaParity, createQualifiedName, ensureDeferredSystemTableCompatibility, generateDDLForEngine, getClassName, isQualifiedName, migratePostgresSystemTimestamps, parseQualifiedName, planPostgresSystemTimestampMigrations } from "@happyvertical/smrt-core";
8
+ import { ObjectRegistry, SchemaComparer, checkLiveSchemaParity, createQualifiedName, ensureDeferredSystemTableCompatibility, generateDDLForEngine, getClassName, isQualifiedName, migratePostgresSystemTimestamps, parseQualifiedName, planForeignKeyCreation, planPostgresSystemTimestampMigrations } from "@happyvertical/smrt-core";
9
9
  import { loadExternalManifestSync } from "@happyvertical/smrt-core/manifest";
10
10
  import { access, cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
11
11
  import { createLogger } from "@happyvertical/logger";
@@ -489,6 +489,14 @@ function getSyntheticMigrationNameForAction(action) {
489
489
  const fingerprint = sqlShapeFingerprint(action);
490
490
  return fingerprint ? `add_index_${action.index.name}_${fingerprint}` : `add_index_${action.index.name}`;
491
491
  }
492
+ case "add_foreign_key": {
493
+ const fingerprint = sqlShapeFingerprint(action);
494
+ return fingerprint ? `add_foreign_key_${action.tableName}_${fingerprint}` : null;
495
+ }
496
+ case "drop_foreign_key": {
497
+ const fingerprint = sqlShapeFingerprint(action);
498
+ return fingerprint ? `drop_foreign_key_${action.tableName}_${fingerprint}` : null;
499
+ }
492
500
  case "drop_index": {
493
501
  if (!action.indexName) return null;
494
502
  const fingerprint = sqlShapeFingerprint(action);
@@ -517,6 +525,14 @@ function getSyntheticMigrationNameForChange(change) {
517
525
  });
518
526
  return fingerprint ? `add_index_${indexName}_${fingerprint}` : `add_index_${indexName}`;
519
527
  }
528
+ case "add_foreign_key": {
529
+ const fingerprint = sqlShapeFingerprint(change);
530
+ return fingerprint ? `add_foreign_key_${change.table}_${fingerprint}` : null;
531
+ }
532
+ case "drop_foreign_key": {
533
+ const fingerprint = sqlShapeFingerprint(change);
534
+ return fingerprint ? `drop_foreign_key_${change.table}_${fingerprint}` : null;
535
+ }
520
536
  case "drop_index": {
521
537
  if (!change.name) return null;
522
538
  const fingerprint = sqlShapeFingerprint({
@@ -557,13 +573,13 @@ function shouldApplySchemaMigrations(state) {
557
573
  return !state.dryRun;
558
574
  }
559
575
  function classifyFailedMigration(migrationName, unresolvedSyntheticMigrationNames) {
560
- if (!migrationName.startsWith("add_column_") && !migrationName.startsWith("drop_column_") && !migrationName.startsWith("alter_column_") && !migrationName.startsWith("add_index_") && !migrationName.startsWith("drop_index_") && !migrationName.startsWith("type_upgrade_")) return "other";
576
+ if (!migrationName.startsWith("add_column_") && !migrationName.startsWith("drop_column_") && !migrationName.startsWith("alter_column_") && !migrationName.startsWith("add_foreign_key_") && !migrationName.startsWith("drop_foreign_key_") && !migrationName.startsWith("add_index_") && !migrationName.startsWith("drop_index_") && !migrationName.startsWith("type_upgrade_")) return "other";
561
577
  return unresolvedSyntheticMigrationNames.has(migrationName) ? "unresolved" : "superseded";
562
578
  }
563
579
  function getUnresolvedGeneratedMigrationNames(changes) {
564
580
  const names = /* @__PURE__ */ new Set();
565
581
  for (const change of changes) {
566
- if (change.type !== "add_column" && change.type !== "drop_column" && change.type !== "alter_column" && change.type !== "add_index" && change.type !== "drop_index" && change.type !== "type_upgrade") continue;
582
+ if (change.type !== "add_column" && change.type !== "drop_column" && change.type !== "alter_column" && change.type !== "add_foreign_key" && change.type !== "drop_foreign_key" && change.type !== "add_index" && change.type !== "drop_index" && change.type !== "type_upgrade") continue;
567
583
  if (change.type === "type_upgrade" && classifyTypeUpgradeSql(change.sql) === "noop") continue;
568
584
  if (change.type === "alter_column" && isAdvisoryOnlyChangeLike(change)) continue;
569
585
  for (const migrationName of getSyntheticMigrationNamesForChange(change)) names.add(migrationName);
@@ -706,6 +722,20 @@ function partitionSchemaChanges(changes, getClassForTable) {
706
722
  });
707
723
  break;
708
724
  }
725
+ case "add_foreign_key":
726
+ case "drop_foreign_key": {
727
+ const action = {
728
+ type: change.type,
729
+ tableName: change.table,
730
+ className,
731
+ sql: change.sql,
732
+ ...change.sqlStatements ? { sqlStatements: change.sqlStatements } : {},
733
+ advisory: change.advisory
734
+ };
735
+ if (isAdvisoryOnlyChangeLike(change)) manualInterventions.push(action);
736
+ else migrations.push(action);
737
+ break;
738
+ }
709
739
  case "drop_index":
710
740
  if (!change.name) continue;
711
741
  migrations.push({
@@ -965,7 +995,8 @@ var dbDiffCommand = {
965
995
  }, null, 2));
966
996
  return;
967
997
  }
968
- const { advisories } = partitionSchemaChanges(diff.changes, (tableName) => tableName);
998
+ const { advisories, manualInterventions } = partitionSchemaChanges(diff.changes, (tableName) => tableName);
999
+ const manualForeignKeyChanges = manualInterventions.filter((change) => change.type === "add_foreign_key" || change.type === "drop_foreign_key");
969
1000
  if (!diff.has_changes) {
970
1001
  console.log("✅ Database schema is up to date - no changes detected\n");
971
1002
  printSchemaAdvisories(advisories, {
@@ -974,7 +1005,7 @@ var dbDiffCommand = {
974
1005
  });
975
1006
  return;
976
1007
  }
977
- if (diff.changes.filter((c) => !(c.advisory && !c.sql && !c.sqlStatements)).length === 0 && diff.added_tables.length === 0 && diff.dropped_tables.length === 0) {
1008
+ if (diff.changes.filter((c) => !(c.advisory && !c.sql && !c.sqlStatements)).length === 0 && manualForeignKeyChanges.length === 0 && diff.added_tables.length === 0 && diff.dropped_tables.length === 0) {
978
1009
  console.log("✅ Database schema is up to date - no migrations needed (see notes below)\n");
979
1010
  printSchemaAdvisories(advisories, {
980
1011
  orphanTables: diff.orphan_tables,
@@ -1055,6 +1086,16 @@ var dbDiffCommand = {
1055
1086
  for (const change of typeMismatches) console.log(` ! ${change.table}.${change.name}: ${change.mismatch?.expected} vs ${change.mismatch?.actual}`);
1056
1087
  console.log(" (Type changes require manual migration)\n");
1057
1088
  }
1089
+ if (manualForeignKeyChanges.length > 0) {
1090
+ console.log(` ⚠️ Foreign-key drift needing a manual step (${manualForeignKeyChanges.length}):`);
1091
+ for (const change of manualForeignKeyChanges) {
1092
+ const operation = change.type === "drop_foreign_key" ? "remove" : "add";
1093
+ console.log(` ! ${change.tableName}: ${operation} constraint`);
1094
+ console.log(` ${change.advisory?.message ?? "foreign-key constraint requires manual repair"}`);
1095
+ for (const sql of change.advisory?.suggestedSql ?? []) console.log(` ↳ ${sql}`);
1096
+ }
1097
+ console.log();
1098
+ }
1058
1099
  printSchemaAdvisories(advisories, {
1059
1100
  orphanTables: diff.orphan_tables,
1060
1101
  verbose: options.verbose
@@ -2361,6 +2402,17 @@ function summarizeSchemaDiff(diff) {
2361
2402
  recommendation: "Run `smrt db:migrate` to add the missing index and reconcile the live schema."
2362
2403
  });
2363
2404
  break;
2405
+ case "add_foreign_key":
2406
+ case "drop_foreign_key": {
2407
+ const relationship = change.foreignKey ? `${change.table}.${change.foreignKey.column} -> ${change.foreignKey.referencesTable}.${change.foreignKey.referencesColumn}` : `${change.table}.${change.name ?? "(unknown)"}`;
2408
+ const isAddition = change.type === "add_foreign_key";
2409
+ drift.push({
2410
+ name: relationship,
2411
+ type: isAddition ? "missing_foreign_key" : "disabled_foreign_key",
2412
+ recommendation: change.advisory?.message ?? (isAddition ? "Run `smrt db:migrate` to add the missing foreign key and reconcile the live schema." : "Run `smrt db:migrate` to remove the disabled foreign key and reconcile the live schema.")
2413
+ });
2414
+ break;
2415
+ }
2364
2416
  case "type_upgrade":
2365
2417
  switch (classifyTypeUpgradeSql(change.sql)) {
2366
2418
  case "executable":
@@ -7752,6 +7804,15 @@ export default testManifest;
7752
7804
  schemaContract: config.schemaContract
7753
7805
  }));
7754
7806
  const initOrder = ObjectRegistry.getInitializationOrder();
7807
+ const engine = resolveDDLPreviewEngine(dbType);
7808
+ const schemas = Object.values(ObjectRegistry.getAllSchemasAsDefinitions());
7809
+ const preflightTables = new Set(schemas.map((schema) => schema.tableName));
7810
+ const missingPreflightTables = [...new Set(initOrder.flatMap((className) => {
7811
+ const tableName = ObjectRegistry.getTableName(className);
7812
+ return tableName && !preflightTables.has(tableName) ? [tableName] : [];
7813
+ }))].sort();
7814
+ if (missingPreflightTables.length > 0) throw new Error(`Cannot safely preflight database setup because structured schema definitions are missing for: ${missingPreflightTables.join(", ")}. Rebuild the package manifests before running db:setup.`);
7815
+ const plan = planForeignKeyCreation(schemas, engine);
7755
7816
  if (options.verbose) {
7756
7817
  console.log("📋 Initialization order (respecting dependencies):");
7757
7818
  for (let i = 0; i < initOrder.length; i++) {
@@ -7764,25 +7825,17 @@ export default testManifest;
7764
7825
  }
7765
7826
  if (options["dry-run"]) {
7766
7827
  console.log("📋 SQL Preview (not executed):\n");
7767
- const { generateSchema } = await import("@happyvertical/smrt-core/schema/utils");
7768
- for (const className of initOrder) {
7769
- const registered = ObjectRegistry.getClass(className);
7770
- if (!registered) continue;
7771
- const tableStrategy = ObjectRegistry.getTableStrategy(className);
7772
- const stiBase = ObjectRegistry.getSTIBase(className);
7773
- const qualifiedClassName = registered.qualifiedName ?? registered.name ?? className;
7774
- if (tableStrategy === "sti" && !!stiBase && stiBase !== qualifiedClassName && stiBase !== className) {
7775
- console.log(`-- Table: ${className} (Base: ${stiBase}, Strategy: STI)`);
7776
- console.log(`-- Shares table with ${stiBase} (STI child)\n`);
7777
- continue;
7778
- }
7779
- const schema = await generateSchema(registered.constructor, void 0, { engine: resolveDDLPreviewEngine(dbType) });
7780
- if (schema && schema.trim() !== "") {
7781
- const tableName = ObjectRegistry.getTableName(className);
7782
- console.log(`-- Table: ${tableName} (Class: ${className}, Strategy: ${tableStrategy === "sti" ? "STI" : "CTI"})`);
7783
- console.log(schema);
7784
- console.log();
7785
- }
7828
+ for (const schema of plan.schemas) {
7829
+ const ddl = generateDDLForEngine(schema, engine);
7830
+ console.log(`-- Table: ${schema.tableName}`);
7831
+ console.log(ddl.createTable);
7832
+ for (const statement of [...ddl.indexes, ...ddl.triggers]) console.log(statement);
7833
+ console.log();
7834
+ }
7835
+ if (plan.deferredStatements.length > 0) {
7836
+ console.log("-- Deferred foreign-key constraints");
7837
+ for (const statement of plan.deferredStatements) console.log(statement);
7838
+ console.log();
7786
7839
  }
7787
7840
  console.log("✅ Dry-run complete (no changes made)\n");
7788
7841
  return;
@@ -7814,23 +7867,42 @@ export default testManifest;
7814
7867
  }
7815
7868
  console.log("\n🗑️ Dropping existing tables...\n");
7816
7869
  const dropOrder = [...initOrder].reverse();
7817
- for (const className of dropOrder) {
7818
- const tableName = ObjectRegistry.getTableName(className);
7819
- if (!tableName) continue;
7820
- try {
7821
- await db.execute`DROP TABLE IF EXISTS ${tableName}`;
7822
- console.log(` ✓ Dropped ${tableName}`);
7823
- } catch (error) {
7824
- if (options.verbose) console.log(` ⚠️ Could not drop ${tableName}: ${error}`);
7870
+ const dropEngine = resolveDDLPreviewEngine(dbType);
7871
+ if (dropEngine === "sqlite") await db.query("PRAGMA foreign_keys = OFF");
7872
+ try {
7873
+ for (const className of dropOrder) {
7874
+ const tableName = ObjectRegistry.getTableName(className);
7875
+ if (!tableName) continue;
7876
+ try {
7877
+ const cascade = dropEngine === "postgres" ? " CASCADE" : "";
7878
+ await db.query(`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`);
7879
+ console.log(` ✓ Dropped ${tableName}`);
7880
+ } catch (error) {
7881
+ if (options.verbose) console.log(` ⚠️ Could not drop ${tableName}: ${error}`);
7882
+ throw new Error(`Refusing to continue after failing to drop ${tableName}; dependency or foreign-key constraints may still be active.`, { cause: error });
7883
+ }
7825
7884
  }
7885
+ } finally {
7886
+ if (dropEngine === "sqlite") await db.query("PRAGMA foreign_keys = ON");
7826
7887
  }
7827
7888
  console.log();
7828
7889
  }
7829
7890
  console.log("🔨 Creating tables...\n");
7830
- const { ensureSchema } = await import("@happyvertical/smrt-core/schema/utils");
7891
+ const { createSchemaManager } = await import("@happyvertical/smrt-core/schema");
7892
+ const schemaManager = createSchemaManager(db, {
7893
+ engine,
7894
+ skipTriggers: typeof db.exportTable === "function"
7895
+ });
7896
+ try {
7897
+ await schemaManager.ensureTables(schemas);
7898
+ } catch (error) {
7899
+ console.error(` ✗ Schema creation failed: ${error}`);
7900
+ if (options.verbose && error instanceof Error && error.stack) console.error(`\n${error.stack}\n`);
7901
+ throw new Error("Refusing to report database setup success after schema creation failed.", { cause: error });
7902
+ }
7831
7903
  let tablesCreated = 0;
7832
7904
  let tablesSkipped = 0;
7833
- for (const className of initOrder) try {
7905
+ for (const className of initOrder) {
7834
7906
  const tableStrategy = ObjectRegistry.getTableStrategy(className);
7835
7907
  const stiBase = ObjectRegistry.getSTIBase(className);
7836
7908
  const registered = ObjectRegistry.getClass(className);
@@ -7840,14 +7912,10 @@ export default testManifest;
7840
7912
  if (options.verbose) console.log(` ⊙ ${className} (shares table with ${stiBase})`);
7841
7913
  continue;
7842
7914
  }
7843
- await ensureSchema(db, className);
7844
7915
  const tableName = ObjectRegistry.getTableName(className);
7845
7916
  const fieldCount = ObjectRegistry.getFields(className)?.size || 0;
7846
7917
  console.log(` ✓ ${tableName} (${fieldCount} columns)`);
7847
7918
  tablesCreated++;
7848
- } catch (error) {
7849
- console.error(` ✗ ${className}: ${error}`);
7850
- if (options.verbose && error instanceof Error && error.stack) console.error(`\n${error.stack}\n`);
7851
7919
  }
7852
7920
  console.log();
7853
7921
  if (tablesSkipped > 0) {
@@ -8230,16 +8298,29 @@ export default testManifest;
8230
8298
  relaxColumns: Boolean(options["relax-columns"]),
8231
8299
  postgresTimestampMigration
8232
8300
  }).compare(manifestSchemas);
8301
+ const engine = tracker.getEngine();
8302
+ const tablePlan = planForeignKeyCreation(diff.added_tables, engine);
8303
+ const plannedTableDDL = new Map(tablePlan.schemas.map((schema) => [schema.tableName, generateDDLForEngine(schema, engine)]));
8304
+ const deferredForeignKeyMigrations = tablePlan.deferredStatements.map((statement) => {
8305
+ const parsed = statement.match(/^ALTER TABLE\s+"((?:[^"]|"")+)"\s+ADD\s+CONSTRAINT\s+"((?:[^"]|"")+)"/i);
8306
+ const unquote = (value, fallback) => value ? value.replace(/""/g, "\"") : fallback;
8307
+ return {
8308
+ type: "add_foreign_key",
8309
+ tableName: unquote(parsed?.[1], "deferred_cycle"),
8310
+ className: unquote(parsed?.[2], "deferred cycle constraint"),
8311
+ sql: statement
8312
+ };
8313
+ });
8233
8314
  const getClassForTable = (tableName) => {
8234
8315
  for (const className of initOrder) if (ObjectRegistry.getTableName(className) === tableName) return className;
8235
8316
  return tableName;
8236
8317
  };
8237
8318
  if (diff.added_tables.length > 0 && isDryRun) {
8238
- for (const schema of diff.added_tables) {
8319
+ for (const schema of tablePlan.schemas) {
8239
8320
  const className = getClassForTable(schema.tableName);
8240
8321
  const fields = Object.keys(schema.columns).length;
8241
8322
  console.log(` 📦 ${schema.tableName} (${className}): Would create table (${fields} columns)`);
8242
- if (options.verbose && schema.ddl) console.log(` ${schema.ddl}`);
8323
+ if (options.verbose) console.log(` ${plannedTableDDL.get(schema.tableName)?.createTable}`);
8243
8324
  }
8244
8325
  console.log();
8245
8326
  }
@@ -8247,14 +8328,26 @@ export default testManifest;
8247
8328
  migrations.push(...partitionedChanges.migrations);
8248
8329
  manualInterventions.push(...partitionedChanges.manualInterventions);
8249
8330
  const advisories = partitionedChanges.advisories;
8250
- assertForceMigrationTargetsExist(forceSelection.forceMigrations, [...diff.added_tables.map((schema) => `create_table_${schema.tableName}`), ...migrations.flatMap((migration) => {
8251
- const migrationName = getSyntheticMigrationNameForAction(migration);
8252
- return migrationName ? [migrationName] : [];
8253
- })]);
8331
+ assertForceMigrationTargetsExist(forceSelection.forceMigrations, [
8332
+ ...diff.added_tables.map((schema) => `create_table_${schema.tableName}`),
8333
+ ...migrations.flatMap((migration) => {
8334
+ const migrationName = getSyntheticMigrationNameForAction(migration);
8335
+ return migrationName ? [migrationName] : [];
8336
+ }),
8337
+ ...deferredForeignKeyMigrations.flatMap((migration) => {
8338
+ const migrationName = getSyntheticMigrationNameForAction(migration);
8339
+ return migrationName ? [migrationName] : [];
8340
+ })
8341
+ ]);
8254
8342
  console.log();
8255
8343
  if (manualInterventions.length > 0) {
8256
8344
  console.log("⚠️ Schema drift detected that requires manual intervention:\n");
8257
8345
  for (const change of manualInterventions) {
8346
+ if (change.type === "add_foreign_key" || change.type === "drop_foreign_key") {
8347
+ console.log(` ${change.tableName}: ${change.advisory?.message ?? change.sql?.replace(/^--\s*/, "") ?? "foreign-key constraint requires manual repair"}`);
8348
+ for (const sql of change.advisory?.suggestedSql ?? []) console.log(` ${sql}`);
8349
+ continue;
8350
+ }
8258
8351
  if (!change.mismatch) continue;
8259
8352
  const detail = change.type === "type_upgrade" ? `${change.tableName}.${change.mismatch.column}: expected ${change.mismatch.expected}, found ${change.mismatch.actual} (cannot auto-apply on this database engine)` : change.type === "alter_column" ? `${change.tableName}.${change.mismatch.column}: expected ${change.mismatch.expected}, found ${change.mismatch.actual} (${change.alteration ?? "alter_column"}; ${change.sql?.replace(/^--\s*/, "") ?? "cannot auto-apply on this database engine"})` : `${change.tableName}.${change.mismatch.column}: expected ${change.mismatch.expected}, found ${change.mismatch.actual}`;
8260
8353
  console.log(` ${detail}`);
@@ -8284,6 +8377,8 @@ export default testManifest;
8284
8377
  }
8285
8378
  const columnMigrations = migrations.filter((m) => m.type === "add_column");
8286
8379
  const indexMigrations = migrations.filter((m) => m.type === "add_index");
8380
+ const foreignKeyMigrations = migrations.filter((m) => m.type === "add_foreign_key");
8381
+ const foreignKeyDrops = migrations.filter((m) => m.type === "drop_foreign_key");
8287
8382
  const indexDrops = migrations.filter((m) => m.type === "drop_index");
8288
8383
  const columnAlterations = migrations.filter((m) => m.type === "alter_column");
8289
8384
  const columnDrops = migrations.filter((m) => m.type === "drop_column");
@@ -8312,7 +8407,26 @@ export default testManifest;
8312
8407
  for (const m of indexMigrations) console.log(` ${m.index?.name} on ${m.tableName}`);
8313
8408
  console.log();
8314
8409
  }
8410
+ if (foreignKeyMigrations.length > 0) {
8411
+ console.log(` 🔗 Foreign-key constraints to add: ${foreignKeyMigrations.length}`);
8412
+ for (const m of foreignKeyMigrations) console.log(` ${m.tableName}`);
8413
+ console.log();
8414
+ }
8415
+ if (foreignKeyDrops.length > 0) {
8416
+ console.log(` 🔗 Foreign-key constraints to drop: ${foreignKeyDrops.length}`);
8417
+ for (const m of foreignKeyDrops) console.log(` ${m.tableName}`);
8418
+ console.log();
8419
+ }
8315
8420
  console.log(" SQL Statements:\n");
8421
+ for (const schema of tablePlan.schemas) {
8422
+ const ddl = plannedTableDDL.get(schema.tableName);
8423
+ if (!ddl) continue;
8424
+ for (const sql of [
8425
+ ddl.createTable,
8426
+ ...ddl.indexes,
8427
+ ...ddl.triggers
8428
+ ]) console.log(` ${sql}`);
8429
+ }
8316
8430
  for (const migration of systemTimestampPreview) {
8317
8431
  const terminator = migration.sql.trimEnd().endsWith(";") ? "" : ";";
8318
8432
  console.log(` ${migration.sql}${terminator}`);
@@ -8321,6 +8435,7 @@ export default testManifest;
8321
8435
  const sqlStatements = m.sqlStatements ?? (m.sql ? [m.sql] : []);
8322
8436
  for (const sql of sqlStatements) console.log(` ${sql};`);
8323
8437
  }
8438
+ for (const sql of tablePlan.deferredStatements) console.log(` ${sql}`);
8324
8439
  console.log();
8325
8440
  }
8326
8441
  if (!repairData) {
@@ -8333,13 +8448,13 @@ export default testManifest;
8333
8448
  let skippedCount = 0;
8334
8449
  let errorCount = 0;
8335
8450
  let stiErrorCount = 0;
8336
- const schemaChangeCount = diff.added_tables.length + migrations.length;
8451
+ const schemaChangeCount = diff.added_tables.length + migrations.length + deferredForeignKeyMigrations.length;
8337
8452
  if (applySchemaMigrations && schemaChangeCount > 0) {
8338
8453
  const migrationDefs = [];
8339
8454
  const migrationLogs = /* @__PURE__ */ new Map();
8340
- const engine = tracker.getEngine();
8341
- for (const schema of diff.added_tables) {
8342
- const ddl = generateDDLForEngine(schema, engine);
8455
+ for (const schema of tablePlan.schemas) {
8456
+ const ddl = plannedTableDDL.get(schema.tableName);
8457
+ if (!ddl) throw new Error(`Cannot create table ${schema.tableName}: planned DDL is unavailable.`);
8343
8458
  const createTableSql = ddl.createTable || schema.ddl;
8344
8459
  if (!createTableSql?.trim()) throw new Error(`Cannot create table ${schema.tableName}: schema definition has no generated DDL.`);
8345
8460
  const migrationName = `create_table_${schema.tableName}`;
@@ -8357,6 +8472,18 @@ export default testManifest;
8357
8472
  });
8358
8473
  migrationLogs.set(migrationName, { successMessage: `Created table ${schema.tableName} (${fields} columns)` });
8359
8474
  }
8475
+ for (const migration of deferredForeignKeyMigrations) {
8476
+ const migrationName = getSyntheticMigrationNameForAction(migration);
8477
+ if (!migrationName) continue;
8478
+ migrationDefs.push({
8479
+ id: migrationName,
8480
+ description: `Add deferred foreign-key constraint ${migration.className} on ${migration.tableName}`,
8481
+ version: "1.0.0",
8482
+ up: migration.sql ? [migration.sql] : [],
8483
+ down: []
8484
+ });
8485
+ migrationLogs.set(migrationName, { successMessage: `Added deferred foreign-key constraint ${migration.className} on ${migration.tableName}` });
8486
+ }
8360
8487
  for (const migration of migrations) {
8361
8488
  const migrationName = getSyntheticMigrationNameForAction(migration);
8362
8489
  if (!migrationName) continue;
@@ -8380,11 +8507,17 @@ export default testManifest;
8380
8507
  } else if (migration.type === "drop_index" && migration.indexName) {
8381
8508
  migrationSql = migration.sql || "";
8382
8509
  actionDesc = `Dropped index ${migration.indexName} on ${migration.tableName}`;
8510
+ } else if (migration.type === "add_foreign_key") {
8511
+ migrationSql = migration.sql || "";
8512
+ actionDesc = `Added foreign-key constraint on ${migration.tableName}`;
8513
+ } else if (migration.type === "drop_foreign_key") {
8514
+ migrationSql = migration.sql || "";
8515
+ actionDesc = `Dropped foreign-key constraint on ${migration.tableName}`;
8383
8516
  } else continue;
8384
8517
  const migrationSqlStatements = migration.sqlStatements ?? (migrationSql ? [migrationSql] : []);
8385
8518
  migrationDefs.push({
8386
8519
  id: migrationName,
8387
- description: migration.type === "add_column" ? `Add column ${migration.column?.name} to ${migration.tableName}` : migration.type === "alter_column" ? `Alter column ${migration.columnName ?? migration.column?.name} on ${migration.tableName} (${migration.alteration ?? "alter"}: ${migration.mismatch?.actual ?? "?"} → ${migration.mismatch?.expected ?? "?"})` : migration.type === "drop_column" ? `Drop column ${migration.columnName} from ${migration.tableName}` : migration.type === "type_upgrade" ? `Upgrade column ${migration.column?.name} on ${migration.tableName} from ${migration.mismatch?.actual} to ${migration.mismatch?.expected}` : migration.type === "drop_index" ? `Drop index ${migration.indexName} on ${migration.tableName}` : `Add index ${migration.index?.name} on ${migration.tableName}`,
8520
+ description: migration.type === "add_column" ? `Add column ${migration.column?.name} to ${migration.tableName}` : migration.type === "alter_column" ? `Alter column ${migration.columnName ?? migration.column?.name} on ${migration.tableName} (${migration.alteration ?? "alter"}: ${migration.mismatch?.actual ?? "?"} → ${migration.mismatch?.expected ?? "?"})` : migration.type === "drop_column" ? `Drop column ${migration.columnName} from ${migration.tableName}` : migration.type === "type_upgrade" ? `Upgrade column ${migration.column?.name} on ${migration.tableName} from ${migration.mismatch?.actual} to ${migration.mismatch?.expected}` : migration.type === "drop_index" ? `Drop index ${migration.indexName} on ${migration.tableName}` : migration.type === "add_foreign_key" ? `Add foreign-key constraint on ${migration.tableName}` : migration.type === "drop_foreign_key" ? `Drop foreign-key constraint on ${migration.tableName}` : `Add index ${migration.index?.name} on ${migration.tableName}`,
8388
8521
  version: "1.0.0",
8389
8522
  up: migrationSqlStatements,
8390
8523
  down: []
package/dist/index.js CHANGED
@@ -49,63 +49,63 @@ var _playgroundCommands = null;
49
49
  var _workbenchCommands = null;
50
50
  async function getGnodeCommands() {
51
51
  if (!_gnodeCommands) {
52
- const { gnodeCommands } = await import("./commands-BOw91sjd.js");
52
+ const { gnodeCommands } = await import("./commands-BAKZil1v.js");
53
53
  _gnodeCommands = gnodeCommands;
54
54
  }
55
55
  return _gnodeCommands;
56
56
  }
57
57
  async function getGitCommands() {
58
58
  if (!_gitCommands) {
59
- const { gitCommands } = await import("./commands-BOw91sjd.js");
59
+ const { gitCommands } = await import("./commands-BAKZil1v.js");
60
60
  _gitCommands = gitCommands;
61
61
  }
62
62
  return _gitCommands;
63
63
  }
64
64
  async function getGenerateCommands() {
65
65
  if (!_generateCommands) {
66
- const { generateCommands } = await import("./commands-BOw91sjd.js");
66
+ const { generateCommands } = await import("./commands-BAKZil1v.js");
67
67
  _generateCommands = generateCommands;
68
68
  }
69
69
  return _generateCommands;
70
70
  }
71
71
  async function getInitCommands() {
72
72
  if (!_initCommands) {
73
- const { initCommands } = await import("./commands-BOw91sjd.js");
73
+ const { initCommands } = await import("./commands-BAKZil1v.js");
74
74
  _initCommands = initCommands;
75
75
  }
76
76
  return _initCommands;
77
77
  }
78
78
  async function getUtilityCommands() {
79
79
  if (!_utilityCommands) {
80
- const { utilityCommands } = await import("./commands-BOw91sjd.js");
80
+ const { utilityCommands } = await import("./commands-BAKZil1v.js");
81
81
  _utilityCommands = utilityCommands;
82
82
  }
83
83
  return _utilityCommands;
84
84
  }
85
85
  async function getDispatchCommands() {
86
86
  if (!_dispatchCommands) {
87
- const { dispatchCommands } = await import("./commands-BOw91sjd.js");
87
+ const { dispatchCommands } = await import("./commands-BAKZil1v.js");
88
88
  _dispatchCommands = dispatchCommands;
89
89
  }
90
90
  return _dispatchCommands;
91
91
  }
92
92
  async function getDocsCommands() {
93
93
  if (!_docsCommands) {
94
- const { docsCommands } = await import("./commands-BOw91sjd.js");
94
+ const { docsCommands } = await import("./commands-BAKZil1v.js");
95
95
  _docsCommands = docsCommands;
96
96
  }
97
97
  return _docsCommands;
98
98
  }
99
99
  async function getPlaygroundCommands() {
100
100
  if (!_playgroundCommands) {
101
- const { playgroundCommands } = await import("./commands-BOw91sjd.js");
101
+ const { playgroundCommands } = await import("./commands-BAKZil1v.js");
102
102
  _playgroundCommands = playgroundCommands;
103
103
  }
104
104
  return _playgroundCommands;
105
105
  }
106
106
  async function getWorkbenchCommands() {
107
107
  if (!_workbenchCommands) {
108
- const { workbenchCommands } = await import("./commands-BOw91sjd.js");
108
+ const { workbenchCommands } = await import("./commands-BAKZil1v.js");
109
109
  _workbenchCommands = workbenchCommands;
110
110
  }
111
111
  return _workbenchCommands;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-cli",
3
- "version": "0.42.7",
3
+ "version": "0.43.1",
4
4
  "description": "Developer CLI for SMRT framework - introspection, testing, and project management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,12 +32,12 @@
32
32
  "acorn": "^8.17.0",
33
33
  "fast-glob": "3.3.3",
34
34
  "tar": "^7.5.19",
35
- "@happyvertical/smrt-config": "0.42.7",
36
- "@happyvertical/smrt-core": "0.42.7",
37
- "@happyvertical/smrt-types": "0.42.7",
38
- "@happyvertical/smrt-agents": "0.42.7",
39
- "@happyvertical/smrt-playground": "0.42.7",
40
- "@happyvertical/smrt-dev-mcp": "0.42.7"
35
+ "@happyvertical/smrt-agents": "0.43.1",
36
+ "@happyvertical/smrt-core": "0.43.1",
37
+ "@happyvertical/smrt-config": "0.43.1",
38
+ "@happyvertical/smrt-types": "0.43.1",
39
+ "@happyvertical/smrt-playground": "0.43.1",
40
+ "@happyvertical/smrt-dev-mcp": "0.43.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "24.13.2",