@happyvertical/smrt-core 0.51.39 → 0.53.0

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.
Files changed (122) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +8 -2
  3. package/agents/model-moves.md +114 -0
  4. package/agents/object-runtime.md +3 -1
  5. package/agents/run-once.md +16 -7
  6. package/agents/schema-paths.md +79 -0
  7. package/dist/cascade.d.ts.map +1 -1
  8. package/dist/cascade.js +6 -3
  9. package/dist/cascade.js.map +1 -1
  10. package/dist/collection.d.ts +10 -1
  11. package/dist/collection.d.ts.map +1 -1
  12. package/dist/collection.js +14 -4
  13. package/dist/collection.js.map +1 -1
  14. package/dist/errors.d.ts +18 -0
  15. package/dist/errors.d.ts.map +1 -1
  16. package/dist/errors.js +21 -0
  17. package/dist/errors.js.map +1 -1
  18. package/dist/generators/preflight-route.d.ts.map +1 -1
  19. package/dist/generators/preflight-route.js +1 -1
  20. package/dist/generators/preflight-route.js.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -1
  24. package/dist/knowledge.d.ts.map +1 -1
  25. package/dist/knowledge.js +3 -0
  26. package/dist/knowledge.js.map +1 -1
  27. package/dist/manifest/static-manifest.d.ts.map +1 -1
  28. package/dist/manifest/static-manifest.js +9 -1
  29. package/dist/manifest/static-manifest.js.map +1 -1
  30. package/dist/manifest/store.js +1 -1
  31. package/dist/manifest/store.js.map +1 -1
  32. package/dist/manifest.json +9 -1
  33. package/dist/migrations/differ.d.ts +56 -0
  34. package/dist/migrations/differ.d.ts.map +1 -1
  35. package/dist/migrations/differ.js +152 -27
  36. package/dist/migrations/differ.js.map +1 -1
  37. package/dist/migrations/index.d.ts +1 -0
  38. package/dist/migrations/index.d.ts.map +1 -1
  39. package/dist/migrations/index.js +2 -1
  40. package/dist/migrations/null-equal-indexes.js +1 -1
  41. package/dist/migrations/orchestrate.d.ts +7 -0
  42. package/dist/migrations/orchestrate.d.ts.map +1 -1
  43. package/dist/migrations/orchestrate.js +4 -2
  44. package/dist/migrations/orchestrate.js.map +1 -1
  45. package/dist/migrations/qualified-name-aliases.d.ts +134 -0
  46. package/dist/migrations/qualified-name-aliases.d.ts.map +1 -0
  47. package/dist/migrations/qualified-name-aliases.js +294 -0
  48. package/dist/migrations/qualified-name-aliases.js.map +1 -0
  49. package/dist/migrations/tracker.d.ts +29 -1
  50. package/dist/migrations/tracker.d.ts.map +1 -1
  51. package/dist/migrations/tracker.js +92 -7
  52. package/dist/migrations/tracker.js.map +1 -1
  53. package/dist/migrations.js +2 -1
  54. package/dist/object.d.ts +128 -0
  55. package/dist/object.d.ts.map +1 -1
  56. package/dist/object.js +333 -44
  57. package/dist/object.js.map +1 -1
  58. package/dist/polymorphic-association.d.ts +18 -0
  59. package/dist/polymorphic-association.d.ts.map +1 -1
  60. package/dist/polymorphic-association.js +27 -2
  61. package/dist/polymorphic-association.js.map +1 -1
  62. package/dist/registry/class-registration.d.ts.map +1 -1
  63. package/dist/registry/class-registration.js +27 -4
  64. package/dist/registry/class-registration.js.map +1 -1
  65. package/dist/registry/name-resolver.d.ts +8 -2
  66. package/dist/registry/name-resolver.d.ts.map +1 -1
  67. package/dist/registry/name-resolver.js +17 -8
  68. package/dist/registry/name-resolver.js.map +1 -1
  69. package/dist/registry/qualified-name-aliases.d.ts +109 -0
  70. package/dist/registry/qualified-name-aliases.d.ts.map +1 -0
  71. package/dist/registry/qualified-name-aliases.js +286 -0
  72. package/dist/registry/qualified-name-aliases.js.map +1 -0
  73. package/dist/registry/relationship-graph.d.ts.map +1 -1
  74. package/dist/registry/relationship-graph.js +1 -1
  75. package/dist/registry/relationship-graph.js.map +1 -1
  76. package/dist/registry/schema-builder.d.ts.map +1 -1
  77. package/dist/registry/schema-builder.js +7 -5
  78. package/dist/registry/schema-builder.js.map +1 -1
  79. package/dist/registry/types.d.ts +26 -0
  80. package/dist/registry/types.d.ts.map +1 -1
  81. package/dist/registry.d.ts +127 -6
  82. package/dist/registry.d.ts.map +1 -1
  83. package/dist/registry.js +234 -14
  84. package/dist/registry.js.map +1 -1
  85. package/dist/scanner/index.d.ts +1 -1
  86. package/dist/scanner/index.d.ts.map +1 -1
  87. package/dist/scanner/manifest-generator.d.ts +14 -2
  88. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  89. package/dist/scanner/manifest-generator.js +48 -9
  90. package/dist/scanner/manifest-generator.js.map +1 -1
  91. package/dist/scanner/types.d.ts +21 -0
  92. package/dist/scanner/types.d.ts.map +1 -1
  93. package/dist/scanner/types.js.map +1 -1
  94. package/dist/schema/conflict-target.d.ts +68 -11
  95. package/dist/schema/conflict-target.d.ts.map +1 -1
  96. package/dist/schema/conflict-target.js +75 -12
  97. package/dist/schema/conflict-target.js.map +1 -1
  98. package/dist/schema/foreign-key-ddl.d.ts +17 -1
  99. package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
  100. package/dist/schema/foreign-key-ddl.js +29 -3
  101. package/dist/schema/foreign-key-ddl.js.map +1 -1
  102. package/dist/schema/foreign-key-policy.d.ts +8 -0
  103. package/dist/schema/foreign-key-policy.d.ts.map +1 -1
  104. package/dist/schema/foreign-key-policy.js +2 -1
  105. package/dist/schema/foreign-key-policy.js.map +1 -1
  106. package/dist/schema/generator.d.ts +7 -0
  107. package/dist/schema/generator.d.ts.map +1 -1
  108. package/dist/schema/generator.js +18 -10
  109. package/dist/schema/generator.js.map +1 -1
  110. package/dist/schema/text-cast-probe.d.ts +50 -1
  111. package/dist/schema/text-cast-probe.d.ts.map +1 -1
  112. package/dist/schema/text-cast-probe.js +46 -11
  113. package/dist/schema/text-cast-probe.js.map +1 -1
  114. package/dist/schema/types.d.ts +6 -0
  115. package/dist/schema/types.d.ts.map +1 -1
  116. package/dist/scripts/browser-plain-json-regression.mjs +4 -1
  117. package/dist/smrt-knowledge.json +22 -11
  118. package/dist/utils/qualified-names.d.ts +11 -0
  119. package/dist/utils/qualified-names.d.ts.map +1 -1
  120. package/dist/utils/qualified-names.js +21 -1
  121. package/dist/utils/qualified-names.js.map +1 -1
  122. package/package.json +11 -11
