@happyvertical/smrt-core 0.47.1 → 0.48.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 (87) hide show
  1. package/AGENTS.md +1 -0
  2. package/agents/generators.md +12 -0
  3. package/agents/registry-snapshot.md +28 -0
  4. package/agents/schema-paths.md +57 -0
  5. package/agents/system-diagnostics.md +6 -1
  6. package/dist/browser.js +3 -2
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +3 -1
  9. package/dist/collection.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -2
  13. package/dist/manifest/static-manifest.js +1 -1
  14. package/dist/manifest/static-manifest.js.map +1 -1
  15. package/dist/manifest/store.js +1 -1
  16. package/dist/manifest.json +1 -1
  17. package/dist/migrations/differ.d.ts +88 -4
  18. package/dist/migrations/differ.d.ts.map +1 -1
  19. package/dist/migrations/differ.js +363 -5
  20. package/dist/migrations/differ.js.map +1 -1
  21. package/dist/migrations/generator.d.ts.map +1 -1
  22. package/dist/migrations/generator.js +2 -1
  23. package/dist/migrations/generator.js.map +1 -1
  24. package/dist/prebuild/index.d.ts.map +1 -1
  25. package/dist/prebuild/index.js +4 -3
  26. package/dist/prebuild/index.js.map +1 -1
  27. package/dist/registry/class-registration.d.ts.map +1 -1
  28. package/dist/registry/class-registration.js +3 -1
  29. package/dist/registry/class-registration.js.map +1 -1
  30. package/dist/registry/schema-builder.js +1 -1
  31. package/dist/registry.js +1 -1
  32. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  33. package/dist/scanner/manifest-generator.js +22 -1
  34. package/dist/scanner/manifest-generator.js.map +1 -1
  35. package/dist/schema/foreign-key-ddl.d.ts +26 -0
  36. package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
  37. package/dist/schema/foreign-key-ddl.js +26 -2
  38. package/dist/schema/foreign-key-ddl.js.map +1 -1
  39. package/dist/schema/foreign-key-orphan-report.d.ts +57 -0
  40. package/dist/schema/foreign-key-orphan-report.d.ts.map +1 -0
  41. package/dist/schema/foreign-key-orphan-report.js +159 -0
  42. package/dist/schema/foreign-key-orphan-report.js.map +1 -0
  43. package/dist/schema/index.d.ts +2 -1
  44. package/dist/schema/index.d.ts.map +1 -1
  45. package/dist/schema/index.js +3 -2
  46. package/dist/schema/live-parity.d.ts +8 -1
  47. package/dist/schema/live-parity.d.ts.map +1 -1
  48. package/dist/schema/live-parity.js +172 -2
  49. package/dist/schema/live-parity.js.map +1 -1
  50. package/dist/schema/text-cast-probe.d.ts +66 -0
  51. package/dist/schema/text-cast-probe.d.ts.map +1 -0
  52. package/dist/schema/text-cast-probe.js +142 -0
  53. package/dist/schema/text-cast-probe.js.map +1 -0
  54. package/dist/schema/types.d.ts +39 -1
  55. package/dist/schema/types.d.ts.map +1 -1
  56. package/dist/schema.js +3 -2
  57. package/dist/smrt-knowledge.json +17 -11
  58. package/dist/system/diagnostics.d.ts +22 -0
  59. package/dist/system/diagnostics.d.ts.map +1 -1
  60. package/dist/system/diagnostics.js +88 -31
  61. package/dist/system/diagnostics.js.map +1 -1
  62. package/dist/system/index.d.ts +1 -0
  63. package/dist/system/index.d.ts.map +1 -1
  64. package/dist/system/index.js +2 -1
  65. package/dist/system/registry-snapshot.d.ts +112 -0
  66. package/dist/system/registry-snapshot.d.ts.map +1 -0
  67. package/dist/system/registry-snapshot.js +171 -0
  68. package/dist/system/registry-snapshot.js.map +1 -0
  69. package/dist/testing/database.d.ts.map +1 -1
  70. package/dist/testing/database.js +10 -2
  71. package/dist/testing/database.js.map +1 -1
  72. package/dist/vite-plugin/dev-plane-route.d.ts +5 -0
  73. package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -0
  74. package/dist/vite-plugin/dev-plane-route.js +91 -0
  75. package/dist/vite-plugin/dev-plane-route.js.map +1 -0
  76. package/dist/vite-plugin/index.d.ts.map +1 -1
  77. package/dist/vite-plugin/index.js +4 -3
  78. package/dist/vite-plugin/index.js.map +1 -1
  79. package/dist/vite-plugin/sveltekit-generator.d.ts +8 -0
  80. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  81. package/dist/vite-plugin/sveltekit-generator.js +2 -0
  82. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  83. package/dist/vite-plugin/web-collections.d.ts +6 -0
  84. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  85. package/dist/vite-plugin/web-collections.js +9 -4
  86. package/dist/vite-plugin/web-collections.js.map +1 -1
  87. package/package.json +4 -4
