@happyvertical/smrt-core 0.52.0 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/AGENTS.md +1 -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/dist/cascade.d.ts.map +1 -1
  7. package/dist/cascade.js +4 -2
  8. package/dist/cascade.js.map +1 -1
  9. package/dist/generators/preflight-route.d.ts.map +1 -1
  10. package/dist/generators/preflight-route.js +1 -1
  11. package/dist/generators/preflight-route.js.map +1 -1
  12. package/dist/knowledge.d.ts.map +1 -1
  13. package/dist/knowledge.js +3 -0
  14. package/dist/knowledge.js.map +1 -1
  15. package/dist/manifest/static-manifest.js +1 -1
  16. package/dist/manifest/static-manifest.js.map +1 -1
  17. package/dist/manifest/store.js +1 -1
  18. package/dist/manifest.json +1 -1
  19. package/dist/migrations/index.d.ts +1 -0
  20. package/dist/migrations/index.d.ts.map +1 -1
  21. package/dist/migrations/index.js +2 -1
  22. package/dist/migrations/qualified-name-aliases.d.ts +134 -0
  23. package/dist/migrations/qualified-name-aliases.d.ts.map +1 -0
  24. package/dist/migrations/qualified-name-aliases.js +294 -0
  25. package/dist/migrations/qualified-name-aliases.js.map +1 -0
  26. package/dist/migrations.js +2 -1
  27. package/dist/polymorphic-association.d.ts +18 -0
  28. package/dist/polymorphic-association.d.ts.map +1 -1
  29. package/dist/polymorphic-association.js +27 -2
  30. package/dist/polymorphic-association.js.map +1 -1
  31. package/dist/registry/class-registration.d.ts.map +1 -1
  32. package/dist/registry/class-registration.js +27 -4
  33. package/dist/registry/class-registration.js.map +1 -1
  34. package/dist/registry/name-resolver.d.ts +8 -2
  35. package/dist/registry/name-resolver.d.ts.map +1 -1
  36. package/dist/registry/name-resolver.js +17 -8
  37. package/dist/registry/name-resolver.js.map +1 -1
  38. package/dist/registry/qualified-name-aliases.d.ts +109 -0
  39. package/dist/registry/qualified-name-aliases.d.ts.map +1 -0
  40. package/dist/registry/qualified-name-aliases.js +286 -0
  41. package/dist/registry/qualified-name-aliases.js.map +1 -0
  42. package/dist/registry/relationship-graph.d.ts.map +1 -1
  43. package/dist/registry/relationship-graph.js +1 -1
  44. package/dist/registry/relationship-graph.js.map +1 -1
  45. package/dist/registry/types.d.ts +26 -0
  46. package/dist/registry/types.d.ts.map +1 -1
  47. package/dist/registry.d.ts +59 -2
  48. package/dist/registry.d.ts.map +1 -1
  49. package/dist/registry.js +115 -6
  50. package/dist/registry.js.map +1 -1
  51. package/dist/scanner/manifest-generator.d.ts +11 -0
  52. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  53. package/dist/scanner/manifest-generator.js +36 -1
  54. package/dist/scanner/manifest-generator.js.map +1 -1
  55. package/dist/schema/conflict-target.js +8 -1
  56. package/dist/schema/conflict-target.js.map +1 -1
  57. package/dist/scripts/browser-plain-json-regression.mjs +4 -1
  58. package/dist/smrt-knowledge.json +15 -9
  59. package/dist/utils/qualified-names.d.ts +11 -0
  60. package/dist/utils/qualified-names.d.ts.map +1 -1
  61. package/dist/utils/qualified-names.js +21 -1
  62. package/dist/utils/qualified-names.js.map +1 -1
  63. 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 };
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qualified-name-aliases.js","names":[],"sources":["../../src/migrations/qualified-name-aliases.ts"],"sourcesContent":["/**\n * Stored references to deprecated qualified names (#3338).\n *\n * `@smrt({ previousQualifiedNames })` keeps an old qualified name resolving\n * after a class moves package; it never rewrites stored data. This module is\n * the operator side of that bridge:\n *\n * - {@link countLegacyQualifiedNameReferences} — read-only counts of rows that\n * still store an old name, which `smrt doctor --db` reports so a consumer\n * can see when an alias is safe to remove;\n * - {@link backfillLegacyQualifiedNames} — an opt-in, idempotent rewrite of\n * those rows to the current name. It never runs automatically.\n *\n * Stored references covered:\n *\n * - `meta_type` of every registered `SmrtPolymorphicAssociation` subclass\n * table (any class can be a polymorphic target, so every alias is counted);\n * - `_meta_type` of an STI table whose family includes an aliased class.\n *\n * Removing an alias is a breaking change for any row this still counts.\n */\n\nimport { createHash } from 'node:crypto';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ObjectRegistry } from '../registry';\nimport { isSmrtCollectionExtendsName } from '../registry/collection-resolution.js';\nimport { isFrameworkBaseClass } from '../registry/framework-base-classes.js';\nimport {\n IMPLICIT_TENANT_COLUMN,\n IMPLICIT_TENANT_FIELD,\n} from '../schema/conflict-target.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { tableExists } from '../system/compatibility.js';\nimport { toSnakeCase } from '../utils/naming.js';\nimport { BackfillTracker } from './backfill-tracker.js';\n\n/** Prefix of the `_smrt_backfills` marker {@link backfillLegacyQualifiedNames} records. */\nexport const LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX =\n '@happyvertical/smrt-core:qualified-name-aliases:v1';\n\n/** One table column that can store a class's qualified name. */\nexport interface LegacyQualifiedNameTarget {\n /** Registered class that owns the table (qualified when available). */\n className: string;\n table: string;\n /** `meta_type` (polymorphic association) or `_meta_type` (STI discriminator). */\n column: string;\n kind: 'polymorphic' | 'sti';\n /** Deprecated names this column may still hold. */\n aliases: string[];\n /** Tenant column of the table, when tenant-owned. */\n tenantColumn?: string;\n /**\n * Upsert identity columns that include `column`. A rewrite that would\n * duplicate a current-name row on these columns is skipped, never forced.\n */\n conflictColumns?: string[];\n}\n\n/** Rows of one target that still store one deprecated name. */\nexport interface LegacyQualifiedNameReference {\n table: string;\n column: string;\n kind: 'polymorphic' | 'sti';\n alias: string;\n current: string;\n count: number;\n}\n\nexport interface LegacyQualifiedNameReport {\n /** Declared alias → current qualified name, sorted by alias. */\n aliases: Array<{ alias: string; current: string }>;\n /** Non-zero counts only, sorted by table, column, alias. */\n references: LegacyQualifiedNameReference[];\n /** Sum of `references[].count`. */\n total: number;\n /** Target tables absent from this database (nothing to count). */\n missingTables: string[];\n /** Target tables excluded from a tenant-scoped run (no tenant column). */\n untenantedTables: string[];\n}\n\nexport interface LegacyQualifiedNameOptions {\n /**\n * Restrict counting/rewriting to one tenant's rows. Tables without a tenant\n * column are then skipped (reported in `untenantedTables`), never touched.\n */\n tenantId?: string;\n /** Engine hint when the adapter URL does not identify it. */\n engineHint?: string;\n}\n\nexport interface LegacyQualifiedNameBackfillOptions\n extends LegacyQualifiedNameOptions {\n /** Report what would change without writing (no marker is recorded). */\n dryRun?: boolean;\n /** Re-run even when this alias set's marker is already recorded. */\n force?: boolean;\n}\n\nexport interface LegacyQualifiedNameBackfillResult {\n /** `false` when the marker was already recorded (and `force` was unset) or on a dry run. */\n ran: boolean;\n dryRun: boolean;\n /** The `_smrt_backfills` marker for this alias set and scope. */\n backfillName: string;\n /** Counts before the rewrite. */\n before: LegacyQualifiedNameReport;\n /** Rows rewritten per table/column/alias. */\n rewritten: LegacyQualifiedNameReference[];\n /**\n * Rows left under their old name because the rewrite would duplicate a row\n * already stored under the current name (resolve those by hand, then re-run).\n */\n skippedDuplicates: LegacyQualifiedNameReference[];\n /** Whether the marker was recorded (only when nothing remains in scope). */\n recorded: boolean;\n}\n\ntype AliasPair = { alias: string; current: string };\n\nfunction compareText(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\nfunction aliasPairs(): AliasPair[] {\n return [...ObjectRegistry.getQualifiedNameAliases()].map(\n ([alias, current]) => ({ alias, current }),\n );\n}\n\nfunction quoteIdentifier(name: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) {\n throw new Error(`Refusing to quote unexpected identifier \"${name}\"`);\n }\n return `\"${name}\"`;\n}\n\nfunction isPolymorphicAssociationFields(fields: Map<string, unknown>): boolean {\n // Same rule as delete planning (cascade.ts): all three columns\n // `SmrtPolymorphicAssociation` contributes.\n return fields.has('metaType') && fields.has('metaId') && fields.has('role');\n}\n\nfunction safeConflictColumns(className: string): string[] {\n try {\n return ObjectRegistry.getConflictColumns(className).map((column) =>\n toSnakeCase(column),\n );\n } catch {\n return [];\n }\n}\n\nfunction safeTenantColumn(className: string): string | undefined {\n let declared: string | undefined;\n try {\n declared = ObjectRegistry.getTenantColumn(className);\n } catch {\n declared = undefined;\n }\n if (declared) return declared;\n // A `tenantId` field makes rows tenant-owned without any declaration.\n return ObjectRegistry.getFields(className).has(IMPLICIT_TENANT_FIELD)\n ? IMPLICIT_TENANT_COLUMN\n : undefined;\n}\n\n/**\n * Every table column that can store a deprecated qualified name, derived from\n * the booted registry. Empty when no class declares `previousQualifiedNames`.\n */\nexport function collectLegacyQualifiedNameTargets(): LegacyQualifiedNameTarget[] {\n const pairs = aliasPairs();\n if (pairs.length === 0) return [];\n const allAliases = pairs.map((pair) => pair.alias);\n const byKey = new Map<string, LegacyQualifiedNameTarget>();\n\n const add = (target: LegacyQualifiedNameTarget) => {\n const key = `${target.table}\\u0000${target.column}`;\n const existing = byKey.get(key);\n if (!existing) {\n byKey.set(key, target);\n return;\n }\n existing.aliases = [\n ...new Set([...existing.aliases, ...target.aliases]),\n ].sort(compareText);\n };\n\n for (const className of ObjectRegistry.getQualifiedClassNames()) {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) continue;\n if (isSmrtCollectionExtendsName(registered.extends)) continue;\n if (isFrameworkBaseClass(registered.name, registered.packageName)) continue;\n if (!isPolymorphicAssociationFields(registered.fields)) continue;\n const table = ObjectRegistry.getTableName(className);\n if (!table) continue;\n const conflictColumns = safeConflictColumns(className);\n const tenantColumn = safeTenantColumn(className);\n add({\n className,\n table,\n column: 'meta_type',\n kind: 'polymorphic',\n aliases: [...allAliases].sort(compareText),\n ...(tenantColumn ? { tenantColumn } : {}),\n ...(conflictColumns.includes('meta_type') ? { conflictColumns } : {}),\n });\n }\n\n for (const { alias, current } of pairs) {\n if (ObjectRegistry.getTableStrategy(current) !== 'sti') continue;\n const table = ObjectRegistry.getTableName(current);\n if (!table) continue;\n const conflictColumns = safeConflictColumns(current);\n const tenantColumn = safeTenantColumn(current);\n add({\n className: current,\n table,\n column: '_meta_type',\n kind: 'sti',\n aliases: [alias],\n ...(tenantColumn ? { tenantColumn } : {}),\n ...(conflictColumns.includes('_meta_type') ? { conflictColumns } : {}),\n });\n }\n\n return [...byKey.values()].sort(\n (left, right) =>\n compareText(left.table, right.table) ||\n compareText(left.column, right.column),\n );\n}\n\nfunction rowsOf(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) return result as Record<string, unknown>[];\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) return rows as Record<string, unknown>[];\n }\n return [];\n}\n\nfunction resolveEngine(db: DatabaseInterface, engineHint?: string): string {\n const withConfig = db as DatabaseInterface & {\n config?: { url?: string; type?: string };\n type?: string;\n };\n return detectEngine(\n db.url || withConfig.config?.url || '',\n engineHint || withConfig.type || withConfig.config?.type,\n );\n}\n\nasync function countTarget(\n db: DatabaseInterface,\n target: LegacyQualifiedNameTarget,\n tenantId: string | undefined,\n): Promise<Map<string, number>> {\n const counts = new Map<string, number>();\n if (target.aliases.length === 0) return counts;\n const column = quoteIdentifier(target.column);\n const params: unknown[] = [...target.aliases];\n let sql =\n `SELECT ${column} AS name, COUNT(*) AS n FROM ${quoteIdentifier(target.table)} ` +\n `WHERE ${column} IN (${target.aliases.map(() => '?').join(', ')})`;\n if (tenantId !== undefined && target.tenantColumn) {\n sql += ` AND ${quoteIdentifier(target.tenantColumn)} = ?`;\n params.push(tenantId);\n }\n sql += ` GROUP BY ${column}`;\n for (const row of rowsOf(await db.query(sql, ...params))) {\n const count = Number(row.n ?? 0);\n if (count > 0) counts.set(String(row.name), count);\n }\n return counts;\n}\n\n/**\n * Count rows that still store a deprecated qualified name. Read-only.\n *\n * Absent tables are reported in `missingTables`, not as errors: the registry\n * can carry classes this database never migrated.\n */\nexport async function countLegacyQualifiedNameReferences(\n db: DatabaseInterface,\n options: LegacyQualifiedNameOptions = {},\n): Promise<LegacyQualifiedNameReport> {\n const pairs = aliasPairs();\n const currentByAlias = new Map(pairs.map((pair) => [pair.alias, pair]));\n const report: LegacyQualifiedNameReport = {\n aliases: pairs,\n references: [],\n total: 0,\n missingTables: [],\n untenantedTables: [],\n };\n const engine = resolveEngine(db, options.engineHint);\n\n for (const target of collectLegacyQualifiedNameTargets()) {\n if (options.tenantId !== undefined && !target.tenantColumn) {\n if (!report.untenantedTables.includes(target.table)) {\n report.untenantedTables.push(target.table);\n }\n continue;\n }\n if (!(await tableExists(db, target.table, engine))) {\n if (!report.missingTables.includes(target.table)) {\n report.missingTables.push(target.table);\n }\n continue;\n }\n const counts = await countTarget(db, target, options.tenantId);\n for (const alias of target.aliases) {\n const count = counts.get(alias) ?? 0;\n if (count === 0) continue;\n report.references.push({\n table: target.table,\n column: target.column,\n kind: target.kind,\n alias,\n current: currentByAlias.get(alias)?.current ?? alias,\n count,\n });\n report.total += count;\n }\n }\n\n report.missingTables.sort(compareText);\n report.untenantedTables.sort(compareText);\n return report;\n}\n\n/**\n * Stable `_smrt_backfills` marker for the declared alias set and scope. A new\n * alias (another model move) produces a new marker, so the backfill runs\n * again for it.\n */\nexport function legacyQualifiedNamesBackfillName(\n pairs: Array<{ alias: string; current: string }> = aliasPairs(),\n tenantId?: string,\n): string {\n const digest = createHash('sha256')\n .update(\n [...pairs]\n .sort((left, right) => compareText(left.alias, right.alias))\n .map(({ alias, current }) => `${alias}=>${current}`)\n .join('\\n'),\n )\n .digest('hex')\n .slice(0, 16);\n return tenantId === undefined\n ? `${LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX}:${digest}`\n : `${LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX}:${digest}:tenant:${tenantId}`;\n}\n\nfunction nullSafeEquals(engine: string, left: string, right: string): string {\n return engine === 'sqlite'\n ? `${left} IS ${right}`\n : `${left} IS NOT DISTINCT FROM ${right}`;\n}\n\nfunction buildRewriteStatement(\n engine: string,\n target: LegacyQualifiedNameTarget,\n tenantId: string | undefined,\n alias: string,\n current: string,\n): { sql: string; params: unknown[] } {\n const table = quoteIdentifier(target.table);\n const column = quoteIdentifier(target.column);\n const params: unknown[] = [current, alias];\n let sql = `UPDATE ${table} SET ${column} = ? WHERE ${column} = ?`;\n if (tenantId !== undefined && target.tenantColumn) {\n sql += ` AND ${quoteIdentifier(target.tenantColumn)} = ?`;\n params.push(tenantId);\n }\n const identity = (target.conflictColumns ?? []).filter(\n (name) => name !== target.column,\n );\n if (target.conflictColumns) {\n // Never rewrite a row onto the identity of one already stored under the\n // current name: that is a duplicate association, which an operator must\n // resolve, not a unique-violation the rewrite should provoke.\n const correlated = identity\n .map((name) =>\n nullSafeEquals(\n engine,\n `__smrt_current.${quoteIdentifier(name)}`,\n `${table}.${quoteIdentifier(name)}`,\n ),\n )\n .join(' AND ');\n sql +=\n ` AND NOT EXISTS (SELECT 1 FROM ${table} AS __smrt_current ` +\n `WHERE __smrt_current.${column} = ?${correlated ? ` AND ${correlated}` : ''})`;\n params.push(current);\n }\n return { sql, params };\n}\n\n/**\n * Rewrite stored deprecated qualified names to their current names.\n *\n * Opt-in and idempotent: it never runs on its own, rewrites only rows whose\n * value is exactly a declared old name, and records a `_smrt_backfills`\n * marker (per alias set and tenant scope) once nothing in scope remains, so a\n * second call is a no-op unless `force` is set. Rows are rewritten inside one\n * transaction where the adapter provides one; the tenant column, ids and all\n * other values are never touched, and a tenant-scoped run only touches that\n * tenant's rows in tenant-owned tables. A row whose rewrite would duplicate a\n * row already stored under the current name is skipped and reported.\n */\nexport async function backfillLegacyQualifiedNames(\n db: DatabaseInterface,\n options: LegacyQualifiedNameBackfillOptions = {},\n): Promise<LegacyQualifiedNameBackfillResult> {\n const pairs = aliasPairs();\n const backfillName = legacyQualifiedNamesBackfillName(\n pairs,\n options.tenantId,\n );\n const dryRun = options.dryRun === true;\n const before = await countLegacyQualifiedNameReferences(db, options);\n const result: LegacyQualifiedNameBackfillResult = {\n ran: false,\n dryRun,\n backfillName,\n before,\n rewritten: [],\n skippedDuplicates: [],\n recorded: false,\n };\n if (pairs.length === 0 || dryRun) return result;\n\n const tracker = new BackfillTracker({ db });\n if (!options.force && (await tracker.isApplied(backfillName))) {\n return result;\n }\n\n const engine = resolveEngine(db, options.engineHint);\n const targets = new Map(\n collectLegacyQualifiedNameTargets().map((target) => [\n `${target.table}\\u0000${target.column}`,\n target,\n ]),\n );\n\n const rewrite = async (handle: DatabaseInterface) => {\n for (const reference of before.references) {\n const target = targets.get(`${reference.table}\\u0000${reference.column}`);\n if (!target) continue;\n const { sql, params } = buildRewriteStatement(\n engine,\n target,\n options.tenantId,\n reference.alias,\n reference.current,\n );\n await handle.query(sql, ...params);\n }\n };\n if (typeof db.transaction === 'function') {\n await db.transaction(async (tx) => rewrite(tx as DatabaseInterface));\n } else {\n await rewrite(db);\n }\n\n const after = await countLegacyQualifiedNameReferences(db, options);\n const remaining = new Map(\n after.references.map((reference) => [\n `${reference.table}\\u0000${reference.column}\\u0000${reference.alias}`,\n reference,\n ]),\n );\n for (const reference of before.references) {\n const left = remaining.get(\n `${reference.table}\\u0000${reference.column}\\u0000${reference.alias}`,\n );\n const rewrittenCount = reference.count - (left?.count ?? 0);\n if (rewrittenCount > 0) {\n result.rewritten.push({ ...reference, count: rewrittenCount });\n }\n }\n result.skippedDuplicates = after.references;\n result.ran = true;\n\n if (after.total === 0) {\n await tracker.recordApplied(backfillName, {\n description:\n 'Stored deprecated qualified names rewritten to current names: ' +\n pairs.map(({ alias, current }) => `${alias} -> ${current}`).join(', '),\n packageName: '@happyvertical/smrt-core',\n });\n result.recorded = true;\n }\n return result;\n}\n"],"mappings":"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"}
@@ -10,5 +10,6 @@ import { MinorUnitsPreflightError, buildMinorUnitsStatements, preflightMinorUnit
10
10
  import { collectNullEqualIndexTargets, migrateNullEqualIndexes, nullEqualDuplicateDetector, nullEqualIndexStatements, preflightNullEqualIndexes } from "./migrations/null-equal-indexes.js";
11
11
  import { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, planPostgresStatements } from "./migrations/tracker.js";
12
12
  import { getPendingSchemaStatements, migrateSmrtSchemas } from "./migrations/orchestrate.js";
13
+ import { LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, backfillLegacyQualifiedNames, collectLegacyQualifiedNameTargets, countLegacyQualifiedNameReferences, legacyQualifiedNamesBackfillName } from "./migrations/qualified-name-aliases.js";
13
14
  import "./migrations/index.js";
14
- 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 };
15
+ 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 };
@@ -80,6 +80,19 @@ export declare class SmrtPolymorphicAssociation extends SmrtObject {
80
80
  /** Sort order for ordering targets within a role. */
81
81
  sortOrder: number;
82
82
  constructor(options?: SmrtPolymorphicAssociationOptions);
83
+ /**
84
+ * Store `metaType` under its target's CURRENT qualified name before every
85
+ * save (#3338). A deprecated name declared in the target's
86
+ * `previousQualifiedNames` is rewritten first, so new rows — and any legacy
87
+ * row that is loaded and saved again — never perpetuate it. A target known
88
+ * only from an installed manifest is lazily loaded first; names that
89
+ * resolve to no class at all are stored as given.
90
+ *
91
+ * This runs in the first save-preparation hook rather than a `save()`
92
+ * override: a public override would be scanned into every subclass's
93
+ * manifest as a `save` method.
94
+ */
95
+ protected validateBeforeSave(): Promise<void>;
83
96
  /**
84
97
  * Resolve and load the target object this association points at.
85
98
  *
@@ -100,6 +113,11 @@ export declare class SmrtPolymorphicAssociation extends SmrtObject {
100
113
  * exists. The target shares this association's database connection via
101
114
  * `this.options`.
102
115
  *
116
+ * A `metaType` written under a class's former qualified name (declared in
117
+ * its `previousQualifiedNames`, e.g. after the class moved package)
118
+ * resolves to the class, eagerly or through the lazy manifest alias index,
119
+ * with a one-time deprecation warning (#3338).
120
+ *
103
121
  * Prefer qualified `metaType` values (`@pkg:Class`): a bare simple name
104
122
  * resolves to the first registered match, and an ambiguous unqualified name
105
123
  * that has to be lazily loaded surfaces a `ConfigurationError` rather than
@@ -1 +1 @@
1
- {"version":3,"file":"polymorphic-association.d.ts","sourceRoot":"","sources":["../src/polymorphic-association.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAGH,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG9D;;;GAGG;AACH,MAAM,WAAW,iCAAkC,SAAQ,iBAAiB;IAC1E,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,0BAA2B,SAAQ,UAAU;IACxD;;;;;;;;OAQG;IACH,MAAM,CAAC,QAAQ,CAAC,6BAA6B,EAAG,IAAI,CAAU;IAE9D,kFAAkF;IAElF,QAAQ,SAAM;IAEd;;;;OAIG;IAEH,MAAM,SAAM;IAEZ,+EAA+E;IAE/E,IAAI,SAAa;IAEjB,qDAAqD;IAErD,SAAS,EAAE,MAAM,CAAK;gBAEV,OAAO,GAAE,iCAAsC;IAQ3D;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACG,OAAO,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAqCtE"}
1
+ {"version":3,"file":"polymorphic-association.d.ts","sourceRoot":"","sources":["../src/polymorphic-association.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAGH,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG9D;;;GAGG;AACH,MAAM,WAAW,iCAAkC,SAAQ,iBAAiB;IAC1E,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,0BAA2B,SAAQ,UAAU;IACxD;;;;;;;;OAQG;IACH,MAAM,CAAC,QAAQ,CAAC,6BAA6B,EAAG,IAAI,CAAU;IAE9D,kFAAkF;IAElF,QAAQ,SAAM;IAEd;;;;OAIG;IAEH,MAAM,SAAM;IAEZ,+EAA+E;IAE/E,IAAI,SAAa;IAEjB,qDAAqD;IAErD,SAAS,EAAE,MAAM,CAAK;gBAEV,OAAO,GAAE,iCAAsC;IAQ3D;;;;;;;;;;;OAWG;cACsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAa5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,OAAO,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAsCtE"}
@@ -78,6 +78,25 @@ var SmrtPolymorphicAssociation = class extends SmrtObject {
78
78
  if (options.sortOrder !== void 0) this.sortOrder = options.sortOrder;
79
79
  }
80
80
  /**
81
+ * Store `metaType` under its target's CURRENT qualified name before every
82
+ * save (#3338). A deprecated name declared in the target's
83
+ * `previousQualifiedNames` is rewritten first, so new rows — and any legacy
84
+ * row that is loaded and saved again — never perpetuate it. A target known
85
+ * only from an installed manifest is lazily loaded first; names that
86
+ * resolve to no class at all are stored as given.
87
+ *
88
+ * This runs in the first save-preparation hook rather than a `save()`
89
+ * override: a public override would be scanned into every subclass's
90
+ * manifest as a `save` method.
91
+ */
92
+ async validateBeforeSave() {
93
+ if (this.metaType) {
94
+ const current = await ObjectRegistry.resolveQualifiedNameAsync(this.metaType, { source: "SmrtPolymorphicAssociation.save" });
95
+ if (current && current !== this.metaType) this.metaType = current;
96
+ }
97
+ await super.validateBeforeSave();
98
+ }
99
+ /**
81
100
  * Resolve and load the target object this association points at.
82
101
  *
83
102
  * Resolves `metaType` to its registered class via
@@ -97,6 +116,11 @@ var SmrtPolymorphicAssociation = class extends SmrtObject {
97
116
  * exists. The target shares this association's database connection via
98
117
  * `this.options`.
99
118
  *
119
+ * A `metaType` written under a class's former qualified name (declared in
120
+ * its `previousQualifiedNames`, e.g. after the class moved package)
121
+ * resolves to the class, eagerly or through the lazy manifest alias index,
122
+ * with a one-time deprecation warning (#3338).
123
+ *
100
124
  * Prefer qualified `metaType` values (`@pkg:Class`): a bare simple name
101
125
  * resolves to the first registered match, and an ambiguous unqualified name
102
126
  * that has to be lazily loaded surfaces a `ConfigurationError` rather than
@@ -106,10 +130,11 @@ var SmrtPolymorphicAssociation = class extends SmrtObject {
106
130
  */
107
131
  async hydrate() {
108
132
  if (!this.metaType || !this.metaId) return null;
109
- let registered = ObjectRegistry.getClassByQualifiedName(this.metaType) ?? ObjectRegistry.getClass(this.metaType);
133
+ const lookup = { source: "SmrtPolymorphicAssociation.hydrate" };
134
+ let registered = ObjectRegistry.getClassByQualifiedName(this.metaType, lookup) ?? ObjectRegistry.getClass(this.metaType);
110
135
  if (!registered) {
111
136
  if (!await ObjectRegistry.tryLoadFromExternalPackage(this.metaType)) return null;
112
- registered = ObjectRegistry.getClassByQualifiedName(this.metaType) ?? ObjectRegistry.getClass(this.metaType);
137
+ registered = ObjectRegistry.getClassByQualifiedName(this.metaType, lookup) ?? ObjectRegistry.getClass(this.metaType);
113
138
  }
114
139
  if (!registered) return null;
115
140
  return await (await ObjectRegistry.getCollection(registered.qualifiedName ?? registered.name, this.options)).get({ id: this.metaId });