@@ -19,6 +19,7 @@ export { buildIntegerWidthStatements, buildIntegerWidthTableStatements, collectI
19
19
  export { buildMinorUnitsStatements, type MinorUnitsColumnReport, type MinorUnitsColumnState, type MinorUnitsOptions, MinorUnitsPreflightError, type MinorUnitsPreflightResult, type MinorUnitsProblemRow, type MinorUnitsRescaleOptions, type MinorUnitsRescaleResult, type MoneyColumnTarget, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits, } from './minor-units.js';
20
20
  export { collectNullEqualIndexTargets, migrateNullEqualIndexes, type NullEqualIndexOptions, type NullEqualIndexPreflight, type NullEqualIndexReport, type NullEqualIndexTarget, nullEqualDuplicateDetector, nullEqualIndexStatements, preflightNullEqualIndexes, } from './null-equal-indexes.js';
21
21
  export { getPendingSchemaStatements, type MigrateSmrtSchemasOptions, type MigrateSmrtSchemasResult, migrateSmrtSchemas, type PendingSchemaStatementsResult, } from './orchestrate.js';
22
+ export { backfillLegacyQualifiedNames, collectLegacyQualifiedNameTargets, countLegacyQualifiedNameReferences, LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, type LegacyQualifiedNameBackfillOptions, type LegacyQualifiedNameBackfillResult, type LegacyQualifiedNameOptions, type LegacyQualifiedNameReference, type LegacyQualifiedNameReport, type LegacyQualifiedNameTarget, legacyQualifiedNamesBackfillName, } from './qualified-name-aliases.js';
22
23
  export { type BuildSqliteRebuildStatementsInput, buildSqliteRebuildStatements, isSqliteRebuildPlaceholder, type PlanSqliteTableRebuildsOptions, planSqliteTableRebuilds, rewriteSqliteCreateTable, SQLITE_REBUILD_TABLE_PREFIX, type SqliteTableRebuildPlan, sqliteRebuildPlaceholderSql, } from './sqlite-rebuild.js';
23
24
  export { buildConcurrentIndexPlan, extractCreatedIndexName, MigrationTracker, parsePostgresTimeoutMs, planPostgresStatements, } from './tracker.js';
24
25
  export type { ApplyMigrationsOptions, ColumnDefinitionWithName, ColumnInfo, DatabaseEngine, DatabaseInterface, DataMigration, DataMigrationResult, DataMigrationStatement, DriftReport, ForeignKeyInfo, IndexDefinition, IndexInfo, MigrationDefinition, MigrationMode, MigrationResult, MigrationStatus, MigrationsConfig, MigrationTrackerOptions, ParsedMigrationFile, ParseMigrationOptions, RollbackOptions, SchemaChange, SchemaDiff, SchemaMigrationRecord, StiDiscriminatorMigration, TableSchemaInfo, } from './types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"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"}
@@ -10,4 +10,5 @@ import { MinorUnitsPreflightError, buildMinorUnitsStatements, preflightMinorUnit
10
10
  import { collectNullEqualIndexTargets, migrateNullEqualIndexes, nullEqualDuplicateDetector, nullEqualIndexStatements, preflightNullEqualIndexes } from "./null-equal-indexes.js";
11
11
  import { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, planPostgresStatements } from "./tracker.js";
12
12
  import { getPendingSchemaStatements, migrateSmrtSchemas } from "./orchestrate.js";
13
- export { BackfillTableUnavailableError, BackfillTracker, FRAMEWORK_BASE_TABLE_NAMES, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, REQUIRED_COLUMN_NOT_ADDED, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, collectNullEqualIndexTargets, computeChecksum, createMigrationDefinition, dropFrameworkBaseTables, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, migrateNullEqualIndexes, migrateSmrtSchemas, normalizeSQL, nullEqualDuplicateDetector, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, preflightNullEqualIndexes, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
13
+ import { LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, backfillLegacyQualifiedNames, collectLegacyQualifiedNameTargets, countLegacyQualifiedNameReferences, legacyQualifiedNamesBackfillName } from "./qualified-name-aliases.js";
14
+ export { BackfillTableUnavailableError, BackfillTracker, FRAMEWORK_BASE_TABLE_NAMES, LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, REQUIRED_COLUMN_NOT_ADDED, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, backfillLegacyQualifiedNames, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, collectLegacyQualifiedNameTargets, collectNullEqualIndexTargets, computeChecksum, countLegacyQualifiedNameReferences, createMigrationDefinition, dropFrameworkBaseTables, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, legacyQualifiedNamesBackfillName, migrateNullEqualIndexes, migrateSmrtSchemas, normalizeSQL, nullEqualDuplicateDetector, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, preflightNullEqualIndexes, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
@@ -1,8 +1,8 @@
1
+ import { nullableConflictIdentity } from "../schema/conflict-target.js";
1
2
  import { toSafeInteger } from "../utils/safe-integer.js";
2
3
  import { quoteIdentifier } from "../schema/sql-identifiers.js";
3
4
  import { detectEngine } from "../schema/ddl/index.js";
4
5
  import { parsePostgresTimeoutMs } from "../postgres-timeouts.js";
5
- import { nullableConflictIdentity } from "../schema/conflict-target.js";
6
6
  //#region src/migrations/null-equal-indexes.ts
7
7
  /** Metadata is the authority; optional business UNIQUE indexes are never inferred. */
8
8
  function collectNullEqualIndexTargets(schemas) {
@@ -68,6 +68,12 @@ export interface MigrateSmrtSchemasOptions {
68
68
  postgresTimestampMigration?: {
69
69
  legacyTimezone: 'UTC';
70
70
  };
71
+ /**
72
+ * Store empty or whitespace-only text as NULL when converging a nullable
73
+ * legacy `text` column to a typed column (#3226). See
74
+ * `DiffOptions.emptyTextAsNull`.
75
+ */
76
+ emptyTextAsNull?: boolean;
71
77
  }
72
78
  export interface MigrateSmrtSchemasResult {
73
79
  applied: boolean;
@@ -130,6 +136,7 @@ export declare function getPendingSchemaStatements(db: DatabaseInterface, option
130
136
  postgresTimestampMigration?: {
131
137
  legacyTimezone: 'UTC';
132
138
  };
139
+ emptyTextAsNull?: boolean;
133
140
  }): Promise<PendingSchemaStatementsResult>;
134
141
  /**
135
142
  * Apply any pending schema changes from `ObjectRegistry` to `db`.
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrate.d.ts","sourceRoot":"","sources":["../../src/migrations/orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EACL,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AAOrB,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,iBAAiB,CAAC;IACtB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wFAAwF;IACxF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;OAGG;IACH,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;CACxD;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC;IACrD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;OAOG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;CACnD,GACL,OAAO,CAAC,6BAA6B,CAAC,CAqBxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA0DnC;AAcD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,EACpD,EAAE,EAAE,iBAAiB,EACrB,UAAU,CAAC,EAAE,cAAc,GAC1B,MAAM,EAAE,CAwCV"}
1
+ {"version":3,"file":"orchestrate.d.ts","sourceRoot":"","sources":["../../src/migrations/orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EACL,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AAOrB,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,iBAAiB,CAAC;IACtB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wFAAwF;IACxF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;OAGG;IACH,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;IACvD;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC;IACrD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;OAOG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;IACvD,eAAe,CAAC,EAAE,OAAO,CAAC;CACtB,GACL,OAAO,CAAC,6BAA6B,CAAC,CAsBxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA2DnC;AAcD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,EACpD,EAAE,EAAE,iBAAiB,EACrB,UAAU,CAAC,EAAE,cAAc,GAC1B,MAAM,EAAE,CAwCV"}
@@ -25,7 +25,8 @@ async function getPendingSchemaStatements(db, options = {}) {
25
25
  const schemas = ObjectRegistry.getAllSchemasAsDefinitions();
26
26
  const diff = await generateSchemaDiff(db, schemas, {
27
27
  engineHint: options.engineHint,
28
- postgresTimestampMigration: options.postgresTimestampMigration
28
+ postgresTimestampMigration: options.postgresTimestampMigration,
29
+ emptyTextAsNull: options.emptyTextAsNull
29
30
  });
30
31
  const statements = collectStatementsFromDiff(diff, db, options.engineHint);
31
32
  const unactionableChanges = collectUnactionableChanges(diff);
@@ -76,7 +77,8 @@ async function getPendingSchemaStatements(db, options = {}) {
76
77
  async function migrateSmrtSchemas(options) {
77
78
  const pending = await getPendingSchemaStatements(options.db, {
78
79
  engineHint: options.engineHint,
79
- postgresTimestampMigration: options.postgresTimestampMigration
80
+ postgresTimestampMigration: options.postgresTimestampMigration,
81
+ emptyTextAsNull: options.emptyTextAsNull
80
82
  });
81
83
  if (!pending.hasChanges || pending.statements.length === 0) return {
82
84
  applied: false,
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrate.js","names":[],"sources":["../../src/migrations/orchestrate.ts"],"sourcesContent":["/**\n * High-level \"make my SMRT app's schema match its database\" orchestration.\n *\n * Wraps `ObjectRegistry.getAllSchemasAsDefinitions()` → `generateSchemaDiff`\n * → `MigrationTracker.applyAll`. Apps that just want to run pending\n * schema changes call `migrateSmrtSchemas({ db, packageName, version })`\n * and don't have to assemble the migration definition by hand.\n *\n * The migration name is timestamped, so re-running on an up-to-date\n * database is a no-op (the diff is empty, no statements are generated,\n * `applied: false` is returned).\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ObjectRegistry } from '../registry.js';\nimport { detectEngine, getDDLStrategy } from '../schema/ddl/index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport { planForeignKeyCreation } from '../schema/foreign-key-planner.js';\nimport type { MigrationResult, SchemaChange } from '../schema/types.js';\nimport {\n generateSchemaDiff,\n getSQLFromChanges,\n hasActionableChanges,\n} from './differ.js';\nimport {\n createMigrationDefinition,\n generateMigrationTimestamp,\n} from './generator.js';\nimport { MigrationTracker } from './tracker.js';\n\nexport interface MigrateSmrtSchemasOptions {\n db: DatabaseInterface;\n /** Stored on the migration record so the audit log shows which app applied it. */\n packageName: string;\n /**\n * Stored on the migration record. Typically the app's package.json\n * version. Optional because most apps don't have a runtime-readable\n * version handy and would hardcode a stale string if forced to supply\n * one — `'unknown'` is honest. Pass an explicit value when you can\n * (e.g. `version: process.env.APP_VERSION`).\n */\n version?: string;\n /** Override the description on the synthetic migration definition. */\n description?: string;\n /** Override the migration name (default: `<timestamp>_smrt_schema_sync`). */\n name?: string;\n /** Forwarded to MigrationTracker — defaults to true for pg-safe runs. */\n postgresSafe?: boolean;\n /** Forwarded to MigrationTracker — defaults to true. */\n reconcile?: boolean;\n /** Forwarded to MigrationTracker — defaults to true for `CREATE INDEX CONCURRENTLY`. */\n useConcurrentIndexes?: boolean;\n /**\n * Forwarded to MigrationTracker (Postgres lock timeout, milliseconds).\n * Lets apps that customize tracker timeouts directly preserve their\n * tuning when going through the orchestrator. Falls back to the\n * tracker's default (30000ms) when omitted.\n */\n lockTimeout?: number;\n /**\n * Forwarded to MigrationTracker (Postgres statement timeout, ms). Falls\n * back to the tracker's default (60000ms) when omitted.\n */\n statementTimeout?: number;\n /**\n * Explicit engine hint forwarded to `detectEngine`, `SchemaComparer`, and\n * `MigrationTracker`. Useful when `db.url` doesn't unambiguously identify\n * the engine (e.g. the JSON adapter, which uses DuckDB internally and may\n * not be recognizable by URL alone). All three layers honor the same hint\n * so the generated DDL, drift comparison, and execution path stay\n * consistent — without it, an empty-URL connection could produce\n * Postgres-flavored DDL but run through the SQLite tracker path.\n *\n * Typed as `DatabaseEngine` so unsupported values (e.g. `'mysql'`) fail at\n * compile time rather than silently falling back to `'sqlite'` at runtime.\n */\n engineHint?: DatabaseEngine;\n /**\n * Confirm that historical PostgreSQL Date writers used UTC wall times before\n * reinterpreting legacy TIMESTAMP/TEXT/JSON columns as TIMESTAMPTZ.\n */\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n}\n\nexport interface MigrateSmrtSchemasResult {\n applied: boolean;\n results: MigrationResult[];\n statements: string[];\n schemaCount: number;\n /**\n * Schema changes the orchestrator detected but cannot apply automatically\n * (incompatible type mismatches, SQLite type upgrades that require table\n * recreation, etc.). Empty when the schema is fully reconciled. Inspect\n * this to surface a \"manual migration required\" warning in CLI output.\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Lets a caller write\n * `if (result.hasManualDrift) ...` without iterating the array first,\n * and makes the case visible to anyone scanning the result shape (the\n * full array is easy to gloss over when `applied: false`).\n */\n hasManualDrift: boolean;\n}\n\nexport interface PendingSchemaStatementsResult {\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>;\n statements: string[];\n schemaCount: number;\n hasChanges: boolean;\n /**\n * Schema changes the differ detected but that have no executable SQL\n * (incompatible `type_mismatch` entries, or `type_upgrade` entries whose\n * only generated SQL is an advisory comment because the engine cannot\n * upgrade the column in place — e.g. SQLite type widening). Surfacing\n * these lets status commands distinguish \"no drift\" from \"drift the\n * orchestrator can't fix on its own.\"\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Mirrors the\n * `MigrateSmrtSchemasResult.hasManualDrift` shortcut.\n */\n hasManualDrift: boolean;\n}\n\n/**\n * Compute the SQL statements needed to bring `db` in sync with the\n * currently-registered SMRT object schemas. Pure inspection — no writes.\n *\n * Useful for status commands (\"how far behind is the schema?\") and as the\n * inner step of `migrateSmrtSchemas`.\n *\n * **Note on the returned `statements`:** these are the engine-correct DDL\n * statements as the differ + DDL strategy produce them, *prior to* any\n * Postgres-specific rewrites the `MigrationTracker` applies at execution\n * time (e.g. rewriting `CREATE INDEX` → `CREATE INDEX CONCURRENTLY` and\n * moving CONCURRENTLY statements outside the surrounding transaction).\n * The list is suitable for preview/status/\"what would change?\" use cases.\n * For the exact byte-for-byte SQL the tracker ran, inspect the\n * `MigrationResult`s from `migrateSmrtSchemas` or the tracker's own logs.\n */\nexport async function getPendingSchemaStatements(\n db: DatabaseInterface,\n options: {\n engineHint?: DatabaseEngine;\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n } = {},\n): Promise<PendingSchemaStatementsResult> {\n const schemas = ObjectRegistry.getAllSchemasAsDefinitions();\n // Forward engineHint into the diff itself so the SchemaComparer's\n // existing-table SQL (ALTER/index drift) uses the same DDL strategy\n // we use for newly-added tables — otherwise the two halves of the\n // statement list can land on different engines (split-brain) when\n // `db.url` is empty or ambiguous.\n const diff = await generateSchemaDiff(db, schemas, {\n engineHint: options.engineHint,\n postgresTimestampMigration: options.postgresTimestampMigration,\n });\n const statements = collectStatementsFromDiff(diff, db, options.engineHint);\n const unactionableChanges = collectUnactionableChanges(diff);\n return {\n diff,\n schemaCount: Object.keys(schemas).length,\n statements,\n hasChanges: hasActionableChanges(diff),\n unactionableChanges,\n hasManualDrift: unactionableChanges.length > 0,\n };\n}\n\n/**\n * Apply any pending schema changes from `ObjectRegistry` to `db`.\n *\n * Returns `applied: false` (with no error) if the database is already in\n * sync. Throws on the first migration failure with the underlying error\n * preserved (callers should let it propagate to the CLI).\n *\n * **Note on the returned `statements`:** mirrors `getPendingSchemaStatements`\n * — the list is the planned DDL, prior to Postgres-specific tracker rewrites\n * (CONCURRENTLY, transaction reordering). The actual executed SQL is\n * tracked in the returned `results`.\n *\n * **Note on `applied` semantics under `reconcile: true` (the default):**\n * the tracker may re-execute a migration that is already in `completed`\n * state when the checksum matches — that counts as `applied: true` in the\n * returned `MigrationResult`. So `result.applied === true` means \"the\n * tracker ran the migration\", not specifically \"the database changed for\n * the first time.\" Pass `reconcile: false` if you need to distinguish a\n * fresh apply from a reconcile-replay over an existing completed record.\n *\n * **Note on repeated failures:** the migration name defaults to a fresh\n * timestamp on every call, so a sequence of failed runs leaves one\n * `failed` row in `_smrt_schema_migrations` per attempt. Pass an explicit\n * `name` if you want to overwrite the same record across retries.\n *\n * **Note on `unactionableChanges`:** the differ can detect schema drift\n * the migration step can't auto-resolve — incompatible type mismatches,\n * SQLite type widening that needs table recreation, etc. Those changes\n * appear in `result.unactionableChanges` and are NOT applied (the\n * statements list is filtered to executable DDL only). When the diff\n * contains *only* unactionable changes, the function returns\n * `applied: false` with a populated `unactionableChanges` so callers can\n * surface a \"manual migration required\" signal. Without inspecting that\n * field, the result would look indistinguishable from \"already in sync.\"\n */\nexport async function migrateSmrtSchemas(\n options: MigrateSmrtSchemasOptions,\n): Promise<MigrateSmrtSchemasResult> {\n const pending = await getPendingSchemaStatements(options.db, {\n engineHint: options.engineHint,\n postgresTimestampMigration: options.postgresTimestampMigration,\n });\n if (!pending.hasChanges || pending.statements.length === 0) {\n return {\n applied: false,\n results: [],\n statements: [],\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n }\n\n const tracker = new MigrationTracker({\n db: options.db,\n engineHint: options.engineHint,\n lockTimeout: options.lockTimeout,\n statementTimeout: options.statementTimeout,\n useConcurrentIndexes: options.useConcurrentIndexes ?? true,\n });\n const migration = createMigrationDefinition(\n options.name ?? `${generateMigrationTimestamp()}_smrt_schema_sync`,\n pending.statements,\n [],\n {\n description: options.description ?? 'Synchronize SMRT object schemas',\n packageName: options.packageName,\n version: options.version ?? 'unknown',\n },\n );\n\n const results = await tracker.applyAll([migration], {\n postgresSafe: options.postgresSafe ?? true,\n reconcile: options.reconcile ?? true,\n });\n\n const failed = results.find((result) => !result.success);\n if (failed) {\n throw failed.error instanceof Error\n ? failed.error\n : new Error(String(failed.error ?? `Migration ${failed.name} failed`));\n }\n\n const applied = results.some(\n (result) => result.applied !== false && result.skipped !== true,\n );\n\n return {\n applied,\n results,\n statements: pending.statements,\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n}\n\n/** Mirror of the `getDatabaseUrl` helper in class.ts — some adapters\n * (JSON, certain in-memory wrappers) leave `db.url` undefined and expose\n * the URL on `db.config?.url` instead. Detecting the engine from a raw\n * `db.url` would throw or silently fall through to the sqlite default.\n */\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n return db.url || dbWithConfig.config?.url || '';\n}\n\n/**\n * Materialize the diff into the engine-correct SQL statements.\n *\n * For new tables we delegate to the engine's `DDLStrategy` so column types\n * (`REAL` → `DOUBLE PRECISION` on Postgres, `JSON` → `JSONB`, etc.),\n * partial-index `WHERE` clauses, and trigger syntax all match what the\n * SchemaComparer would expect on subsequent runs. Emitting the raw\n * abstract `SchemaDefinition.ddl` here would cause immediate type-drift\n * migrations on Postgres/DuckDB.\n *\n * Column/index/trigger changes for existing tables continue to come from\n * `getSQLFromDiff`, which is already engine-aware via the differ's per-\n * change `sqlStatements`.\n *\n * Exported for statement-ordering tests; it is not part of the package's\n * public entry point (`migrations/index.ts` re-exports names explicitly).\n *\n * @param diff - Schema diff to materialize.\n * @param db - Database whose URL selects the DDL strategy.\n * @param engineHint - Explicit engine when `db.url` is empty or ambiguous.\n * @returns Ordered, executable DDL statements.\n */\nexport function collectStatementsFromDiff(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n db: DatabaseInterface,\n engineHint?: DatabaseEngine,\n): string[] {\n const strategy = getDDLStrategy(\n detectEngine(resolveDatabaseUrl(db), engineHint),\n );\n const statements: string[] = [];\n const tablePlan = planForeignKeyCreation(diff.added_tables, strategy.engine);\n // #2608: the pre-R11 uuid convergence has to precede *every* foreign-key\n // statement. `planForeignKeyCreation` only defers the constraints inside a\n // mutual cycle, so an acyclic new child table keeps its FK **inline in\n // CREATE TABLE** — a fresh `uuid` child pointing at a legacy `text` parent\n // would fail with SQLSTATE 42804 before the parent could be converted.\n // Conversions only ever rewrite columns that already exist, so they are\n // safe to run ahead of every CREATE TABLE in this batch.\n statements.push(\n ...getSQLFromChanges(\n diff.changes.filter((change) => change.phase === 'pre_foreign_key'),\n ),\n );\n for (const schema of tablePlan.schemas) {\n statements.push(strategy.generateCreateTable(schema));\n statements.push(...strategy.generateIndexes(schema));\n statements.push(...strategy.generateTriggers(schema));\n }\n statements.push(...tablePlan.deferredStatements);\n statements.push(\n ...getSQLFromChanges(\n diff.changes.filter((change) => change.phase !== 'pre_foreign_key'),\n ),\n );\n // Drop empty and comment-only entries. SQLite type-widening upgrades\n // surface as `-- SQLite: Type upgrade for X requires table recreation`\n // — passing those through to the tracker records a successful migration\n // without actually fixing the column, so the same drift re-appears on\n // every subsequent run. Those changes are surfaced separately via\n // `unactionableChanges` so callers can prompt for manual remediation.\n return statements.filter((statement) => {\n const trimmed = statement.trim();\n if (trimmed.length === 0) return false;\n return !isCommentOnlySql(trimmed);\n });\n}\n\n/**\n * True if `sql` contains no executable DDL — only line comments (`--`)\n * and/or block comments (`/* … *​/`). Used to discard advisory comments\n * the differ emits when a column upgrade can't be performed in place\n * (e.g. SQLite type widening). Defensive: strips block comments first so\n * a future differ that emits `/* type_upgrade: no-op *​/` doesn't slip\n * through.\n */\nfunction isCommentOnlySql(sql: string): boolean {\n // Strip /* ... */ block comments (non-greedy, spans newlines).\n const withoutBlockComments = sql.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n const lines = withoutBlockComments\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n if (lines.length === 0) return sql.trim().length > 0;\n return lines.every((line) => line.startsWith('--'));\n}\n\n/**\n * True if `sql` is an advisory comment that says *nothing needs to happen* —\n * as opposed to one that says a human has to act.\n *\n * The differ emits both kinds. \"This SQLite column already stores JSON as\n * TEXT\" and \"#2370: this column is rebuilt together with its sibling\" are\n * no-ops; \"requires table recreation\" / \"blocked: …\" are real manual drift.\n * Treating the first kind as drift makes a fully auto-repairable migration\n * report `hasManualDrift: true`.\n *\n * Keep in sync with `classifyTypeUpgradeSql`'s `noop` arm in\n * `@happyvertical/smrt-cli` (`src/commands/db-migrate-actions.ts`), which\n * applies the same rule on the CLI side.\n */\nfunction isNoOpAdvisorySql(sql: string): boolean {\n const trimmed = sql.trim();\n return (\n /no change needed/i.test(trimmed) || /already stores .* as /i.test(trimmed)\n );\n}\n\n/**\n * Surface changes the differ produced but that the migrator cannot apply\n * automatically — `type_mismatch` entries (the differ explicitly gives up on\n * these), `type_upgrade`/`alter_column` entries whose generated SQL is\n * advisory-comment-only (a SQLite rebuild that would be unsafe, an in-place\n * constraint change SQLite cannot perform), and warning-level report-only\n * advisories (an orphan NOT NULL column that will break inserts, a required\n * column that could not be enforced, a stale unique constraint, a relaxation\n * the caller has not opted into). Info-level advisories (a harmless orphan\n * column, a stale default) and comments that mean \"already handled\" (the\n * no-op advisories a table rebuild leaves on covered siblings, #2370) are\n * deliberately excluded so `hasManualDrift` keeps meaning \"something needs\n * an operator\". Callers use this to distinguish \"schema is in sync\" from\n * \"schema is drifted but we can't fix it from here\".\n */\nfunction collectUnactionableChanges(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n): SchemaChange[] {\n const unactionable: SchemaChange[] = [];\n for (const change of diff.changes) {\n if (change.type === 'type_mismatch') {\n unactionable.push(change);\n continue;\n }\n const statements = change.sqlStatements ?? (change.sql ? [change.sql] : []);\n if (statements.length === 0) {\n if (change.advisory?.severity === 'warning') {\n unactionable.push(change);\n }\n continue;\n }\n if (\n statements.every((stmt) => isCommentOnlySql(stmt.trim())) &&\n !statements.every((stmt) => isNoOpAdvisorySql(stmt))\n ) {\n unactionable.push(change);\n }\n }\n return unactionable;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"orchestrate.js","names":[],"sources":["../../src/migrations/orchestrate.ts"],"sourcesContent":["/**\n * High-level \"make my SMRT app's schema match its database\" orchestration.\n *\n * Wraps `ObjectRegistry.getAllSchemasAsDefinitions()` → `generateSchemaDiff`\n * → `MigrationTracker.applyAll`. Apps that just want to run pending\n * schema changes call `migrateSmrtSchemas({ db, packageName, version })`\n * and don't have to assemble the migration definition by hand.\n *\n * The migration name is timestamped, so re-running on an up-to-date\n * database is a no-op (the diff is empty, no statements are generated,\n * `applied: false` is returned).\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ObjectRegistry } from '../registry.js';\nimport { detectEngine, getDDLStrategy } from '../schema/ddl/index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport { planForeignKeyCreation } from '../schema/foreign-key-planner.js';\nimport type { MigrationResult, SchemaChange } from '../schema/types.js';\nimport {\n generateSchemaDiff,\n getSQLFromChanges,\n hasActionableChanges,\n} from './differ.js';\nimport {\n createMigrationDefinition,\n generateMigrationTimestamp,\n} from './generator.js';\nimport { MigrationTracker } from './tracker.js';\n\nexport interface MigrateSmrtSchemasOptions {\n db: DatabaseInterface;\n /** Stored on the migration record so the audit log shows which app applied it. */\n packageName: string;\n /**\n * Stored on the migration record. Typically the app's package.json\n * version. Optional because most apps don't have a runtime-readable\n * version handy and would hardcode a stale string if forced to supply\n * one — `'unknown'` is honest. Pass an explicit value when you can\n * (e.g. `version: process.env.APP_VERSION`).\n */\n version?: string;\n /** Override the description on the synthetic migration definition. */\n description?: string;\n /** Override the migration name (default: `<timestamp>_smrt_schema_sync`). */\n name?: string;\n /** Forwarded to MigrationTracker — defaults to true for pg-safe runs. */\n postgresSafe?: boolean;\n /** Forwarded to MigrationTracker — defaults to true. */\n reconcile?: boolean;\n /** Forwarded to MigrationTracker — defaults to true for `CREATE INDEX CONCURRENTLY`. */\n useConcurrentIndexes?: boolean;\n /**\n * Forwarded to MigrationTracker (Postgres lock timeout, milliseconds).\n * Lets apps that customize tracker timeouts directly preserve their\n * tuning when going through the orchestrator. Falls back to the\n * tracker's default (30000ms) when omitted.\n */\n lockTimeout?: number;\n /**\n * Forwarded to MigrationTracker (Postgres statement timeout, ms). Falls\n * back to the tracker's default (60000ms) when omitted.\n */\n statementTimeout?: number;\n /**\n * Explicit engine hint forwarded to `detectEngine`, `SchemaComparer`, and\n * `MigrationTracker`. Useful when `db.url` doesn't unambiguously identify\n * the engine (e.g. the JSON adapter, which uses DuckDB internally and may\n * not be recognizable by URL alone). All three layers honor the same hint\n * so the generated DDL, drift comparison, and execution path stay\n * consistent — without it, an empty-URL connection could produce\n * Postgres-flavored DDL but run through the SQLite tracker path.\n *\n * Typed as `DatabaseEngine` so unsupported values (e.g. `'mysql'`) fail at\n * compile time rather than silently falling back to `'sqlite'` at runtime.\n */\n engineHint?: DatabaseEngine;\n /**\n * Confirm that historical PostgreSQL Date writers used UTC wall times before\n * reinterpreting legacy TIMESTAMP/TEXT/JSON columns as TIMESTAMPTZ.\n */\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n /**\n * Store empty or whitespace-only text as NULL when converging a nullable\n * legacy `text` column to a typed column (#3226). See\n * `DiffOptions.emptyTextAsNull`.\n */\n emptyTextAsNull?: boolean;\n}\n\nexport interface MigrateSmrtSchemasResult {\n applied: boolean;\n results: MigrationResult[];\n statements: string[];\n schemaCount: number;\n /**\n * Schema changes the orchestrator detected but cannot apply automatically\n * (incompatible type mismatches, SQLite type upgrades that require table\n * recreation, etc.). Empty when the schema is fully reconciled. Inspect\n * this to surface a \"manual migration required\" warning in CLI output.\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Lets a caller write\n * `if (result.hasManualDrift) ...` without iterating the array first,\n * and makes the case visible to anyone scanning the result shape (the\n * full array is easy to gloss over when `applied: false`).\n */\n hasManualDrift: boolean;\n}\n\nexport interface PendingSchemaStatementsResult {\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>;\n statements: string[];\n schemaCount: number;\n hasChanges: boolean;\n /**\n * Schema changes the differ detected but that have no executable SQL\n * (incompatible `type_mismatch` entries, or `type_upgrade` entries whose\n * only generated SQL is an advisory comment because the engine cannot\n * upgrade the column in place — e.g. SQLite type widening). Surfacing\n * these lets status commands distinguish \"no drift\" from \"drift the\n * orchestrator can't fix on its own.\"\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Mirrors the\n * `MigrateSmrtSchemasResult.hasManualDrift` shortcut.\n */\n hasManualDrift: boolean;\n}\n\n/**\n * Compute the SQL statements needed to bring `db` in sync with the\n * currently-registered SMRT object schemas. Pure inspection — no writes.\n *\n * Useful for status commands (\"how far behind is the schema?\") and as the\n * inner step of `migrateSmrtSchemas`.\n *\n * **Note on the returned `statements`:** these are the engine-correct DDL\n * statements as the differ + DDL strategy produce them, *prior to* any\n * Postgres-specific rewrites the `MigrationTracker` applies at execution\n * time (e.g. rewriting `CREATE INDEX` → `CREATE INDEX CONCURRENTLY` and\n * moving CONCURRENTLY statements outside the surrounding transaction).\n * The list is suitable for preview/status/\"what would change?\" use cases.\n * For the exact byte-for-byte SQL the tracker ran, inspect the\n * `MigrationResult`s from `migrateSmrtSchemas` or the tracker's own logs.\n */\nexport async function getPendingSchemaStatements(\n db: DatabaseInterface,\n options: {\n engineHint?: DatabaseEngine;\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n emptyTextAsNull?: boolean;\n } = {},\n): Promise<PendingSchemaStatementsResult> {\n const schemas = ObjectRegistry.getAllSchemasAsDefinitions();\n // Forward engineHint into the diff itself so the SchemaComparer's\n // existing-table SQL (ALTER/index drift) uses the same DDL strategy\n // we use for newly-added tables — otherwise the two halves of the\n // statement list can land on different engines (split-brain) when\n // `db.url` is empty or ambiguous.\n const diff = await generateSchemaDiff(db, schemas, {\n engineHint: options.engineHint,\n postgresTimestampMigration: options.postgresTimestampMigration,\n emptyTextAsNull: options.emptyTextAsNull,\n });\n const statements = collectStatementsFromDiff(diff, db, options.engineHint);\n const unactionableChanges = collectUnactionableChanges(diff);\n return {\n diff,\n schemaCount: Object.keys(schemas).length,\n statements,\n hasChanges: hasActionableChanges(diff),\n unactionableChanges,\n hasManualDrift: unactionableChanges.length > 0,\n };\n}\n\n/**\n * Apply any pending schema changes from `ObjectRegistry` to `db`.\n *\n * Returns `applied: false` (with no error) if the database is already in\n * sync. Throws on the first migration failure with the underlying error\n * preserved (callers should let it propagate to the CLI).\n *\n * **Note on the returned `statements`:** mirrors `getPendingSchemaStatements`\n * — the list is the planned DDL, prior to Postgres-specific tracker rewrites\n * (CONCURRENTLY, transaction reordering). The actual executed SQL is\n * tracked in the returned `results`.\n *\n * **Note on `applied` semantics under `reconcile: true` (the default):**\n * the tracker may re-execute a migration that is already in `completed`\n * state when the checksum matches — that counts as `applied: true` in the\n * returned `MigrationResult`. So `result.applied === true` means \"the\n * tracker ran the migration\", not specifically \"the database changed for\n * the first time.\" Pass `reconcile: false` if you need to distinguish a\n * fresh apply from a reconcile-replay over an existing completed record.\n *\n * **Note on repeated failures:** the migration name defaults to a fresh\n * timestamp on every call, so a sequence of failed runs leaves one\n * `failed` row in `_smrt_schema_migrations` per attempt. Pass an explicit\n * `name` if you want to overwrite the same record across retries.\n *\n * **Note on `unactionableChanges`:** the differ can detect schema drift\n * the migration step can't auto-resolve — incompatible type mismatches,\n * SQLite type widening that needs table recreation, etc. Those changes\n * appear in `result.unactionableChanges` and are NOT applied (the\n * statements list is filtered to executable DDL only). When the diff\n * contains *only* unactionable changes, the function returns\n * `applied: false` with a populated `unactionableChanges` so callers can\n * surface a \"manual migration required\" signal. Without inspecting that\n * field, the result would look indistinguishable from \"already in sync.\"\n */\nexport async function migrateSmrtSchemas(\n options: MigrateSmrtSchemasOptions,\n): Promise<MigrateSmrtSchemasResult> {\n const pending = await getPendingSchemaStatements(options.db, {\n engineHint: options.engineHint,\n postgresTimestampMigration: options.postgresTimestampMigration,\n emptyTextAsNull: options.emptyTextAsNull,\n });\n if (!pending.hasChanges || pending.statements.length === 0) {\n return {\n applied: false,\n results: [],\n statements: [],\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n }\n\n const tracker = new MigrationTracker({\n db: options.db,\n engineHint: options.engineHint,\n lockTimeout: options.lockTimeout,\n statementTimeout: options.statementTimeout,\n useConcurrentIndexes: options.useConcurrentIndexes ?? true,\n });\n const migration = createMigrationDefinition(\n options.name ?? `${generateMigrationTimestamp()}_smrt_schema_sync`,\n pending.statements,\n [],\n {\n description: options.description ?? 'Synchronize SMRT object schemas',\n packageName: options.packageName,\n version: options.version ?? 'unknown',\n },\n );\n\n const results = await tracker.applyAll([migration], {\n postgresSafe: options.postgresSafe ?? true,\n reconcile: options.reconcile ?? true,\n });\n\n const failed = results.find((result) => !result.success);\n if (failed) {\n throw failed.error instanceof Error\n ? failed.error\n : new Error(String(failed.error ?? `Migration ${failed.name} failed`));\n }\n\n const applied = results.some(\n (result) => result.applied !== false && result.skipped !== true,\n );\n\n return {\n applied,\n results,\n statements: pending.statements,\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n}\n\n/** Mirror of the `getDatabaseUrl` helper in class.ts — some adapters\n * (JSON, certain in-memory wrappers) leave `db.url` undefined and expose\n * the URL on `db.config?.url` instead. Detecting the engine from a raw\n * `db.url` would throw or silently fall through to the sqlite default.\n */\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n return db.url || dbWithConfig.config?.url || '';\n}\n\n/**\n * Materialize the diff into the engine-correct SQL statements.\n *\n * For new tables we delegate to the engine's `DDLStrategy` so column types\n * (`REAL` → `DOUBLE PRECISION` on Postgres, `JSON` → `JSONB`, etc.),\n * partial-index `WHERE` clauses, and trigger syntax all match what the\n * SchemaComparer would expect on subsequent runs. Emitting the raw\n * abstract `SchemaDefinition.ddl` here would cause immediate type-drift\n * migrations on Postgres/DuckDB.\n *\n * Column/index/trigger changes for existing tables continue to come from\n * `getSQLFromDiff`, which is already engine-aware via the differ's per-\n * change `sqlStatements`.\n *\n * Exported for statement-ordering tests; it is not part of the package's\n * public entry point (`migrations/index.ts` re-exports names explicitly).\n *\n * @param diff - Schema diff to materialize.\n * @param db - Database whose URL selects the DDL strategy.\n * @param engineHint - Explicit engine when `db.url` is empty or ambiguous.\n * @returns Ordered, executable DDL statements.\n */\nexport function collectStatementsFromDiff(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n db: DatabaseInterface,\n engineHint?: DatabaseEngine,\n): string[] {\n const strategy = getDDLStrategy(\n detectEngine(resolveDatabaseUrl(db), engineHint),\n );\n const statements: string[] = [];\n const tablePlan = planForeignKeyCreation(diff.added_tables, strategy.engine);\n // #2608: the pre-R11 uuid convergence has to precede *every* foreign-key\n // statement. `planForeignKeyCreation` only defers the constraints inside a\n // mutual cycle, so an acyclic new child table keeps its FK **inline in\n // CREATE TABLE** — a fresh `uuid` child pointing at a legacy `text` parent\n // would fail with SQLSTATE 42804 before the parent could be converted.\n // Conversions only ever rewrite columns that already exist, so they are\n // safe to run ahead of every CREATE TABLE in this batch.\n statements.push(\n ...getSQLFromChanges(\n diff.changes.filter((change) => change.phase === 'pre_foreign_key'),\n ),\n );\n for (const schema of tablePlan.schemas) {\n statements.push(strategy.generateCreateTable(schema));\n statements.push(...strategy.generateIndexes(schema));\n statements.push(...strategy.generateTriggers(schema));\n }\n statements.push(...tablePlan.deferredStatements);\n statements.push(\n ...getSQLFromChanges(\n diff.changes.filter((change) => change.phase !== 'pre_foreign_key'),\n ),\n );\n // Drop empty and comment-only entries. SQLite type-widening upgrades\n // surface as `-- SQLite: Type upgrade for X requires table recreation`\n // — passing those through to the tracker records a successful migration\n // without actually fixing the column, so the same drift re-appears on\n // every subsequent run. Those changes are surfaced separately via\n // `unactionableChanges` so callers can prompt for manual remediation.\n return statements.filter((statement) => {\n const trimmed = statement.trim();\n if (trimmed.length === 0) return false;\n return !isCommentOnlySql(trimmed);\n });\n}\n\n/**\n * True if `sql` contains no executable DDL — only line comments (`--`)\n * and/or block comments (`/* … *​/`). Used to discard advisory comments\n * the differ emits when a column upgrade can't be performed in place\n * (e.g. SQLite type widening). Defensive: strips block comments first so\n * a future differ that emits `/* type_upgrade: no-op *​/` doesn't slip\n * through.\n */\nfunction isCommentOnlySql(sql: string): boolean {\n // Strip /* ... */ block comments (non-greedy, spans newlines).\n const withoutBlockComments = sql.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n const lines = withoutBlockComments\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n if (lines.length === 0) return sql.trim().length > 0;\n return lines.every((line) => line.startsWith('--'));\n}\n\n/**\n * True if `sql` is an advisory comment that says *nothing needs to happen* —\n * as opposed to one that says a human has to act.\n *\n * The differ emits both kinds. \"This SQLite column already stores JSON as\n * TEXT\" and \"#2370: this column is rebuilt together with its sibling\" are\n * no-ops; \"requires table recreation\" / \"blocked: …\" are real manual drift.\n * Treating the first kind as drift makes a fully auto-repairable migration\n * report `hasManualDrift: true`.\n *\n * Keep in sync with `classifyTypeUpgradeSql`'s `noop` arm in\n * `@happyvertical/smrt-cli` (`src/commands/db-migrate-actions.ts`), which\n * applies the same rule on the CLI side.\n */\nfunction isNoOpAdvisorySql(sql: string): boolean {\n const trimmed = sql.trim();\n return (\n /no change needed/i.test(trimmed) || /already stores .* as /i.test(trimmed)\n );\n}\n\n/**\n * Surface changes the differ produced but that the migrator cannot apply\n * automatically — `type_mismatch` entries (the differ explicitly gives up on\n * these), `type_upgrade`/`alter_column` entries whose generated SQL is\n * advisory-comment-only (a SQLite rebuild that would be unsafe, an in-place\n * constraint change SQLite cannot perform), and warning-level report-only\n * advisories (an orphan NOT NULL column that will break inserts, a required\n * column that could not be enforced, a stale unique constraint, a relaxation\n * the caller has not opted into). Info-level advisories (a harmless orphan\n * column, a stale default) and comments that mean \"already handled\" (the\n * no-op advisories a table rebuild leaves on covered siblings, #2370) are\n * deliberately excluded so `hasManualDrift` keeps meaning \"something needs\n * an operator\". Callers use this to distinguish \"schema is in sync\" from\n * \"schema is drifted but we can't fix it from here\".\n */\nfunction collectUnactionableChanges(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n): SchemaChange[] {\n const unactionable: SchemaChange[] = [];\n for (const change of diff.changes) {\n if (change.type === 'type_mismatch') {\n unactionable.push(change);\n continue;\n }\n const statements = change.sqlStatements ?? (change.sql ? [change.sql] : []);\n if (statements.length === 0) {\n if (change.advisory?.severity === 'warning') {\n unactionable.push(change);\n }\n continue;\n }\n if (\n statements.every((stmt) => isCommentOnlySql(stmt.trim())) &&\n !statements.every((stmt) => isNoOpAdvisorySql(stmt))\n ) {\n unactionable.push(change);\n }\n }\n return unactionable;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAmJA,eAAsB,2BACpB,IACA,UAII,CAAC,GACmC;CACxC,MAAM,UAAU,eAAe,2BAA2B;CAM1D,MAAM,OAAO,MAAM,mBAAmB,IAAI,SAAS;EACjD,YAAY,QAAQ;EACpB,4BAA4B,QAAQ;EACpC,iBAAiB,QAAQ;CAC3B,CAAC;CACD,MAAM,aAAa,0BAA0B,MAAM,IAAI,QAAQ,UAAU;CACzE,MAAM,sBAAsB,2BAA2B,IAAI;CAC3D,OAAO;EACL;EACA,aAAa,OAAO,KAAK,OAAO,CAAC,CAAC;EAClC;EACA,YAAY,qBAAqB,IAAI;EACrC;EACA,gBAAgB,oBAAoB,SAAS;CAC/C;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,eAAsB,mBACpB,SACmC;CACnC,MAAM,UAAU,MAAM,2BAA2B,QAAQ,IAAI;EAC3D,YAAY,QAAQ;EACpB,4BAA4B,QAAQ;EACpC,iBAAiB,QAAQ;CAC3B,CAAC;CACD,IAAI,CAAC,QAAQ,cAAc,QAAQ,WAAW,WAAW,GACvD,OAAO;EACL,SAAS;EACT,SAAS,CAAC;EACV,YAAY,CAAC;EACb,aAAa,QAAQ;EACrB,qBAAqB,QAAQ;EAC7B,gBAAgB,QAAQ;CAC1B;CAGF,MAAM,UAAU,IAAI,iBAAiB;EACnC,IAAI,QAAQ;EACZ,YAAY,QAAQ;EACpB,aAAa,QAAQ;EACrB,kBAAkB,QAAQ;EAC1B,sBAAsB,QAAQ,wBAAwB;CACxD,CAAC;CACD,MAAM,YAAY,0BAChB,QAAQ,QAAQ,GAAG,2BAA2B,EAAE,oBAChD,QAAQ,YACR,CAAC,GACD;EACE,aAAa,QAAQ,eAAe;EACpC,aAAa,QAAQ;EACrB,SAAS,QAAQ,WAAW;CAC9B,CACF;CAEA,MAAM,UAAU,MAAM,QAAQ,SAAS,CAAC,SAAS,GAAG;EAClD,cAAc,QAAQ,gBAAgB;EACtC,WAAW,QAAQ,aAAa;CAClC,CAAC;CAED,MAAM,SAAS,QAAQ,MAAM,WAAW,CAAC,OAAO,OAAO;CACvD,IAAI,QACF,MAAM,OAAO,iBAAiB,QAC1B,OAAO,QACP,IAAI,MAAM,OAAO,OAAO,SAAS,aAAa,OAAO,KAAK,QAAQ,CAAC;CAOzE,OAAO;EACL,SALc,QAAQ,MACrB,WAAW,OAAO,YAAY,SAAS,OAAO,YAAY,IAI3D;EACA;EACA,YAAY,QAAQ;EACpB,aAAa,QAAQ;EACrB,qBAAqB,QAAQ;EAC7B,gBAAgB,QAAQ;CAC1B;AACF;;;;;;AAOA,SAAS,mBAAmB,IAA+B;CACzD,MAAM,eAAe;CAGrB,OAAO,GAAG,OAAO,aAAa,QAAQ,OAAO;AAC/C;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,0BACd,MACA,IACA,YACU;CACV,MAAM,WAAW,eACf,aAAa,mBAAmB,EAAE,GAAG,UAAU,CACjD;CACA,MAAM,aAAuB,CAAC;CAC9B,MAAM,YAAY,uBAAuB,KAAK,cAAc,SAAS,MAAM;CAQ3E,WAAW,KACT,GAAG,kBACD,KAAK,QAAQ,QAAQ,WAAW,OAAO,UAAU,iBAAiB,CACpE,CACF;CACA,KAAK,MAAM,UAAU,UAAU,SAAS;EACtC,WAAW,KAAK,SAAS,oBAAoB,MAAM,CAAC;EACpD,WAAW,KAAK,GAAG,SAAS,gBAAgB,MAAM,CAAC;EACnD,WAAW,KAAK,GAAG,SAAS,iBAAiB,MAAM,CAAC;CACtD;CACA,WAAW,KAAK,GAAG,UAAU,kBAAkB;CAC/C,WAAW,KACT,GAAG,kBACD,KAAK,QAAQ,QAAQ,WAAW,OAAO,UAAU,iBAAiB,CACpE,CACF;CAOA,OAAO,WAAW,QAAQ,cAAc;EACtC,MAAM,UAAU,UAAU,KAAK;EAC/B,IAAI,QAAQ,WAAW,GAAG,OAAO;EACjC,OAAO,CAAC,iBAAiB,OAAO;CAClC,CAAC;AACH;;;;;;;;;AAUA,SAAS,iBAAiB,KAAsB;CAG9C,MAAM,QADuB,IAAI,QAAQ,qBAAqB,EAChD,CAAA,CACX,MAAM,IAAI,CAAC,CACX,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,CAC1B,QAAQ,SAAS,KAAK,SAAS,CAAC;CACnC,IAAI,MAAM,WAAW,GAAG,OAAO,IAAI,KAAK,CAAC,CAAC,SAAS;CACnD,OAAO,MAAM,OAAO,SAAS,KAAK,WAAW,IAAI,CAAC;AACpD;;;;;;;;;;;;;;;AAgBA,SAAS,kBAAkB,KAAsB;CAC/C,MAAM,UAAU,IAAI,KAAK;CACzB,OACE,oBAAoB,KAAK,OAAO,KAAK,yBAAyB,KAAK,OAAO;AAE9E;;;;;;;;;;;;;;;;AAiBA,SAAS,2BACP,MACgB;CAChB,MAAM,eAA+B,CAAC;CACtC,KAAK,MAAM,UAAU,KAAK,SAAS;EACjC,IAAI,OAAO,SAAS,iBAAiB;GACnC,aAAa,KAAK,MAAM;GACxB;EACF;EACA,MAAM,aAAa,OAAO,kBAAkB,OAAO,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;EACzE,IAAI,WAAW,WAAW,GAAG;GAC3B,IAAI,OAAO,UAAU,aAAa,WAChC,aAAa,KAAK,MAAM;GAE1B;EACF;EACA,IACE,WAAW,OAAO,SAAS,iBAAiB,KAAK,KAAK,CAAC,CAAC,KACxD,CAAC,WAAW,OAAO,SAAS,kBAAkB,IAAI,CAAC,GAEnD,aAAa,KAAK,MAAM;CAE5B;CACA,OAAO;AACT"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Stored references to deprecated qualified names (#3338).
3
+ *
4
+ * `@smrt({ previousQualifiedNames })` keeps an old qualified name resolving
5
+ * after a class moves package; it never rewrites stored data. This module is
6
+ * the operator side of that bridge:
7
+ *
8
+ * - {@link countLegacyQualifiedNameReferences} — read-only counts of rows that
9
+ * still store an old name, which `smrt doctor --db` reports so a consumer
10
+ * can see when an alias is safe to remove;
11
+ * - {@link backfillLegacyQualifiedNames} — an opt-in, idempotent rewrite of
12
+ * those rows to the current name. It never runs automatically.
13
+ *
14
+ * Stored references covered:
15
+ *
16
+ * - `meta_type` of every registered `SmrtPolymorphicAssociation` subclass
17
+ * table (any class can be a polymorphic target, so every alias is counted);
18
+ * - `_meta_type` of an STI table whose family includes an aliased class.
19
+ *
20
+ * Removing an alias is a breaking change for any row this still counts.
21
+ */
22
+ import type { DatabaseInterface } from '@happyvertical/sql';
23
+ /** Prefix of the `_smrt_backfills` marker {@link backfillLegacyQualifiedNames} records. */
24
+ export declare const LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX = "@happyvertical/smrt-core:qualified-name-aliases:v1";
25
+ /** One table column that can store a class's qualified name. */
26
+ export interface LegacyQualifiedNameTarget {
27
+ /** Registered class that owns the table (qualified when available). */
28
+ className: string;
29
+ table: string;
30
+ /** `meta_type` (polymorphic association) or `_meta_type` (STI discriminator). */
31
+ column: string;
32
+ kind: 'polymorphic' | 'sti';
33
+ /** Deprecated names this column may still hold. */
34
+ aliases: string[];
35
+ /** Tenant column of the table, when tenant-owned. */
36
+ tenantColumn?: string;
37
+ /**
38
+ * Upsert identity columns that include `column`. A rewrite that would
39
+ * duplicate a current-name row on these columns is skipped, never forced.
40
+ */
41
+ conflictColumns?: string[];
42
+ }
43
+ /** Rows of one target that still store one deprecated name. */
44
+ export interface LegacyQualifiedNameReference {
45
+ table: string;
46
+ column: string;
47
+ kind: 'polymorphic' | 'sti';
48
+ alias: string;
49
+ current: string;
50
+ count: number;
51
+ }
52
+ export interface LegacyQualifiedNameReport {
53
+ /** Declared alias → current qualified name, sorted by alias. */
54
+ aliases: Array<{
55
+ alias: string;
56
+ current: string;
57
+ }>;
58
+ /** Non-zero counts only, sorted by table, column, alias. */
59
+ references: LegacyQualifiedNameReference[];
60
+ /** Sum of `references[].count`. */
61
+ total: number;
62
+ /** Target tables absent from this database (nothing to count). */
63
+ missingTables: string[];
64
+ /** Target tables excluded from a tenant-scoped run (no tenant column). */
65
+ untenantedTables: string[];
66
+ }
67
+ export interface LegacyQualifiedNameOptions {
68
+ /**
69
+ * Restrict counting/rewriting to one tenant's rows. Tables without a tenant
70
+ * column are then skipped (reported in `untenantedTables`), never touched.
71
+ */
72
+ tenantId?: string;
73
+ /** Engine hint when the adapter URL does not identify it. */
74
+ engineHint?: string;
75
+ }
76
+ export interface LegacyQualifiedNameBackfillOptions extends LegacyQualifiedNameOptions {
77
+ /** Report what would change without writing (no marker is recorded). */
78
+ dryRun?: boolean;
79
+ /** Re-run even when this alias set's marker is already recorded. */
80
+ force?: boolean;
81
+ }
82
+ export interface LegacyQualifiedNameBackfillResult {
83
+ /** `false` when the marker was already recorded (and `force` was unset) or on a dry run. */
84
+ ran: boolean;
85
+ dryRun: boolean;
86
+ /** The `_smrt_backfills` marker for this alias set and scope. */
87
+ backfillName: string;
88
+ /** Counts before the rewrite. */
89
+ before: LegacyQualifiedNameReport;
90
+ /** Rows rewritten per table/column/alias. */
91
+ rewritten: LegacyQualifiedNameReference[];
92
+ /**
93
+ * Rows left under their old name because the rewrite would duplicate a row
94
+ * already stored under the current name (resolve those by hand, then re-run).
95
+ */
96
+ skippedDuplicates: LegacyQualifiedNameReference[];
97
+ /** Whether the marker was recorded (only when nothing remains in scope). */
98
+ recorded: boolean;
99
+ }
100
+ /**
101
+ * Every table column that can store a deprecated qualified name, derived from
102
+ * the booted registry. Empty when no class declares `previousQualifiedNames`.
103
+ */
104
+ export declare function collectLegacyQualifiedNameTargets(): LegacyQualifiedNameTarget[];
105
+ /**
106
+ * Count rows that still store a deprecated qualified name. Read-only.
107
+ *
108
+ * Absent tables are reported in `missingTables`, not as errors: the registry
109
+ * can carry classes this database never migrated.
110
+ */
111
+ export declare function countLegacyQualifiedNameReferences(db: DatabaseInterface, options?: LegacyQualifiedNameOptions): Promise<LegacyQualifiedNameReport>;
112
+ /**
113
+ * Stable `_smrt_backfills` marker for the declared alias set and scope. A new
114
+ * alias (another model move) produces a new marker, so the backfill runs
115
+ * again for it.
116
+ */
117
+ export declare function legacyQualifiedNamesBackfillName(pairs?: Array<{
118
+ alias: string;
119
+ current: string;
120
+ }>, tenantId?: string): string;
121
+ /**
122
+ * Rewrite stored deprecated qualified names to their current names.
123
+ *
124
+ * Opt-in and idempotent: it never runs on its own, rewrites only rows whose
125
+ * value is exactly a declared old name, and records a `_smrt_backfills`
126
+ * marker (per alias set and tenant scope) once nothing in scope remains, so a
127
+ * second call is a no-op unless `force` is set. Rows are rewritten inside one
128
+ * transaction where the adapter provides one; the tenant column, ids and all
129
+ * other values are never touched, and a tenant-scoped run only touches that
130
+ * tenant's rows in tenant-owned tables. A row whose rewrite would duplicate a
131
+ * row already stored under the current name is skipped and reported.
132
+ */
133
+ export declare function backfillLegacyQualifiedNames(db: DatabaseInterface, options?: LegacyQualifiedNameBackfillOptions): Promise<LegacyQualifiedNameBackfillResult>;
134
+ //# sourceMappingURL=qualified-name-aliases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qualified-name-aliases.d.ts","sourceRoot":"","sources":["../../src/migrations/qualified-name-aliases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAa5D,2FAA2F;AAC3F,eAAO,MAAM,sCAAsC,uDACG,CAAC;AAEvD,gEAAgE;AAChE,MAAM,WAAW,yBAAyB;IACxC,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,aAAa,GAAG,KAAK,CAAC;IAC5B,mDAAmD;IACnD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,+DAA+D;AAC/D,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,aAAa,GAAG,KAAK,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,yBAAyB;IACxC,gEAAgE;IAChE,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,4DAA4D;IAC5D,UAAU,EAAE,4BAA4B,EAAE,CAAC;IAC3C,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,0EAA0E;IAC1E,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kCACf,SAAQ,0BAA0B;IAClC,wEAAwE;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,iCAAiC;IAChD,4FAA4F;IAC5F,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,MAAM,EAAE,yBAAyB,CAAC;IAClC,6CAA6C;IAC7C,SAAS,EAAE,4BAA4B,EAAE,CAAC;IAC1C;;;OAGG;IACH,iBAAiB,EAAE,4BAA4B,EAAE,CAAC;IAClD,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAmDD;;;GAGG;AACH,wBAAgB,iCAAiC,IAAI,yBAAyB,EAAE,CA6D/E;AA8CD;;;;;GAKG;AACH,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CA4CpC;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,KAAK,GAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAgB,EAC/D,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,CAaR;AA+CD;;;;;;;;;;;GAWG;AACH,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,kCAAuC,GAC/C,OAAO,CAAC,iCAAiC,CAAC,CAiF5C"}
@@ -0,0 +1,294 @@
1
+ import { IMPLICIT_TENANT_COLUMN } from "../schema/conflict-target.js";
2
+ import { toSnakeCase } from "../utils/naming.js";
3
+ import { detectEngine } from "../schema/ddl/index.js";
4
+ import { tableExists } from "../system/compatibility.js";
5
+ import { isFrameworkBaseClass } from "../registry/framework-base-classes.js";
6
+ import { isSmrtCollectionExtendsName } from "../registry/collection-resolution.js";
7
+ import { ObjectRegistry } from "../registry.js";
8
+ import { BackfillTracker } from "./backfill-tracker.js";
9
+ import { createHash } from "node:crypto";
10
+ //#region src/migrations/qualified-name-aliases.ts
11
+ /**
12
+ * Stored references to deprecated qualified names (#3338).
13
+ *
14
+ * `@smrt({ previousQualifiedNames })` keeps an old qualified name resolving
15
+ * after a class moves package; it never rewrites stored data. This module is
16
+ * the operator side of that bridge:
17
+ *
18
+ * - {@link countLegacyQualifiedNameReferences} — read-only counts of rows that
19
+ * still store an old name, which `smrt doctor --db` reports so a consumer
20
+ * can see when an alias is safe to remove;
21
+ * - {@link backfillLegacyQualifiedNames} — an opt-in, idempotent rewrite of
22
+ * those rows to the current name. It never runs automatically.
23
+ *
24
+ * Stored references covered:
25
+ *
26
+ * - `meta_type` of every registered `SmrtPolymorphicAssociation` subclass
27
+ * table (any class can be a polymorphic target, so every alias is counted);
28
+ * - `_meta_type` of an STI table whose family includes an aliased class.
29
+ *
30
+ * Removing an alias is a breaking change for any row this still counts.
31
+ */
32
+ /** Prefix of the `_smrt_backfills` marker {@link backfillLegacyQualifiedNames} records. */
33
+ var LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX = "@happyvertical/smrt-core:qualified-name-aliases:v1";
34
+ function compareText(left, right) {
35
+ return left < right ? -1 : left > right ? 1 : 0;
36
+ }
37
+ function aliasPairs() {
38
+ return [...ObjectRegistry.getQualifiedNameAliases()].map(([alias, current]) => ({
39
+ alias,
40
+ current
41
+ }));
42
+ }
43
+ function quoteIdentifier(name) {
44
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) throw new Error(`Refusing to quote unexpected identifier "${name}"`);
45
+ return `"${name}"`;
46
+ }
47
+ function isPolymorphicAssociationFields(fields) {
48
+ return fields.has("metaType") && fields.has("metaId") && fields.has("role");
49
+ }
50
+ function safeConflictColumns(className) {
51
+ try {
52
+ return ObjectRegistry.getConflictColumns(className).map((column) => toSnakeCase(column));
53
+ } catch {
54
+ return [];
55
+ }
56
+ }
57
+ function safeTenantColumn(className) {
58
+ let declared;
59
+ try {
60
+ declared = ObjectRegistry.getTenantColumn(className);
61
+ } catch {
62
+ declared = void 0;
63
+ }
64
+ if (declared) return declared;
65
+ return ObjectRegistry.getFields(className).has("tenantId") ? IMPLICIT_TENANT_COLUMN : void 0;
66
+ }
67
+ /**
68
+ * Every table column that can store a deprecated qualified name, derived from
69
+ * the booted registry. Empty when no class declares `previousQualifiedNames`.
70
+ */
71
+ function collectLegacyQualifiedNameTargets() {
72
+ const pairs = aliasPairs();
73
+ if (pairs.length === 0) return [];
74
+ const allAliases = pairs.map((pair) => pair.alias);
75
+ const byKey = /* @__PURE__ */ new Map();
76
+ const add = (target) => {
77
+ const key = `${target.table}\u0000${target.column}`;
78
+ const existing = byKey.get(key);
79
+ if (!existing) {
80
+ byKey.set(key, target);
81
+ return;
82
+ }
83
+ existing.aliases = [.../* @__PURE__ */ new Set([...existing.aliases, ...target.aliases])].sort(compareText);
84
+ };
85
+ for (const className of ObjectRegistry.getQualifiedClassNames()) {
86
+ const registered = ObjectRegistry.getClass(className);
87
+ if (!registered) continue;
88
+ if (isSmrtCollectionExtendsName(registered.extends)) continue;
89
+ if (isFrameworkBaseClass(registered.name, registered.packageName)) continue;
90
+ if (!isPolymorphicAssociationFields(registered.fields)) continue;
91
+ const table = ObjectRegistry.getTableName(className);
92
+ if (!table) continue;
93
+ const conflictColumns = safeConflictColumns(className);
94
+ const tenantColumn = safeTenantColumn(className);
95
+ add({
96
+ className,
97
+ table,
98
+ column: "meta_type",
99
+ kind: "polymorphic",
100
+ aliases: [...allAliases].sort(compareText),
101
+ ...tenantColumn ? { tenantColumn } : {},
102
+ ...conflictColumns.includes("meta_type") ? { conflictColumns } : {}
103
+ });
104
+ }
105
+ for (const { alias, current } of pairs) {
106
+ if (ObjectRegistry.getTableStrategy(current) !== "sti") continue;
107
+ const table = ObjectRegistry.getTableName(current);
108
+ if (!table) continue;
109
+ const conflictColumns = safeConflictColumns(current);
110
+ const tenantColumn = safeTenantColumn(current);
111
+ add({
112
+ className: current,
113
+ table,
114
+ column: "_meta_type",
115
+ kind: "sti",
116
+ aliases: [alias],
117
+ ...tenantColumn ? { tenantColumn } : {},
118
+ ...conflictColumns.includes("_meta_type") ? { conflictColumns } : {}
119
+ });
120
+ }
121
+ return [...byKey.values()].sort((left, right) => compareText(left.table, right.table) || compareText(left.column, right.column));
122
+ }
123
+ function rowsOf(result) {
124
+ if (Array.isArray(result)) return result;
125
+ if (result && typeof result === "object" && "rows" in result) {
126
+ const rows = result.rows;
127
+ if (Array.isArray(rows)) return rows;
128
+ }
129
+ return [];
130
+ }
131
+ function resolveEngine(db, engineHint) {
132
+ const withConfig = db;
133
+ return detectEngine(db.url || withConfig.config?.url || "", engineHint || withConfig.type || withConfig.config?.type);
134
+ }
135
+ async function countTarget(db, target, tenantId) {
136
+ const counts = /* @__PURE__ */ new Map();
137
+ if (target.aliases.length === 0) return counts;
138
+ const column = quoteIdentifier(target.column);
139
+ const params = [...target.aliases];
140
+ let sql = `SELECT ${column} AS name, COUNT(*) AS n FROM ${quoteIdentifier(target.table)} WHERE ${column} IN (${target.aliases.map(() => "?").join(", ")})`;
141
+ if (tenantId !== void 0 && target.tenantColumn) {
142
+ sql += ` AND ${quoteIdentifier(target.tenantColumn)} = ?`;
143
+ params.push(tenantId);
144
+ }
145
+ sql += ` GROUP BY ${column}`;
146
+ for (const row of rowsOf(await db.query(sql, ...params))) {
147
+ const count = Number(row.n ?? 0);
148
+ if (count > 0) counts.set(String(row.name), count);
149
+ }
150
+ return counts;
151
+ }
152
+ /**
153
+ * Count rows that still store a deprecated qualified name. Read-only.
154
+ *
155
+ * Absent tables are reported in `missingTables`, not as errors: the registry
156
+ * can carry classes this database never migrated.
157
+ */
158
+ async function countLegacyQualifiedNameReferences(db, options = {}) {
159
+ const pairs = aliasPairs();
160
+ const currentByAlias = new Map(pairs.map((pair) => [pair.alias, pair]));
161
+ const report = {
162
+ aliases: pairs,
163
+ references: [],
164
+ total: 0,
165
+ missingTables: [],
166
+ untenantedTables: []
167
+ };
168
+ const engine = resolveEngine(db, options.engineHint);
169
+ for (const target of collectLegacyQualifiedNameTargets()) {
170
+ if (options.tenantId !== void 0 && !target.tenantColumn) {
171
+ if (!report.untenantedTables.includes(target.table)) report.untenantedTables.push(target.table);
172
+ continue;
173
+ }
174
+ if (!await tableExists(db, target.table, engine)) {
175
+ if (!report.missingTables.includes(target.table)) report.missingTables.push(target.table);
176
+ continue;
177
+ }
178
+ const counts = await countTarget(db, target, options.tenantId);
179
+ for (const alias of target.aliases) {
180
+ const count = counts.get(alias) ?? 0;
181
+ if (count === 0) continue;
182
+ report.references.push({
183
+ table: target.table,
184
+ column: target.column,
185
+ kind: target.kind,
186
+ alias,
187
+ current: currentByAlias.get(alias)?.current ?? alias,
188
+ count
189
+ });
190
+ report.total += count;
191
+ }
192
+ }
193
+ report.missingTables.sort(compareText);
194
+ report.untenantedTables.sort(compareText);
195
+ return report;
196
+ }
197
+ /**
198
+ * Stable `_smrt_backfills` marker for the declared alias set and scope. A new
199
+ * alias (another model move) produces a new marker, so the backfill runs
200
+ * again for it.
201
+ */
202
+ function legacyQualifiedNamesBackfillName(pairs = aliasPairs(), tenantId) {
203
+ const digest = createHash("sha256").update([...pairs].sort((left, right) => compareText(left.alias, right.alias)).map(({ alias, current }) => `${alias}=>${current}`).join("\n")).digest("hex").slice(0, 16);
204
+ return tenantId === void 0 ? `${LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX}:${digest}` : `${LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX}:${digest}:tenant:${tenantId}`;
205
+ }
206
+ function nullSafeEquals(engine, left, right) {
207
+ return engine === "sqlite" ? `${left} IS ${right}` : `${left} IS NOT DISTINCT FROM ${right}`;
208
+ }
209
+ function buildRewriteStatement(engine, target, tenantId, alias, current) {
210
+ const table = quoteIdentifier(target.table);
211
+ const column = quoteIdentifier(target.column);
212
+ const params = [current, alias];
213
+ let sql = `UPDATE ${table} SET ${column} = ? WHERE ${column} = ?`;
214
+ if (tenantId !== void 0 && target.tenantColumn) {
215
+ sql += ` AND ${quoteIdentifier(target.tenantColumn)} = ?`;
216
+ params.push(tenantId);
217
+ }
218
+ const identity = (target.conflictColumns ?? []).filter((name) => name !== target.column);
219
+ if (target.conflictColumns) {
220
+ const correlated = identity.map((name) => nullSafeEquals(engine, `__smrt_current.${quoteIdentifier(name)}`, `${table}.${quoteIdentifier(name)}`)).join(" AND ");
221
+ sql += ` AND NOT EXISTS (SELECT 1 FROM ${table} AS __smrt_current WHERE __smrt_current.${column} = ?${correlated ? ` AND ${correlated}` : ""})`;
222
+ params.push(current);
223
+ }
224
+ return {
225
+ sql,
226
+ params
227
+ };
228
+ }
229
+ /**
230
+ * Rewrite stored deprecated qualified names to their current names.
231
+ *
232
+ * Opt-in and idempotent: it never runs on its own, rewrites only rows whose
233
+ * value is exactly a declared old name, and records a `_smrt_backfills`
234
+ * marker (per alias set and tenant scope) once nothing in scope remains, so a
235
+ * second call is a no-op unless `force` is set. Rows are rewritten inside one
236
+ * transaction where the adapter provides one; the tenant column, ids and all
237
+ * other values are never touched, and a tenant-scoped run only touches that
238
+ * tenant's rows in tenant-owned tables. A row whose rewrite would duplicate a
239
+ * row already stored under the current name is skipped and reported.
240
+ */
241
+ async function backfillLegacyQualifiedNames(db, options = {}) {
242
+ const pairs = aliasPairs();
243
+ const backfillName = legacyQualifiedNamesBackfillName(pairs, options.tenantId);
244
+ const dryRun = options.dryRun === true;
245
+ const before = await countLegacyQualifiedNameReferences(db, options);
246
+ const result = {
247
+ ran: false,
248
+ dryRun,
249
+ backfillName,
250
+ before,
251
+ rewritten: [],
252
+ skippedDuplicates: [],
253
+ recorded: false
254
+ };
255
+ if (pairs.length === 0 || dryRun) return result;
256
+ const tracker = new BackfillTracker({ db });
257
+ if (!options.force && await tracker.isApplied(backfillName)) return result;
258
+ const engine = resolveEngine(db, options.engineHint);
259
+ const targets = new Map(collectLegacyQualifiedNameTargets().map((target) => [`${target.table}\u0000${target.column}`, target]));
260
+ const rewrite = async (handle) => {
261
+ for (const reference of before.references) {
262
+ const target = targets.get(`${reference.table}\u0000${reference.column}`);
263
+ if (!target) continue;
264
+ const { sql, params } = buildRewriteStatement(engine, target, options.tenantId, reference.alias, reference.current);
265
+ await handle.query(sql, ...params);
266
+ }
267
+ };
268
+ if (typeof db.transaction === "function") await db.transaction(async (tx) => rewrite(tx));
269
+ else await rewrite(db);
270
+ const after = await countLegacyQualifiedNameReferences(db, options);
271
+ const remaining = new Map(after.references.map((reference) => [`${reference.table}\u0000${reference.column}\u0000${reference.alias}`, reference]));
272
+ for (const reference of before.references) {
273
+ const left = remaining.get(`${reference.table}\u0000${reference.column}\u0000${reference.alias}`);
274
+ const rewrittenCount = reference.count - (left?.count ?? 0);
275
+ if (rewrittenCount > 0) result.rewritten.push({
276
+ ...reference,
277
+ count: rewrittenCount
278
+ });
279
+ }
280
+ result.skippedDuplicates = after.references;
281
+ result.ran = true;
282
+ if (after.total === 0) {
283
+ await tracker.recordApplied(backfillName, {
284
+ description: "Stored deprecated qualified names rewritten to current names: " + pairs.map(({ alias, current }) => `${alias} -> ${current}`).join(", "),
285
+ packageName: "@happyvertical/smrt-core"
286
+ });
287
+ result.recorded = true;
288
+ }
289
+ return result;
290
+ }
291
+ //#endregion
292
+ export { LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, backfillLegacyQualifiedNames, collectLegacyQualifiedNameTargets, countLegacyQualifiedNameReferences, legacyQualifiedNamesBackfillName };
293
+
294
+ //# sourceMappingURL=qualified-name-aliases.js.map