@happyvertical/smrt-core 0.51.5 → 0.51.7

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.
@@ -35,4 +35,75 @@ export declare function columnsHaveNonEmptyValueBatch(db: DatabaseInterface, tab
35
35
  * `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check).
36
36
  */
37
37
  export declare function columnsAllValuesUuidShapedBatch(db: DatabaseInterface, engine: DatabaseEngine, table: string, columns: string[]): Promise<Map<string, boolean>>;
38
+ /**
39
+ * One rename-data-pending candidate pairing: `targetName` (a declared
40
+ * column confirmed empty) might have had its data left behind in
41
+ * `sourceName` (an undeclared, populated, type-compatible column).
42
+ * `requiresShapeCheck` marks a UUID-cast pairing whose source still needs
43
+ * {@link columnsAllValuesUuidShapedBatch} before it can be trusted.
44
+ * `extra` carries whatever caller-specific payload (e.g. differ.ts's
45
+ * `isUuidCast` repair-SQL flag) needs to travel with a surviving candidate.
46
+ */
47
+ export interface RenameDataPendingCandidate<Extra = undefined> {
48
+ targetName: string;
49
+ sourceName: string;
50
+ requiresShapeCheck: boolean;
51
+ extra: Extra;
52
+ }
53
+ /**
54
+ * Group rename-data-pending candidates by declared target column and
55
+ * resolve inference ambiguity (#2911: false positives that both blocked
56
+ * production deploys through `db:status:assert` and, worse, recommended
57
+ * copying data into the wrong column). Shared by every
58
+ * `detectRenameDataPending()` copy — `migrations/differ.ts`'s single-table
59
+ * and cross-table-batched variants, and `schema/live-parity.ts`'s — so this
60
+ * inference rule can never drift between them the way the #2874 regression
61
+ * drifted before column-data-probes.ts existed (#2878).
62
+ *
63
+ * "Ambiguity must suppress a finding, never multiply it" (#2911) covers two
64
+ * independent shapes:
65
+ *
66
+ * - **Multiple sources, one target.** More than one undeclared column is a
67
+ * compatible, populated candidate for the *same* target: which one is
68
+ * the real rename origin cannot be inferred. This was already handled
69
+ * pre-#2911 by emitting an ambiguous, no-suggested-SQL advisory instead
70
+ * of guessing — preserved here as a returned list with more than one
71
+ * entry. Two targets that each independently have several qualifying
72
+ * sources — even the *same* several sources — each still get their own
73
+ * ambiguous finding: every individual finding already discloses that it
74
+ * could not pick a source, so this shape does not need the extra
75
+ * cross-target check below.
76
+ * - **One source, multiple targets (#2911's actual bug).** A source column
77
+ * that would otherwise be the single, *unambiguous* match for a target
78
+ * is also a candidate — sole or one of several — for one or more
79
+ * *other* targets too — e.g. `tenants.timezone` alone nominated as the
80
+ * rename source for `hierarchy_path`, `repo_template`, and `github_org`
81
+ * simultaneously. A single column cannot be the renamed predecessor of
82
+ * three unrelated columns at once, so a source this heuristic was about
83
+ * to trust as one target's sole (and therefore actionable,
84
+ * `suggestedSql`-bearing) candidate is disqualified the moment *any*
85
+ * other target's candidate list — sole or ambiguous — also names it:
86
+ * printing a confident copy-then-`DROP COLUMN` for one target while
87
+ * another target's own advisory names that same column as a possible
88
+ * source is exactly the data-corrupting guess #2911 exists to prevent,
89
+ * even though the second target's own ambiguous finding is already
90
+ * safe (no `suggestedSql`) on its own. Only a target that would
91
+ * otherwise resolve to exactly one candidate can be suppressed this
92
+ * way — an already-ambiguous target's own finding is never dropped,
93
+ * only consulted as evidence that a *different* target's sole source is
94
+ * contested; two targets that merely happen to share the same pair of
95
+ * ambiguous candidates (a plausible, unrelated coincidence — see the
96
+ * batched-probe-fallback regression in
97
+ * `migrations/__tests__/differ.test.ts`) keep both of their
98
+ * already-safe ambiguous findings untouched.
99
+ *
100
+ * A target absent from the returned map produced no surviving, trustworthy
101
+ * candidate — emit nothing for it. A target present with exactly one
102
+ * candidate is an unambiguous match; more than one is the first
103
+ * (multiple-sources) ambiguity shape above.
104
+ */
105
+ export declare function resolveRenameDataPendingCandidates<Extra = undefined>(candidates: RenameDataPendingCandidate<Extra>[], isShaped: (sourceName: string) => boolean): Map<string, {
106
+ sourceName: string;
107
+ extra: Extra;
108
+ }[]>;
38
109
  //# sourceMappingURL=column-data-probes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"column-data-probes.d.ts","sourceRoot":"","sources":["../../src/schema/column-data-probes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAOrD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,MAAM,GACnB,MAAM,CAKR;AAED;;;;;;GAMG;AACH,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAkB/B;AA6CD;;;;;;;;;;;GAWG;AACH,wBAAsB,+BAA+B,CACnD,EAAE,EAAE,iBAAiB,EACrB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAuB/B"}
1
+ {"version":3,"file":"column-data-probes.d.ts","sourceRoot":"","sources":["../../src/schema/column-data-probes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAOrD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,MAAM,GACnB,MAAM,CAKR;AAED;;;;;;GAMG;AACH,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAkB/B;AA6CD;;;;;;;;;;;GAWG;AACH,wBAAsB,+BAA+B,CACnD,EAAE,EAAE,iBAAiB,EACrB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAuB/B;AA4CD;;;;;;;;GAQG;AACH,MAAM,WAAW,0BAA0B,CAAC,KAAK,GAAG,SAAS;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,KAAK,EAAE,KAAK,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,wBAAgB,kCAAkC,CAAC,KAAK,GAAG,SAAS,EAClE,UAAU,EAAE,0BAA0B,CAAC,KAAK,CAAC,EAAE,EAC/C,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,GACxC,GAAG,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,EAAE,CAAC,CAsCrD"}
@@ -100,7 +100,85 @@ async function allNonEmptyValuesUuidShapedSingle(db, engine, table, column) {
100
100
  const quotedColumn = quoteIdentifier(column);
101
101
  return ((await db.query(`SELECT 1 AS invalid FROM ${quotedTable} WHERE ${nonEmptyValuePredicate(quotedColumn)} AND ${uuidInvalidShapePredicate(engine, quotedColumn)} LIMIT 1`))?.rows?.length ?? 0) === 0;
102
102
  }
103
+ /**
104
+ * Group rename-data-pending candidates by declared target column and
105
+ * resolve inference ambiguity (#2911: false positives that both blocked
106
+ * production deploys through `db:status:assert` and, worse, recommended
107
+ * copying data into the wrong column). Shared by every
108
+ * `detectRenameDataPending()` copy — `migrations/differ.ts`'s single-table
109
+ * and cross-table-batched variants, and `schema/live-parity.ts`'s — so this
110
+ * inference rule can never drift between them the way the #2874 regression
111
+ * drifted before column-data-probes.ts existed (#2878).
112
+ *
113
+ * "Ambiguity must suppress a finding, never multiply it" (#2911) covers two
114
+ * independent shapes:
115
+ *
116
+ * - **Multiple sources, one target.** More than one undeclared column is a
117
+ * compatible, populated candidate for the *same* target: which one is
118
+ * the real rename origin cannot be inferred. This was already handled
119
+ * pre-#2911 by emitting an ambiguous, no-suggested-SQL advisory instead
120
+ * of guessing — preserved here as a returned list with more than one
121
+ * entry. Two targets that each independently have several qualifying
122
+ * sources — even the *same* several sources — each still get their own
123
+ * ambiguous finding: every individual finding already discloses that it
124
+ * could not pick a source, so this shape does not need the extra
125
+ * cross-target check below.
126
+ * - **One source, multiple targets (#2911's actual bug).** A source column
127
+ * that would otherwise be the single, *unambiguous* match for a target
128
+ * is also a candidate — sole or one of several — for one or more
129
+ * *other* targets too — e.g. `tenants.timezone` alone nominated as the
130
+ * rename source for `hierarchy_path`, `repo_template`, and `github_org`
131
+ * simultaneously. A single column cannot be the renamed predecessor of
132
+ * three unrelated columns at once, so a source this heuristic was about
133
+ * to trust as one target's sole (and therefore actionable,
134
+ * `suggestedSql`-bearing) candidate is disqualified the moment *any*
135
+ * other target's candidate list — sole or ambiguous — also names it:
136
+ * printing a confident copy-then-`DROP COLUMN` for one target while
137
+ * another target's own advisory names that same column as a possible
138
+ * source is exactly the data-corrupting guess #2911 exists to prevent,
139
+ * even though the second target's own ambiguous finding is already
140
+ * safe (no `suggestedSql`) on its own. Only a target that would
141
+ * otherwise resolve to exactly one candidate can be suppressed this
142
+ * way — an already-ambiguous target's own finding is never dropped,
143
+ * only consulted as evidence that a *different* target's sole source is
144
+ * contested; two targets that merely happen to share the same pair of
145
+ * ambiguous candidates (a plausible, unrelated coincidence — see the
146
+ * batched-probe-fallback regression in
147
+ * `migrations/__tests__/differ.test.ts`) keep both of their
148
+ * already-safe ambiguous findings untouched.
149
+ *
150
+ * A target absent from the returned map produced no surviving, trustworthy
151
+ * candidate — emit nothing for it. A target present with exactly one
152
+ * candidate is an unambiguous match; more than one is the first
153
+ * (multiple-sources) ambiguity shape above.
154
+ */
155
+ function resolveRenameDataPendingCandidates(candidates, isShaped) {
156
+ const survivors = candidates.filter((candidate) => !candidate.requiresShapeCheck || isShaped(candidate.sourceName));
157
+ const byTarget = /* @__PURE__ */ new Map();
158
+ for (const candidate of survivors) {
159
+ const list = byTarget.get(candidate.targetName) ?? [];
160
+ list.push({
161
+ sourceName: candidate.sourceName,
162
+ extra: candidate.extra
163
+ });
164
+ byTarget.set(candidate.targetName, list);
165
+ }
166
+ const targetsBySource = /* @__PURE__ */ new Map();
167
+ for (const [targetName, list] of byTarget) for (const { sourceName } of list) {
168
+ const set = targetsBySource.get(sourceName) ?? /* @__PURE__ */ new Set();
169
+ set.add(targetName);
170
+ targetsBySource.set(sourceName, set);
171
+ }
172
+ const resolved = /* @__PURE__ */ new Map();
173
+ for (const [targetName, list] of byTarget) {
174
+ if (list.length === 1) {
175
+ if ((targetsBySource.get(list[0].sourceName)?.size ?? 0) > 1) continue;
176
+ }
177
+ resolved.set(targetName, list);
178
+ }
179
+ return resolved;
180
+ }
103
181
  //#endregion
104
- export { columnsAllValuesUuidShapedBatch, columnsHaveNonEmptyValueBatch, nonEmptyValuePredicate, uuidInvalidShapePredicate };
182
+ export { columnsAllValuesUuidShapedBatch, columnsHaveNonEmptyValueBatch, nonEmptyValuePredicate, resolveRenameDataPendingCandidates, uuidInvalidShapePredicate };
105
183
 
106
184
  //# sourceMappingURL=column-data-probes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"column-data-probes.js","names":[],"sources":["../../src/schema/column-data-probes.ts"],"sourcesContent":["/**\n * Shared live-data column probes (#2874, consolidated for #2878).\n *\n * `detectRenameDataPending()` exists in both `migrations/differ.ts` and\n * `schema/live-parity.ts` — tracked as its own maintenance hazard by #2878,\n * since the #2874 regression had to be fixed twice, in lockstep, in #2876\n * (once per copy). Until now the two batched live-data probes that function\n * depends on were duplicated right along with it: does a named column hold\n * any non-empty value, and does every non-empty value in a named column\n * look UUID-shaped? Both copies used the exact same shape —\n * uncorrelated scalar subqueries, one per column, each with its own\n * `LIMIT 1` early exit (#2874 review finding F1: never an aggregate over\n * the whole table, which would force a full scan per probed column even\n * when the very first row already answers it), and the same positional\n * `c<index>` aliasing to sidestep PostgreSQL's 63-byte identifier\n * truncation (#2874 review finding F2'). Extracted here so the two call\n * sites can never drift out of lockstep again.\n *\n * Both functions fall back to isolated per-column probing when the batched\n * statement itself fails, so one bad column (dropped concurrently, a `CAST`\n * the engine rejects) withholds only that column's result rather than the\n * whole table's (#2874 review finding F2). A column absent from the\n * returned map means \"could not be probed\" — callers apply their own\n * fail-closed default, not this module.\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport {\n CANONICAL_UUID_PATTERN,\n CANONICAL_UUID_SQLITE_GLOB_PATTERN,\n} from './foreign-key-ddl.js';\nimport { quoteIdentifier } from './sql-identifiers.js';\n\n/**\n * SQL predicate: a quoted column holds a non-null, non-empty value. Shared\n * by every probe below (single-table batch, single-column fallback, and\n * `migrations/differ.ts`'s cross-table batch, #2878) so the emptiness rule\n * can never drift between them the way #2874's regression drifted between\n * `differ.ts` and `live-parity.ts` before this module existed.\n */\nexport function nonEmptyValuePredicate(quotedColumn: string): string {\n return `${quotedColumn} IS NOT NULL AND CAST(${quotedColumn} AS TEXT) <> ''`;\n}\n\n/**\n * SQL predicate: a quoted column's value does *not* look UUID-shaped\n * ({@link CANONICAL_UUID_PATTERN}), engine-aware (PostgreSQL regex vs.\n * SQLite `GLOB`). Shared for the same reason as {@link nonEmptyValuePredicate}.\n */\nexport function uuidInvalidShapePredicate(\n engine: DatabaseEngine,\n quotedColumn: string,\n): string {\n return engine === 'postgres'\n ? `CAST(${quotedColumn} AS TEXT) !~* '${CANONICAL_UUID_PATTERN}'`\n : `NOT (LENGTH(CAST(${quotedColumn} AS TEXT)) = 36 ` +\n `AND LOWER(CAST(${quotedColumn} AS TEXT)) GLOB '${CANONICAL_UUID_SQLITE_GLOB_PATTERN}')`;\n}\n\n/**\n * Live-data probe, batched across every column named: does each hold any\n * non-null, non-empty value? One round trip regardless of column count,\n * one row of uncorrelated scalar subqueries,\n * `(SELECT 1 FROM t WHERE ... LIMIT 1) AS c<N>`, portable across PostgreSQL\n * and SQLite.\n */\nexport async function columnsHaveNonEmptyValueBatch(\n db: DatabaseInterface,\n table: string,\n columns: string[],\n): Promise<Map<string, boolean>> {\n if (columns.length === 0) return new Map();\n try {\n return await columnsHaveNonEmptyValueBatchQuery(db, table, columns);\n } catch {\n const hasData = new Map<string, boolean>();\n for (const column of columns) {\n try {\n hasData.set(\n column,\n await columnHasNonEmptyValueSingle(db, table, column),\n );\n } catch {\n // Left absent: the caller's own default applies (#2874 review F2).\n }\n }\n return hasData;\n }\n}\n\nasync function columnsHaveNonEmptyValueBatchQuery(\n db: DatabaseInterface,\n table: string,\n columns: string[],\n): Promise<Map<string, boolean>> {\n const quotedTable = quoteIdentifier(table);\n // Positional aliases (`c0`, `c1`, …), not the column name (#2874 review\n // finding F2'): PostgreSQL silently truncates a `name` identifier —\n // including a quoted alias — to 63 bytes, so a long column name, or two\n // columns sharing their first 63 bytes, would collide on the same output\n // key and mis-key a result. Positional aliases are immune to identifier\n // length and never collide with each other.\n const selects = columns.map((column, index) => {\n const quotedColumn = quoteIdentifier(column);\n return (\n `(SELECT 1 FROM ${quotedTable} WHERE ${nonEmptyValuePredicate(quotedColumn)} ` +\n `LIMIT 1) AS c${index}`\n );\n });\n const result = await db.query(`SELECT ${selects.join(', ')}`);\n const row = (result?.rows?.[0] ?? {}) as Record<string, unknown>;\n const hasData = new Map<string, boolean>();\n columns.forEach((column, index) => {\n hasData.set(column, row[`c${index}`] != null);\n });\n return hasData;\n}\n\n/** Single-column fallback for {@link columnsHaveNonEmptyValueBatch}. */\nasync function columnHasNonEmptyValueSingle(\n db: DatabaseInterface,\n table: string,\n column: string,\n): Promise<boolean> {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n const result = await db.query(\n `SELECT 1 AS present FROM ${quotedTable} ` +\n `WHERE ${nonEmptyValuePredicate(quotedColumn)} LIMIT 1`,\n );\n return (result?.rows?.length ?? 0) > 0;\n}\n\n/**\n * Live-data probe, batched across every column named: are all of a\n * column's non-empty values UUID-shaped ({@link CANONICAL_UUID_PATTERN})?\n * One round trip regardless of column count, mirroring\n * {@link columnsHaveNonEmptyValueBatch}: a value is absent from the result\n * exactly when no invalid row exists, so this also short-circuits on the\n * first invalid row rather than counting every one. PostgreSQL pushes the\n * shape check into its regex operator; SQLite has no regex operator, but\n * its case-sensitive `GLOB` can still express the fixed 36-character\n * canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN} against\n * `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check).\n */\nexport async function columnsAllValuesUuidShapedBatch(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n table: string,\n columns: string[],\n): Promise<Map<string, boolean>> {\n if (columns.length === 0) return new Map();\n try {\n return await columnsAllValuesUuidShapedBatchQuery(\n db,\n engine,\n table,\n columns,\n );\n } catch {\n const shaped = new Map<string, boolean>();\n for (const column of columns) {\n try {\n shaped.set(\n column,\n await allNonEmptyValuesUuidShapedSingle(db, engine, table, column),\n );\n } catch {\n // Left absent: the caller's own default applies (#2874 review F2).\n }\n }\n return shaped;\n }\n}\n\nasync function columnsAllValuesUuidShapedBatchQuery(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n table: string,\n columns: string[],\n): Promise<Map<string, boolean>> {\n const quotedTable = quoteIdentifier(table);\n // Positional aliases, not the column name (#2874 review finding F2') —\n // see {@link columnsHaveNonEmptyValueBatchQuery}.\n const selects = columns.map((column, index) => {\n const quotedColumn = quoteIdentifier(column);\n return (\n `(SELECT 1 FROM ${quotedTable} WHERE ${nonEmptyValuePredicate(quotedColumn)} ` +\n `AND ${uuidInvalidShapePredicate(engine, quotedColumn)} LIMIT 1) AS c${index}`\n );\n });\n const result = await db.query(`SELECT ${selects.join(', ')}`);\n const row = (result?.rows?.[0] ?? {}) as Record<string, unknown>;\n const shaped = new Map<string, boolean>();\n columns.forEach((column, index) => {\n shaped.set(column, row[`c${index}`] == null);\n });\n return shaped;\n}\n\n/** Single-column fallback for {@link columnsAllValuesUuidShapedBatch}. */\nasync function allNonEmptyValuesUuidShapedSingle(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n table: string,\n column: string,\n): Promise<boolean> {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n const result = await db.query(\n `SELECT 1 AS invalid FROM ${quotedTable} ` +\n `WHERE ${nonEmptyValuePredicate(quotedColumn)} ` +\n `AND ${uuidInvalidShapePredicate(engine, quotedColumn)} LIMIT 1`,\n );\n return (result?.rows?.length ?? 0) === 0;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"column-data-probes.js","names":[],"sources":["../../src/schema/column-data-probes.ts"],"sourcesContent":["/**\n * Shared live-data column probes (#2874, consolidated for #2878).\n *\n * `detectRenameDataPending()` exists in both `migrations/differ.ts` and\n * `schema/live-parity.ts` — tracked as its own maintenance hazard by #2878,\n * since the #2874 regression had to be fixed twice, in lockstep, in #2876\n * (once per copy). Until now the two batched live-data probes that function\n * depends on were duplicated right along with it: does a named column hold\n * any non-empty value, and does every non-empty value in a named column\n * look UUID-shaped? Both copies used the exact same shape —\n * uncorrelated scalar subqueries, one per column, each with its own\n * `LIMIT 1` early exit (#2874 review finding F1: never an aggregate over\n * the whole table, which would force a full scan per probed column even\n * when the very first row already answers it), and the same positional\n * `c<index>` aliasing to sidestep PostgreSQL's 63-byte identifier\n * truncation (#2874 review finding F2'). Extracted here so the two call\n * sites can never drift out of lockstep again.\n *\n * Both functions fall back to isolated per-column probing when the batched\n * statement itself fails, so one bad column (dropped concurrently, a `CAST`\n * the engine rejects) withholds only that column's result rather than the\n * whole table's (#2874 review finding F2). A column absent from the\n * returned map means \"could not be probed\" — callers apply their own\n * fail-closed default, not this module.\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport {\n CANONICAL_UUID_PATTERN,\n CANONICAL_UUID_SQLITE_GLOB_PATTERN,\n} from './foreign-key-ddl.js';\nimport { quoteIdentifier } from './sql-identifiers.js';\n\n/**\n * SQL predicate: a quoted column holds a non-null, non-empty value. Shared\n * by every probe below (single-table batch, single-column fallback, and\n * `migrations/differ.ts`'s cross-table batch, #2878) so the emptiness rule\n * can never drift between them the way #2874's regression drifted between\n * `differ.ts` and `live-parity.ts` before this module existed.\n */\nexport function nonEmptyValuePredicate(quotedColumn: string): string {\n return `${quotedColumn} IS NOT NULL AND CAST(${quotedColumn} AS TEXT) <> ''`;\n}\n\n/**\n * SQL predicate: a quoted column's value does *not* look UUID-shaped\n * ({@link CANONICAL_UUID_PATTERN}), engine-aware (PostgreSQL regex vs.\n * SQLite `GLOB`). Shared for the same reason as {@link nonEmptyValuePredicate}.\n */\nexport function uuidInvalidShapePredicate(\n engine: DatabaseEngine,\n quotedColumn: string,\n): string {\n return engine === 'postgres'\n ? `CAST(${quotedColumn} AS TEXT) !~* '${CANONICAL_UUID_PATTERN}'`\n : `NOT (LENGTH(CAST(${quotedColumn} AS TEXT)) = 36 ` +\n `AND LOWER(CAST(${quotedColumn} AS TEXT)) GLOB '${CANONICAL_UUID_SQLITE_GLOB_PATTERN}')`;\n}\n\n/**\n * Live-data probe, batched across every column named: does each hold any\n * non-null, non-empty value? One round trip regardless of column count,\n * one row of uncorrelated scalar subqueries,\n * `(SELECT 1 FROM t WHERE ... LIMIT 1) AS c<N>`, portable across PostgreSQL\n * and SQLite.\n */\nexport async function columnsHaveNonEmptyValueBatch(\n db: DatabaseInterface,\n table: string,\n columns: string[],\n): Promise<Map<string, boolean>> {\n if (columns.length === 0) return new Map();\n try {\n return await columnsHaveNonEmptyValueBatchQuery(db, table, columns);\n } catch {\n const hasData = new Map<string, boolean>();\n for (const column of columns) {\n try {\n hasData.set(\n column,\n await columnHasNonEmptyValueSingle(db, table, column),\n );\n } catch {\n // Left absent: the caller's own default applies (#2874 review F2).\n }\n }\n return hasData;\n }\n}\n\nasync function columnsHaveNonEmptyValueBatchQuery(\n db: DatabaseInterface,\n table: string,\n columns: string[],\n): Promise<Map<string, boolean>> {\n const quotedTable = quoteIdentifier(table);\n // Positional aliases (`c0`, `c1`, …), not the column name (#2874 review\n // finding F2'): PostgreSQL silently truncates a `name` identifier —\n // including a quoted alias — to 63 bytes, so a long column name, or two\n // columns sharing their first 63 bytes, would collide on the same output\n // key and mis-key a result. Positional aliases are immune to identifier\n // length and never collide with each other.\n const selects = columns.map((column, index) => {\n const quotedColumn = quoteIdentifier(column);\n return (\n `(SELECT 1 FROM ${quotedTable} WHERE ${nonEmptyValuePredicate(quotedColumn)} ` +\n `LIMIT 1) AS c${index}`\n );\n });\n const result = await db.query(`SELECT ${selects.join(', ')}`);\n const row = (result?.rows?.[0] ?? {}) as Record<string, unknown>;\n const hasData = new Map<string, boolean>();\n columns.forEach((column, index) => {\n hasData.set(column, row[`c${index}`] != null);\n });\n return hasData;\n}\n\n/** Single-column fallback for {@link columnsHaveNonEmptyValueBatch}. */\nasync function columnHasNonEmptyValueSingle(\n db: DatabaseInterface,\n table: string,\n column: string,\n): Promise<boolean> {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n const result = await db.query(\n `SELECT 1 AS present FROM ${quotedTable} ` +\n `WHERE ${nonEmptyValuePredicate(quotedColumn)} LIMIT 1`,\n );\n return (result?.rows?.length ?? 0) > 0;\n}\n\n/**\n * Live-data probe, batched across every column named: are all of a\n * column's non-empty values UUID-shaped ({@link CANONICAL_UUID_PATTERN})?\n * One round trip regardless of column count, mirroring\n * {@link columnsHaveNonEmptyValueBatch}: a value is absent from the result\n * exactly when no invalid row exists, so this also short-circuits on the\n * first invalid row rather than counting every one. PostgreSQL pushes the\n * shape check into its regex operator; SQLite has no regex operator, but\n * its case-sensitive `GLOB` can still express the fixed 36-character\n * canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN} against\n * `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check).\n */\nexport async function columnsAllValuesUuidShapedBatch(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n table: string,\n columns: string[],\n): Promise<Map<string, boolean>> {\n if (columns.length === 0) return new Map();\n try {\n return await columnsAllValuesUuidShapedBatchQuery(\n db,\n engine,\n table,\n columns,\n );\n } catch {\n const shaped = new Map<string, boolean>();\n for (const column of columns) {\n try {\n shaped.set(\n column,\n await allNonEmptyValuesUuidShapedSingle(db, engine, table, column),\n );\n } catch {\n // Left absent: the caller's own default applies (#2874 review F2).\n }\n }\n return shaped;\n }\n}\n\nasync function columnsAllValuesUuidShapedBatchQuery(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n table: string,\n columns: string[],\n): Promise<Map<string, boolean>> {\n const quotedTable = quoteIdentifier(table);\n // Positional aliases, not the column name (#2874 review finding F2') —\n // see {@link columnsHaveNonEmptyValueBatchQuery}.\n const selects = columns.map((column, index) => {\n const quotedColumn = quoteIdentifier(column);\n return (\n `(SELECT 1 FROM ${quotedTable} WHERE ${nonEmptyValuePredicate(quotedColumn)} ` +\n `AND ${uuidInvalidShapePredicate(engine, quotedColumn)} LIMIT 1) AS c${index}`\n );\n });\n const result = await db.query(`SELECT ${selects.join(', ')}`);\n const row = (result?.rows?.[0] ?? {}) as Record<string, unknown>;\n const shaped = new Map<string, boolean>();\n columns.forEach((column, index) => {\n shaped.set(column, row[`c${index}`] == null);\n });\n return shaped;\n}\n\n/** Single-column fallback for {@link columnsAllValuesUuidShapedBatch}. */\nasync function allNonEmptyValuesUuidShapedSingle(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n table: string,\n column: string,\n): Promise<boolean> {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n const result = await db.query(\n `SELECT 1 AS invalid FROM ${quotedTable} ` +\n `WHERE ${nonEmptyValuePredicate(quotedColumn)} ` +\n `AND ${uuidInvalidShapePredicate(engine, quotedColumn)} LIMIT 1`,\n );\n return (result?.rows?.length ?? 0) === 0;\n}\n\n/**\n * One rename-data-pending candidate pairing: `targetName` (a declared\n * column confirmed empty) might have had its data left behind in\n * `sourceName` (an undeclared, populated, type-compatible column).\n * `requiresShapeCheck` marks a UUID-cast pairing whose source still needs\n * {@link columnsAllValuesUuidShapedBatch} before it can be trusted.\n * `extra` carries whatever caller-specific payload (e.g. differ.ts's\n * `isUuidCast` repair-SQL flag) needs to travel with a surviving candidate.\n */\nexport interface RenameDataPendingCandidate<Extra = undefined> {\n targetName: string;\n sourceName: string;\n requiresShapeCheck: boolean;\n extra: Extra;\n}\n\n/**\n * Group rename-data-pending candidates by declared target column and\n * resolve inference ambiguity (#2911: false positives that both blocked\n * production deploys through `db:status:assert` and, worse, recommended\n * copying data into the wrong column). Shared by every\n * `detectRenameDataPending()` copy — `migrations/differ.ts`'s single-table\n * and cross-table-batched variants, and `schema/live-parity.ts`'s — so this\n * inference rule can never drift between them the way the #2874 regression\n * drifted before column-data-probes.ts existed (#2878).\n *\n * \"Ambiguity must suppress a finding, never multiply it\" (#2911) covers two\n * independent shapes:\n *\n * - **Multiple sources, one target.** More than one undeclared column is a\n * compatible, populated candidate for the *same* target: which one is\n * the real rename origin cannot be inferred. This was already handled\n * pre-#2911 by emitting an ambiguous, no-suggested-SQL advisory instead\n * of guessing — preserved here as a returned list with more than one\n * entry. Two targets that each independently have several qualifying\n * sources — even the *same* several sources — each still get their own\n * ambiguous finding: every individual finding already discloses that it\n * could not pick a source, so this shape does not need the extra\n * cross-target check below.\n * - **One source, multiple targets (#2911's actual bug).** A source column\n * that would otherwise be the single, *unambiguous* match for a target\n * is also a candidate — sole or one of several — for one or more\n * *other* targets too — e.g. `tenants.timezone` alone nominated as the\n * rename source for `hierarchy_path`, `repo_template`, and `github_org`\n * simultaneously. A single column cannot be the renamed predecessor of\n * three unrelated columns at once, so a source this heuristic was about\n * to trust as one target's sole (and therefore actionable,\n * `suggestedSql`-bearing) candidate is disqualified the moment *any*\n * other target's candidate list — sole or ambiguous — also names it:\n * printing a confident copy-then-`DROP COLUMN` for one target while\n * another target's own advisory names that same column as a possible\n * source is exactly the data-corrupting guess #2911 exists to prevent,\n * even though the second target's own ambiguous finding is already\n * safe (no `suggestedSql`) on its own. Only a target that would\n * otherwise resolve to exactly one candidate can be suppressed this\n * way — an already-ambiguous target's own finding is never dropped,\n * only consulted as evidence that a *different* target's sole source is\n * contested; two targets that merely happen to share the same pair of\n * ambiguous candidates (a plausible, unrelated coincidence — see the\n * batched-probe-fallback regression in\n * `migrations/__tests__/differ.test.ts`) keep both of their\n * already-safe ambiguous findings untouched.\n *\n * A target absent from the returned map produced no surviving, trustworthy\n * candidate — emit nothing for it. A target present with exactly one\n * candidate is an unambiguous match; more than one is the first\n * (multiple-sources) ambiguity shape above.\n */\nexport function resolveRenameDataPendingCandidates<Extra = undefined>(\n candidates: RenameDataPendingCandidate<Extra>[],\n isShaped: (sourceName: string) => boolean,\n): Map<string, { sourceName: string; extra: Extra }[]> {\n const survivors = candidates.filter(\n (candidate) =>\n !candidate.requiresShapeCheck || isShaped(candidate.sourceName),\n );\n\n const byTarget = new Map<string, { sourceName: string; extra: Extra }[]>();\n for (const candidate of survivors) {\n const list = byTarget.get(candidate.targetName) ?? [];\n list.push({ sourceName: candidate.sourceName, extra: candidate.extra });\n byTarget.set(candidate.targetName, list);\n }\n\n // One source, multiple targets (#2911): for every surviving source, how\n // many distinct targets does it appear as a candidate for at all —\n // whether it is that target's sole candidate or one of several. A sole\n // candidate is only trustworthy (and only then actionable, carrying\n // `suggestedSql`) when no other target's candidate list, ambiguous or\n // not, also names it.\n const targetsBySource = new Map<string, Set<string>>();\n for (const [targetName, list] of byTarget) {\n for (const { sourceName } of list) {\n const set = targetsBySource.get(sourceName) ?? new Set<string>();\n set.add(targetName);\n targetsBySource.set(sourceName, set);\n }\n }\n\n const resolved = new Map<string, { sourceName: string; extra: Extra }[]>();\n for (const [targetName, list] of byTarget) {\n if (list.length === 1) {\n const isContestedSoleSource =\n (targetsBySource.get(list[0].sourceName)?.size ?? 0) > 1;\n if (isContestedSoleSource) continue;\n }\n resolved.set(targetName, list);\n }\n return resolved;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"live-parity.d.ts","sourceRoot":"","sources":["../../src/schema/live-parity.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"live-parity.d.ts","sourceRoot":"","sources":["../../src/schema/live-parity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAgB5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,OAAO,KAAK,EAAoB,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE,iDAAiD;AACjD,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE9D,8CAA8C;AAC9C,MAAM,MAAM,qBAAqB,GAC7B,eAAe,GACf,aAAa,GACb,gBAAgB,GAChB,cAAc,GACd,mBAAmB,GACnB,0BAA0B,GAC1B,eAAe,GACf,aAAa,GACb,qBAAqB,GACrB,2BAA2B,GAC3B,4BAA4B,GAC5B,2BAA2B,GAC3B,eAAe;AACjB,uEAAuE;GACrE,sBAAsB;AACxB;;;;;GAKG;GACD,qBAAqB,CAAC;AAE1B,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,MAAM,EAAE,mBAAmB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,0CAA0C;AAC1C,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,cAAc,CAAC;IACvB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,oBAAoB,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,GAAG,aAAa,CAAC;IAC3C,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;IAC3C,0DAA0D;IAC1D,EAAE,EAAE,OAAO,CAAC;CACb;AAED,uDAAuD;AACvD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,iBAAiB,CAAC;IACtB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC3C,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAC;IACxD,sDAAsD;IACtD,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,2EAA2E;AAC3E,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,QAAQ,CAAC;AA+C3D;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI3D;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAuIjC;AAkYD,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsCrD;AA0lBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CA4C/D"}
@@ -1,7 +1,7 @@
1
1
  import { detectEngine, getDDLStrategy } from "./ddl/index.js";
2
2
  import { RETIRED_SYSTEM_TABLES } from "../system/schema.js";
3
3
  import { getSystemTableShapes } from "./system-table-shapes.js";
4
- import { columnsAllValuesUuidShapedBatch, columnsHaveNonEmptyValueBatch } from "./column-data-probes.js";
4
+ import { columnsAllValuesUuidShapedBatch, columnsHaveNonEmptyValueBatch, resolveRenameDataPendingCandidates } from "./column-data-probes.js";
5
5
  import { collectIntegerWidthTargets, preflightIntegerWidthWidening } from "../migrations/integer-width.js";
6
6
  //#region src/schema/live-parity.ts
7
7
  /**
@@ -370,7 +370,7 @@ function buildRenameDataPendingFinding(table, declaredColumn, candidates) {
370
370
  const candidateList = candidates.map((name) => `\`${name}\``).join(", ");
371
371
  return {
372
372
  kind: "rename_data_pending",
373
- severity: "warning",
373
+ severity: "info",
374
374
  table: table.name,
375
375
  target: declaredColumn,
376
376
  origin: table.origin,
@@ -426,18 +426,16 @@ async function detectRenameDataPending(db, engine, table, liveColumns) {
426
426
  }
427
427
  }
428
428
  const shaped = shapeCheckExtras.size > 0 ? await columnsAllValuesUuidShapedBatch(db, engine, table.name, [...shapeCheckExtras]) : /* @__PURE__ */ new Map();
429
- const candidatesByColumn = /* @__PURE__ */ new Map();
430
- for (const candidate of pending) {
431
- if (candidate.requiresShapeCheck && !(shaped.get(candidate.extraName) ?? false)) continue;
432
- const list = candidatesByColumn.get(candidate.declaredName) ?? [];
433
- list.push(candidate.extraName);
434
- candidatesByColumn.set(candidate.declaredName, list);
435
- }
429
+ const candidatesByColumn = resolveRenameDataPendingCandidates(pending.map((candidate) => ({
430
+ targetName: candidate.declaredName,
431
+ sourceName: candidate.extraName,
432
+ requiresShapeCheck: candidate.requiresShapeCheck,
433
+ extra: void 0
434
+ })), (sourceName) => shaped.get(sourceName) ?? false);
436
435
  const findings = [];
437
436
  for (const [declaredName, candidates] of candidatesByColumn) {
438
- if (candidates.length === 0) continue;
439
- candidates.sort();
440
- findings.push(buildRenameDataPendingFinding(table, declaredName, candidates));
437
+ const names = candidates.map((c) => c.sourceName).sort();
438
+ findings.push(buildRenameDataPendingFinding(table, declaredName, names));
441
439
  }
442
440
  return findings;
443
441
  }