@happyvertical/smrt-core 0.51.6 → 0.51.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/agents/build-knowledge.md +13 -0
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +141 -49
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/manifest/discover-smrt-packages.d.ts +3 -2
- package/dist/manifest/discover-smrt-packages.d.ts.map +1 -1
- package/dist/manifest/discover-smrt-packages.js +34 -15
- package/dist/manifest/discover-smrt-packages.js.map +1 -1
- package/dist/manifest/package-manifest-exports.d.ts +34 -0
- package/dist/manifest/package-manifest-exports.d.ts.map +1 -0
- package/dist/manifest/package-manifest-exports.js +144 -0
- package/dist/manifest/package-manifest-exports.js.map +1 -0
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +19 -27
- package/dist/migrations/differ.js.map +1 -1
- package/dist/schema/column-data-probes.d.ts +71 -0
- package/dist/schema/column-data-probes.d.ts.map +1 -1
- package/dist/schema/column-data-probes.js +79 -1
- package/dist/schema/column-data-probes.js.map +1 -1
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +10 -12
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +93 -3
- package/dist/vite-plugin/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"live-parity.d.ts","sourceRoot":"","sources":["../../src/schema/live-parity.ts"],"names":[],"mappings":"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"}
|
|
@@ -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: "
|
|
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 =
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
439
|
-
|
|
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
|
}
|