@@ -70,10 +70,6 @@ export interface DiffOptions {
70
70
  * constraint-owned.
71
71
  */
72
72
  export declare function uniqueColumnIndexName(tableName: string, columnName: string): string;
73
- /**
74
- * True when a change is report-only: it carries an advisory and no
75
- * executable statement. Such changes never reach the migration stream.
76
- */
77
73
  export declare function isAdvisoryOnlyChange(change: SchemaChange): boolean;
78
74
  /**
79
75
  * True for an info-level report-only change: listed in `SchemaDiff.changes`
@@ -189,6 +185,16 @@ export declare class SchemaComparer {
189
185
  * planner treats as a block — SMRT never coerces identifier values.
190
186
  */
191
187
  private probeUuidShape;
188
+ /**
189
+ * Probe a candidate `text` -> `timestamptz`/`jsonb` column (#2771, #2772)
190
+ * with a real, exception-safe cast attempt (see `text-cast-probe.ts`).
191
+ * Unlike {@link probeUuidShape}, a failed/unrealistic probe
192
+ * (`unavailable`) is the caller's cue to fall back to the pre-existing
193
+ * behavior for that pair rather than surface a new finding — this keeps
194
+ * the new convergence strictly additive for every case that isn't
195
+ * affirmatively confirmed safe or affirmatively confirmed unsafe.
196
+ */
197
+ private probeTextCastShape;
192
198
  /**
193
199
  * Turn the convergence plan into diff entries: executable `type_upgrade`
194
200
  * conversions marked `phase: 'pre_foreign_key'`, plus report-only warnings
@@ -220,6 +226,84 @@ export declare class SchemaComparer {
220
226
  * that orphan gets an executable `DROP NOT NULL` where the engine allows.
221
227
  */
222
228
  private compareColumns;
229
+ /**
230
+ * Detect a pending rename backfill (#2752): a manifest-declared column
231
+ * that exists live but holds no data, paired with an orphan (undeclared)
232
+ * live column of a compatible type that does hold data — the shape a
233
+ * framework field rename leaves behind when `db:migrate` adds the new
234
+ * column additively and never moves data into it. For each such pair,
235
+ * emit the copy-then-drop repair as an advisory: never executed,
236
+ * PostgreSQL and SQLite only (DuckDB/JSON are out of scope). The repair
237
+ * is genuinely idempotent SQL on PostgreSQL (a self-guarding
238
+ * `DO $$ ... $$` block); SQLite has no conditional-DDL construct, so its
239
+ * repair is operator-mediated instead — a guard query plus instructions,
240
+ * not a blind-rerun-safe statement. See {@link describeRenameDataPending}.
241
+ */
242
+ private detectRenameDataPending;
243
+ /**
244
+ * Live-data probe: does the column hold any non-null, non-empty value?
245
+ * Used by {@link detectRenameDataPending}. Errors propagate to the
246
+ * caller, which skips the pair rather than guessing (#2752).
247
+ */
248
+ private columnHasNonEmptyValue;
249
+ /**
250
+ * Live-data probe: are every one of a column's non-empty values
251
+ * UUID-shaped ({@link CANONICAL_UUID_PATTERN})? PostgreSQL pushes the
252
+ * check into the query with its regex operator; SQLite has no regex
253
+ * operator, but its case-sensitive `GLOB` can still express the fixed
254
+ * 36-character canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN}
255
+ * against `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check), so
256
+ * both engines run one server-side aggregate `count(*)` rather than
257
+ * fetching every non-empty value into JS to test in a loop — important on
258
+ * a production table with many rows (#2767 review).
259
+ */
260
+ private allNonEmptyValuesUuidShaped;
261
+ /**
262
+ * Render the repair for one rename-data-pending pair (#2752): copy
263
+ * non-empty `oldColumn` into `newColumn` only where `newColumn` is still
264
+ * empty (casting to `uuid` when the new column is native uuid, otherwise a
265
+ * plain copy), then drop `oldColumn`.
266
+ *
267
+ * Idempotency review findings and the fix:
268
+ *
269
+ * - (P1) The `newColumn`-is-empty predicate previously compared any
270
+ * non-uuid destination to `''` (`col IS NULL OR col = ''`), which is
271
+ * only valid for text-affinity types — PostgreSQL rejects `col = ''`
272
+ * for `integer`/`boolean`/`timestamp`/etc. before a same-type rename
273
+ * pair of one of those types ever copies anything. Fixed by comparing
274
+ * `CAST(col AS TEXT) = ''` instead, mirroring the same portable
275
+ * emptiness check {@link columnHasNonEmptyValue} already uses for
276
+ * detection, so the predicate is valid for every column type.
277
+ * - (P2) An `UPDATE` unconditionally naming `oldColumn` fails once that
278
+ * column is gone, on either engine, regardless of the DROP's own
279
+ * idempotency — a guard query placed before the DROP (or even before
280
+ * the UPDATE) still leaves a human decision between "run" and "don't",
281
+ * which is not genuine SQL-level idempotency. PostgreSQL CAN express a
282
+ * truly self-contained, unconditionally-safe-to-rerun statement here:
283
+ * an anonymous `DO $$ ... $$` block (the same idiom this file already
284
+ * uses for `generatePostgresIntegerPreflightSQL`) that checks
285
+ * `information_schema.columns` and only runs the UPDATE + DROP when the
286
+ * old column still exists — one executable statement, no operator
287
+ * judgment required, safe to rerun any number of times. SQLite has no
288
+ * procedural block or conditional-DDL construct at all (confirmed
289
+ * against a real connection), so there the contract is explicitly
290
+ * downgraded from "idempotent SQL" to "operator-mediated": a guard
291
+ * query plus instructions, not a single statement that is itself safe
292
+ * to blindly rerun.
293
+ *
294
+ * This is report-only either way: `db:migrate` never executes it.
295
+ */
296
+ private describeRenameDataPending;
297
+ /**
298
+ * Render the advisory for an ambiguous rename-data-pending match (#2767
299
+ * review): more than one orphan column is a populated, type-compatible
300
+ * candidate for the same empty declared column, so the real rename source
301
+ * cannot be inferred. Unlike {@link describeRenameDataPending}, this
302
+ * carries no `suggestedSql` — running a per-candidate copy-and-drop would
303
+ * let output order decide which candidate's data wins and then drop every
304
+ * one of them, which is not a repair an advisory should suggest blindly.
305
+ */
306
+ private describeRenameDataPendingAmbiguous;
223
307
  /**
224
308
  * Compare nullability and default between a manifest column and the live
225
309
  * column, emitting `alter_column` changes (#2369).
@@ -1 +1 @@
1
- {"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/migrations/differ.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/migrations/differ.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import { normalizeForeignKeyAction, requireForeignKeyAction } from "../schema/foreign-key-policy.js";
2
2
  import { isJsonPathIndex, isStiSubtypeUniqueIndex, renderIndexTarget } from "../schema/index-utils.js";
3
- import { foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyConstraint, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaForeignKeys, schemaForeignKeysForEngine } from "../schema/foreign-key-ddl.js";
3
+ import { CANONICAL_UUID_PATTERN, CANONICAL_UUID_SQLITE_GLOB_PATTERN, foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyAddStatements, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaForeignKeys, schemaForeignKeysForEngine } from "../schema/foreign-key-ddl.js";
4
4
  import { detectEngine, getDDLStrategy } from "../schema/ddl/index.js";
5
+ import { maskSampleValue, probeCastSafety, renderJsonbColumnConversion, renderTimestamptzColumnConversion } from "../schema/text-cast-probe.js";
5
6
  import { describeForeignKeyTypeConflict, planUuidConvergence, renderUuidConvergenceRepairHint, renderUuidShapeProbe } from "../schema/uuid-convergence.js";
6
7
  import "../schema/utils.js";
7
8
  import { planSqliteTableRebuilds, sqliteRebuildPlaceholderSql } from "./sqlite-rebuild.js";
@@ -64,6 +65,18 @@ function uniqueColumnIndexName(tableName, columnName) {
64
65
  * True when a change is report-only: it carries an advisory and no
65
66
  * executable statement. Such changes never reach the migration stream.
66
67
  */
68
+ /**
69
+ * Single- vs double-precision float classification for #2770's float-width
70
+ * drift detection. `null` for anything that isn't unambiguously one or the
71
+ * other (DECIMAL/NUMERIC have no fixed binary width and are out of scope).
72
+ */
73
+ function floatPrecisionOf(type, engine) {
74
+ const upper = type.toUpperCase().trim().replace(/\(\s*\d+(\s*,\s*\d+)?\s*\)/g, "");
75
+ if (/^(REAL|FLOAT4)$/.test(upper)) return "single";
76
+ if (engine === "duckdb" && upper === "FLOAT") return "single";
77
+ if (/^(FLOAT8|DOUBLE|DOUBLE PRECISION|FLOAT)$/.test(upper)) return "double";
78
+ return null;
79
+ }
67
80
  function isAdvisoryOnlyChange(change) {
68
81
  if (!change.advisory) return false;
69
82
  return (change.sqlStatements ?? (change.sql ? [change.sql] : [])).length === 0;
@@ -475,6 +488,18 @@ var SchemaComparer = class {
475
488
  }
476
489
  }
477
490
  /**
491
+ * Probe a candidate `text` -> `timestamptz`/`jsonb` column (#2771, #2772)
492
+ * with a real, exception-safe cast attempt (see `text-cast-probe.ts`).
493
+ * Unlike {@link probeUuidShape}, a failed/unrealistic probe
494
+ * (`unavailable`) is the caller's cue to fall back to the pre-existing
495
+ * behavior for that pair rather than surface a new finding — this keeps
496
+ * the new convergence strictly additive for every case that isn't
497
+ * affirmatively confirmed safe or affirmatively confirmed unsafe.
498
+ */
499
+ async probeTextCastShape(tableName, columnName, kind) {
500
+ return probeCastSafety(this.db, tableName, columnName, kind);
501
+ }
502
+ /**
478
503
  * Turn the convergence plan into diff entries: executable `type_upgrade`
479
504
  * conversions marked `phase: 'pre_foreign_key'`, plus report-only warnings
480
505
  * for components the planner refused.
@@ -608,6 +633,7 @@ var SchemaComparer = class {
608
633
  table: tableName,
609
634
  name: foreignKeyConstraintName(tableName, foreignKey),
610
635
  foreignKey,
636
+ engineUnsupported: true,
611
637
  advisory: {
612
638
  severity: "warning",
613
639
  message: `${engineReason} Run the orphan detector, repair rows, and rebuild the table with the generated constraint.`,
@@ -645,6 +671,8 @@ var SchemaComparer = class {
645
671
  table: tableName,
646
672
  name: foreignKeyConstraintName(tableName, foreignKey),
647
673
  foreignKey,
674
+ orphanBlocked: true,
675
+ orphanNullable: orphanOptions.nullable,
648
676
  advisory: {
649
677
  severity: "warning",
650
678
  message: `Cannot add foreign key ${tableName}.${foreignKey.column}: existing rows do not match ${foreignKey.referencesTable}.${foreignKey.referencesColumn}. Repair them, then rerun.`,
@@ -659,7 +687,7 @@ var SchemaComparer = class {
659
687
  table: tableName,
660
688
  name: constraintName,
661
689
  foreignKey,
662
- sqlStatements: [`ALTER TABLE ${this.quoteIdentifier(tableName)} ADD ${renderForeignKeyConstraint(tableName, foreignKey)} NOT VALID`, `ALTER TABLE ${this.quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${this.quoteIdentifier(constraintName)}`]
690
+ sqlStatements: renderForeignKeyAddStatements(tableName, foreignKey)
663
691
  });
664
692
  }
665
693
  return changes;
@@ -680,7 +708,9 @@ var SchemaComparer = class {
680
708
  async getForeignKeyOrphanOptions(tableName, manifest, dbSchema, foreignKey, manifestSchemas) {
681
709
  const sourceColumn = manifest.columns[foreignKey.column];
682
710
  const targetColumn = manifestSchemas[foreignKey.referencesTable]?.columns[foreignKey.referencesColumn];
683
- const nullable = sourceColumn?.notNull !== true;
711
+ const manifestNullable = sourceColumn?.notNull !== true;
712
+ const liveNotNull = dbSchema.columns[foreignKey.column]?.notNull === true;
713
+ const nullable = manifestNullable && !liveNotNull;
684
714
  const uuidComparison = sourceColumn?.type === "UUID" && (targetColumn === void 0 || targetColumn.type === "UUID");
685
715
  if (!uuidComparison) return {
686
716
  nullable,
@@ -743,10 +773,148 @@ var SchemaComparer = class {
743
773
  const expectedEngineType = this.ddlStrategy.mapType(validatedType);
744
774
  const normalizedExpected = this.normalizeType(expectedEngineType);
745
775
  const normalizedActual = this.normalizeType(dbCol.type);
776
+ if ((this.engine === "postgres" || this.engine === "duckdb") && normalizedExpected === "REAL" && normalizedActual === "REAL") {
777
+ const expectedPrecision = floatPrecisionOf(expectedEngineType, this.engine);
778
+ const actualPrecision = floatPrecisionOf(dbCol.type, this.engine);
779
+ if (expectedPrecision && actualPrecision && expectedPrecision !== actualPrecision) {
780
+ typeDrifted = true;
781
+ const table = this.quoteIdentifier(tableName);
782
+ const column = this.quoteIdentifier(colName);
783
+ if (expectedPrecision === "double" && actualPrecision === "single") {
784
+ const targetType = this.engine === "postgres" ? "DOUBLE PRECISION" : "DOUBLE";
785
+ changes.push({
786
+ type: "type_upgrade",
787
+ table: tableName,
788
+ name: colName,
789
+ column: colDef,
790
+ mismatch: {
791
+ expected: expectedEngineType,
792
+ actual: dbCol.type
793
+ },
794
+ sql: `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${targetType} USING ${column}::${targetType}`
795
+ });
796
+ } else changes.push({
797
+ type: "type_upgrade",
798
+ table: tableName,
799
+ name: colName,
800
+ column: colDef,
801
+ mismatch: {
802
+ expected: expectedEngineType,
803
+ actual: dbCol.type
804
+ },
805
+ advisory: {
806
+ severity: "warning",
807
+ message: `blocked: ${tableName}.${colName} is declared single-precision (${expectedEngineType}) but the live column is double-precision (${dbCol.type}). Narrowing loses precision, so this stays manual: confirm the narrower declaration is intentional, or widen the manifest field instead of the column.`,
808
+ suggestedSql: [`ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${expectedEngineType} USING ${column}::${expectedEngineType}`]
809
+ }
810
+ });
811
+ }
812
+ }
746
813
  const isUuidTextEquivalent = this.isUuidTextEquivalentColumn(colName, colDef, normalizedExpected, normalizedActual);
747
- if (normalizedExpected !== normalizedActual && !isUuidTextEquivalent && !(normalizedExpected === "JSON" && normalizedActual === "TEXT" || normalizedExpected === "TEXT" && normalizedActual === "JSON")) {
814
+ const jsonUpgradeCandidate = this.engine === "postgres" && normalizedExpected === "JSON" && normalizedActual === "TEXT";
815
+ let jsonProbe;
816
+ if (jsonUpgradeCandidate) jsonProbe = await this.probeTextCastShape(tableName, colName, "jsonb");
817
+ const isJsonTextEquivalent = normalizedExpected === "TEXT" && normalizedActual === "JSON" || normalizedExpected === "JSON" && normalizedActual === "TEXT" && (!jsonUpgradeCandidate || jsonProbe?.status === "unavailable");
818
+ const timestamptzUpgradeCandidate = this.engine === "postgres" && this.normalizeType(colDef.type) === "TIMESTAMP" && normalizedActual === "TEXT" && normalizedExpected === "TIMESTAMPTZ" && !this.isCompatibleTypeUpgrade(colDef.type, dbCol.type);
819
+ let timestamptzProbe;
820
+ if (timestamptzUpgradeCandidate) timestamptzProbe = await this.probeTextCastShape(tableName, colName, "timestamptz");
821
+ if (normalizedExpected !== normalizedActual && !isUuidTextEquivalent && !isJsonTextEquivalent) {
748
822
  typeDrifted = true;
749
- if (this.isCompatibleTypeUpgrade(colDef.type, dbCol.type)) {
823
+ const hasLiveDefault = dbCol.defaultValue !== null && dbCol.defaultValue !== void 0;
824
+ const conversionOptions = {
825
+ hasLiveDefault,
826
+ manifestDefaultValue: colDef.defaultValue
827
+ };
828
+ const liveOnlyDefaultNeedsRelaxOptIn = hasLiveDefault && colDef.defaultValue === void 0 && !this.options.relaxColumns;
829
+ if (jsonUpgradeCandidate && jsonProbe?.status === "clean" && liveOnlyDefaultNeedsRelaxOptIn) changes.push({
830
+ type: "type_upgrade",
831
+ table: tableName,
832
+ name: colName,
833
+ column: colDef,
834
+ mismatch: {
835
+ expected: colDef.type,
836
+ actual: dbCol.type
837
+ },
838
+ advisory: {
839
+ severity: "warning",
840
+ message: `blocked: ${tableName}.${colName} has a live default (${String(dbCol.defaultValue)}) the manifest no longer declares; converging to jsonb requires dropping it first. ${this.relaxHint("drop it as part of this conversion")}`,
841
+ suggestedSql: renderJsonbColumnConversion(tableName, colName, conversionOptions)
842
+ }
843
+ });
844
+ else if (jsonUpgradeCandidate && jsonProbe?.status === "clean") {
845
+ const statements = renderJsonbColumnConversion(tableName, colName, conversionOptions);
846
+ changes.push({
847
+ type: "type_upgrade",
848
+ table: tableName,
849
+ name: colName,
850
+ column: colDef,
851
+ mismatch: {
852
+ expected: colDef.type,
853
+ actual: dbCol.type
854
+ },
855
+ sql: statements[statements.length - 1],
856
+ sqlStatements: statements
857
+ });
858
+ } else if (jsonUpgradeCandidate && jsonProbe?.status === "dirty") changes.push({
859
+ type: "type_upgrade",
860
+ table: tableName,
861
+ name: colName,
862
+ column: colDef,
863
+ mismatch: {
864
+ expected: colDef.type,
865
+ actual: dbCol.type
866
+ },
867
+ advisory: {
868
+ severity: "warning",
869
+ message: `blocked: ${tableName}.${colName} is declared JSON but ${jsonProbe.count} live value(s) are not valid JSON (sample: ${jsonProbe.sample ? maskSampleValue(jsonProbe.sample) : "unavailable"}). Repair or clear the offending value(s), then rerun \`smrt db:migrate\`.`,
870
+ suggestedSql: renderJsonbColumnConversion(tableName, colName, conversionOptions)
871
+ }
872
+ });
873
+ else if (timestamptzUpgradeCandidate && timestamptzProbe?.status === "clean" && liveOnlyDefaultNeedsRelaxOptIn) changes.push({
874
+ type: "type_upgrade",
875
+ table: tableName,
876
+ name: colName,
877
+ column: colDef,
878
+ mismatch: {
879
+ expected: colDef.type,
880
+ actual: dbCol.type
881
+ },
882
+ advisory: {
883
+ severity: "warning",
884
+ message: `blocked: ${tableName}.${colName} has a live default (${String(dbCol.defaultValue)}) the manifest no longer declares; converging to timestamptz requires dropping it first. ${this.relaxHint("drop it as part of this conversion")}`,
885
+ suggestedSql: renderTimestamptzColumnConversion(tableName, colName, conversionOptions)
886
+ }
887
+ });
888
+ else if (timestamptzUpgradeCandidate && timestamptzProbe?.status === "clean") {
889
+ const statements = renderTimestamptzColumnConversion(tableName, colName, conversionOptions);
890
+ changes.push({
891
+ type: "type_upgrade",
892
+ table: tableName,
893
+ name: colName,
894
+ column: colDef,
895
+ mismatch: {
896
+ expected: colDef.type,
897
+ actual: dbCol.type
898
+ },
899
+ sql: statements[statements.length - 1],
900
+ sqlStatements: statements
901
+ });
902
+ } else if (timestamptzUpgradeCandidate && timestamptzProbe?.status === "dirty") changes.push({
903
+ type: "type_upgrade",
904
+ table: tableName,
905
+ name: colName,
906
+ column: colDef,
907
+ mismatch: {
908
+ expected: colDef.type,
909
+ actual: dbCol.type
910
+ },
911
+ advisory: {
912
+ severity: "warning",
913
+ message: `blocked: ${tableName}.${colName} is declared TIMESTAMP but ${timestamptzProbe.count} live value(s) do not parse as an unambiguous timestamp (sample: ${timestamptzProbe.sample ? maskSampleValue(timestamptzProbe.sample) : "unavailable"}). Repair the offending value(s), or confirm legacy naive wall-clock provenance with \`smrt db:migrate --postgres-timestamp-legacy-timezone=UTC\`, then rerun.`,
914
+ suggestedSql: renderTimestamptzColumnConversion(tableName, colName, conversionOptions)
915
+ }
916
+ });
917
+ else if (this.isCompatibleTypeUpgrade(colDef.type, dbCol.type)) {
750
918
  const generatedSQL = this.generateTypeUpgradeSQL(tableName, colName, colDef, dbCol.type);
751
919
  changes.push({
752
920
  type: "type_upgrade",
@@ -791,9 +959,199 @@ var SchemaComparer = class {
791
959
  }
792
960
  changes.push(this.describeOrphanColumn(tableName, colName, dbCol));
793
961
  }
962
+ changes.push(...await this.detectRenameDataPending(tableName, manifest, dbSchema));
794
963
  return changes;
795
964
  }
796
965
  /**
966
+ * Detect a pending rename backfill (#2752): a manifest-declared column
967
+ * that exists live but holds no data, paired with an orphan (undeclared)
968
+ * live column of a compatible type that does hold data — the shape a
969
+ * framework field rename leaves behind when `db:migrate` adds the new
970
+ * column additively and never moves data into it. For each such pair,
971
+ * emit the copy-then-drop repair as an advisory: never executed,
972
+ * PostgreSQL and SQLite only (DuckDB/JSON are out of scope). The repair
973
+ * is genuinely idempotent SQL on PostgreSQL (a self-guarding
974
+ * `DO $$ ... $$` block); SQLite has no conditional-DDL construct, so its
975
+ * repair is operator-mediated instead — a guard query plus instructions,
976
+ * not a blind-rerun-safe statement. See {@link describeRenameDataPending}.
977
+ */
978
+ async detectRenameDataPending(tableName, manifest, dbSchema) {
979
+ if (this.engine !== "postgres" && this.engine !== "sqlite") return [];
980
+ const dbColumnNames = new Set(Object.keys(dbSchema.columns));
981
+ const manifestColumnNames = new Set(Object.keys(manifest.columns));
982
+ const orphanColumnNames = [...dbColumnNames].filter((name) => !manifestColumnNames.has(name));
983
+ if (orphanColumnNames.length === 0) return [];
984
+ const changes = [];
985
+ for (const [colName, colDef] of Object.entries(manifest.columns)) {
986
+ if (!dbSchema.columns[colName]) continue;
987
+ let declaredHasData;
988
+ try {
989
+ declaredHasData = await this.columnHasNonEmptyValue(tableName, colName);
990
+ } catch {
991
+ continue;
992
+ }
993
+ if (declaredHasData) continue;
994
+ const validatedType = isValidSQLDataType(colDef.type) ? colDef.type : "TEXT";
995
+ const isLogicalUuid = validatedType === "UUID";
996
+ const declaredNormalized = this.normalizeType(this.ddlStrategy.mapType(validatedType));
997
+ const candidates = [];
998
+ for (const orphanName of orphanColumnNames) {
999
+ const orphanCol = dbSchema.columns[orphanName];
1000
+ const orphanNormalized = this.normalizeType(orphanCol.type);
1001
+ const requiresShapeCheck = isLogicalUuid && orphanNormalized === "TEXT";
1002
+ if (!requiresShapeCheck && !(orphanNormalized === declaredNormalized)) continue;
1003
+ let orphanHasData;
1004
+ try {
1005
+ orphanHasData = await this.columnHasNonEmptyValue(tableName, orphanName);
1006
+ } catch {
1007
+ continue;
1008
+ }
1009
+ if (!orphanHasData) continue;
1010
+ if (requiresShapeCheck) {
1011
+ let shaped;
1012
+ try {
1013
+ shaped = await this.allNonEmptyValuesUuidShaped(tableName, orphanName);
1014
+ } catch {
1015
+ continue;
1016
+ }
1017
+ if (!shaped) continue;
1018
+ }
1019
+ candidates.push({
1020
+ orphanName,
1021
+ isUuidCast: declaredNormalized === "UUID"
1022
+ });
1023
+ }
1024
+ if (candidates.length === 1) changes.push(this.describeRenameDataPending(tableName, colName, candidates[0].orphanName, candidates[0].isUuidCast));
1025
+ else if (candidates.length > 1) changes.push(this.describeRenameDataPendingAmbiguous(tableName, colName, candidates.map((c) => c.orphanName)));
1026
+ }
1027
+ return changes;
1028
+ }
1029
+ /**
1030
+ * Live-data probe: does the column hold any non-null, non-empty value?
1031
+ * Used by {@link detectRenameDataPending}. Errors propagate to the
1032
+ * caller, which skips the pair rather than guessing (#2752).
1033
+ */
1034
+ async columnHasNonEmptyValue(tableName, colName) {
1035
+ const quotedTable = this.quoteIdentifier(tableName);
1036
+ const quotedCol = this.quoteIdentifier(colName);
1037
+ return ((await this.db.query(`SELECT 1 AS present FROM ${quotedTable} WHERE ${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> '' LIMIT 1`)).rows?.length ?? 0) > 0;
1038
+ }
1039
+ /**
1040
+ * Live-data probe: are every one of a column's non-empty values
1041
+ * UUID-shaped ({@link CANONICAL_UUID_PATTERN})? PostgreSQL pushes the
1042
+ * check into the query with its regex operator; SQLite has no regex
1043
+ * operator, but its case-sensitive `GLOB` can still express the fixed
1044
+ * 36-character canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN}
1045
+ * against `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check), so
1046
+ * both engines run one server-side aggregate `count(*)` rather than
1047
+ * fetching every non-empty value into JS to test in a loop — important on
1048
+ * a production table with many rows (#2767 review).
1049
+ */
1050
+ async allNonEmptyValuesUuidShaped(tableName, colName) {
1051
+ const quotedTable = this.quoteIdentifier(tableName);
1052
+ const quotedCol = this.quoteIdentifier(colName);
1053
+ const nonEmptyPredicate = `${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> ''`;
1054
+ if (this.engine === "postgres") {
1055
+ const row = (await this.db.query(`SELECT count(*) AS invalid_count FROM ${quotedTable} WHERE ${nonEmptyPredicate} AND CAST(${quotedCol} AS TEXT) !~* '${CANONICAL_UUID_PATTERN}'`)).rows?.[0];
1056
+ return Number(row?.invalid_count ?? 0) === 0;
1057
+ }
1058
+ const row = (await this.db.query(`SELECT count(*) AS invalid_count FROM ${quotedTable} WHERE ${nonEmptyPredicate} AND NOT (LENGTH(CAST(${quotedCol} AS TEXT)) = 36 AND LOWER(CAST(${quotedCol} AS TEXT)) GLOB '${CANONICAL_UUID_SQLITE_GLOB_PATTERN}')`)).rows?.[0];
1059
+ return Number(row?.invalid_count ?? 0) === 0;
1060
+ }
1061
+ /**
1062
+ * Render the repair for one rename-data-pending pair (#2752): copy
1063
+ * non-empty `oldColumn` into `newColumn` only where `newColumn` is still
1064
+ * empty (casting to `uuid` when the new column is native uuid, otherwise a
1065
+ * plain copy), then drop `oldColumn`.
1066
+ *
1067
+ * Idempotency review findings and the fix:
1068
+ *
1069
+ * - (P1) The `newColumn`-is-empty predicate previously compared any
1070
+ * non-uuid destination to `''` (`col IS NULL OR col = ''`), which is
1071
+ * only valid for text-affinity types — PostgreSQL rejects `col = ''`
1072
+ * for `integer`/`boolean`/`timestamp`/etc. before a same-type rename
1073
+ * pair of one of those types ever copies anything. Fixed by comparing
1074
+ * `CAST(col AS TEXT) = ''` instead, mirroring the same portable
1075
+ * emptiness check {@link columnHasNonEmptyValue} already uses for
1076
+ * detection, so the predicate is valid for every column type.
1077
+ * - (P2) An `UPDATE` unconditionally naming `oldColumn` fails once that
1078
+ * column is gone, on either engine, regardless of the DROP's own
1079
+ * idempotency — a guard query placed before the DROP (or even before
1080
+ * the UPDATE) still leaves a human decision between "run" and "don't",
1081
+ * which is not genuine SQL-level idempotency. PostgreSQL CAN express a
1082
+ * truly self-contained, unconditionally-safe-to-rerun statement here:
1083
+ * an anonymous `DO $$ ... $$` block (the same idiom this file already
1084
+ * uses for `generatePostgresIntegerPreflightSQL`) that checks
1085
+ * `information_schema.columns` and only runs the UPDATE + DROP when the
1086
+ * old column still exists — one executable statement, no operator
1087
+ * judgment required, safe to rerun any number of times. SQLite has no
1088
+ * procedural block or conditional-DDL construct at all (confirmed
1089
+ * against a real connection), so there the contract is explicitly
1090
+ * downgraded from "idempotent SQL" to "operator-mediated": a guard
1091
+ * query plus instructions, not a single statement that is itself safe
1092
+ * to blindly rerun.
1093
+ *
1094
+ * This is report-only either way: `db:migrate` never executes it.
1095
+ */
1096
+ describeRenameDataPending(tableName, newColumn, oldColumn, newIsNativeUuid) {
1097
+ const quotedTable = this.quoteIdentifier(tableName);
1098
+ const quotedNew = this.quoteIdentifier(newColumn);
1099
+ const quotedOld = this.quoteIdentifier(oldColumn);
1100
+ const copySql = `UPDATE ${quotedTable} SET ${quotedNew} = ${newIsNativeUuid ? `${quotedOld}::uuid` : quotedOld} WHERE ${newIsNativeUuid ? `${quotedNew} IS NULL` : `(${quotedNew} IS NULL OR CAST(${quotedNew} AS TEXT) = '')`} AND ${quotedOld} IS NOT NULL AND CAST(${quotedOld} AS TEXT) <> ''`;
1101
+ let suggestedSql;
1102
+ let dropIdempotencyNote;
1103
+ if (this.engine === "postgres") {
1104
+ suggestedSql = [`DO $$ BEGIN IF ${`EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ${this.quoteLiteral(tableName)} AND column_name = ${this.quoteLiteral(oldColumn)})`} THEN ${copySql}; ${`ALTER TABLE ${quotedTable} DROP COLUMN ${quotedOld}`}; END IF; END $$`];
1105
+ dropIdempotencyNote = `This single statement is idempotent: rerunning it after the rename is complete is a genuine no-op, because it only touches ${oldColumn} when the block finds it still exists.`;
1106
+ } else {
1107
+ suggestedSql = [
1108
+ `SELECT count(*) AS old_column_still_present FROM pragma_table_info(${this.quoteLiteral(tableName)}) WHERE name = ${this.quoteLiteral(oldColumn)}`,
1109
+ copySql,
1110
+ this.generateDropColumnSQL(tableName, oldColumn)
1111
+ ];
1112
+ dropIdempotencyNote = `SQLite has no conditional-DDL construct, so this is operator-mediated, not self-contained idempotent SQL: run the guard query first, and only run the UPDATE and DROP below when it reports ${oldColumn} still present.`;
1113
+ }
1114
+ return {
1115
+ type: "rename_data_pending",
1116
+ table: tableName,
1117
+ name: newColumn,
1118
+ mismatch: {
1119
+ expected: `data in ${newColumn}`,
1120
+ actual: `data appears to still be in ${oldColumn}`
1121
+ },
1122
+ advisory: {
1123
+ severity: "warning",
1124
+ message: `${tableName}.${newColumn} is declared but empty, while undeclared column ${tableName}.${oldColumn} holds data of a compatible type. This looks like a framework field rename whose data was never moved (db:migrate adds columns additively and never drops the old one). ${dropIdempotencyNote}`,
1125
+ suggestedSql
1126
+ }
1127
+ };
1128
+ }
1129
+ /**
1130
+ * Render the advisory for an ambiguous rename-data-pending match (#2767
1131
+ * review): more than one orphan column is a populated, type-compatible
1132
+ * candidate for the same empty declared column, so the real rename source
1133
+ * cannot be inferred. Unlike {@link describeRenameDataPending}, this
1134
+ * carries no `suggestedSql` — running a per-candidate copy-and-drop would
1135
+ * let output order decide which candidate's data wins and then drop every
1136
+ * one of them, which is not a repair an advisory should suggest blindly.
1137
+ */
1138
+ describeRenameDataPendingAmbiguous(tableName, newColumn, candidateColumns) {
1139
+ const candidateList = candidateColumns.map((name) => `${tableName}.${name}`).join(", ");
1140
+ return {
1141
+ type: "rename_data_pending",
1142
+ table: tableName,
1143
+ name: newColumn,
1144
+ mismatch: {
1145
+ expected: `data in ${newColumn}`,
1146
+ actual: `data appears to still be in one of several columns: ${candidateList}`
1147
+ },
1148
+ advisory: {
1149
+ severity: "warning",
1150
+ message: `${tableName}.${newColumn} is declared but empty, while ${candidateColumns.length} undeclared columns of a compatible type hold data (${candidateList}). This looks like a framework field rename, but which column is the actual rename source is ambiguous — no suggested repair SQL is printed. An operator must identify the correct source column and write its own targeted copy-then-drop.`
1151
+ }
1152
+ };
1153
+ }
1154
+ /**
797
1155
  * Compare nullability and default between a manifest column and the live
798
1156
  * column, emitting `alter_column` changes (#2369).
799
1157
  *