@happyvertical/smrt-core 0.50.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/dist/index.js +2 -1
- package/dist/knowledge-graph.d.ts +95 -0
- package/dist/knowledge-graph.d.ts.map +1 -0
- package/dist/knowledge-graph.js +268 -0
- package/dist/knowledge-graph.js.map +1 -0
- package/dist/knowledge.d.ts +1 -0
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +57 -4
- package/dist/knowledge.js.map +1 -1
- 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 +50 -18
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +136 -50
- package/dist/migrations/differ.js.map +1 -1
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +133 -36
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/smrt-knowledge.json +12 -7
- package/package.json +4 -4
|
@@ -241,23 +241,55 @@ export declare class SchemaComparer {
|
|
|
241
241
|
*/
|
|
242
242
|
private detectRenameDataPending;
|
|
243
243
|
/**
|
|
244
|
-
* Live-data probe
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
|
|
260
|
-
|
|
244
|
+
* Live-data probe, batched across every column named: does each hold any
|
|
245
|
+
* non-null, non-empty value? One round trip regardless of column count
|
|
246
|
+
* (#2874) — one row of uncorrelated scalar subqueries,
|
|
247
|
+
* `(SELECT 1 FROM t WHERE ... LIMIT 1) AS "col"`, portable across
|
|
248
|
+
* PostgreSQL and SQLite. Deliberately *not* an aggregate
|
|
249
|
+
* (`MAX(CASE WHEN ...)`) over the whole table: an aggregate forces a full
|
|
250
|
+
* scan for every probed column even when the very first row already
|
|
251
|
+
* answers it, which would turn a healthy, mostly-populated large table
|
|
252
|
+
* into a guaranteed full scan on every comparison — worse than the
|
|
253
|
+
* original per-column probe for exactly the schemas #2874 cares about
|
|
254
|
+
* (#2874 review finding F1). Each subquery keeps the original
|
|
255
|
+
* `LIMIT 1` early exit; only the round trip is batched, not the
|
|
256
|
+
* per-column scan cost.
|
|
257
|
+
*
|
|
258
|
+
* Falls back to {@link columnHasNonEmptyValueSingle} per column when the
|
|
259
|
+
* batched statement itself fails (a column dropped concurrently, or a
|
|
260
|
+
* `CAST` the engine rejects) — #2874 review finding F2: a single bad
|
|
261
|
+
* column must withhold only that column's result, not the whole table's
|
|
262
|
+
* detection. A column absent from the returned map means "could not be
|
|
263
|
+
* probed"; callers apply their own fail-closed default.
|
|
264
|
+
*/
|
|
265
|
+
private columnsHaveNonEmptyValueBatch;
|
|
266
|
+
private columnsHaveNonEmptyValueBatchQuery;
|
|
267
|
+
/** Single-column fallback for {@link columnsHaveNonEmptyValueBatch}. */
|
|
268
|
+
private columnHasNonEmptyValueSingle;
|
|
269
|
+
/**
|
|
270
|
+
* Live-data probe, batched across every orphan column named: are every
|
|
271
|
+
* one of each column's non-empty values UUID-shaped
|
|
272
|
+
* ({@link CANONICAL_UUID_PATTERN})? One round trip regardless of column
|
|
273
|
+
* count (#2874), mirroring {@link columnsHaveNonEmptyValueBatch}: one row
|
|
274
|
+
* of uncorrelated scalar subqueries, each
|
|
275
|
+
* `(SELECT 1 FROM t WHERE <non-empty> AND <invalid> LIMIT 1)` — a live
|
|
276
|
+
* value is absent from the result exactly when no invalid row exists, so
|
|
277
|
+
* this also short-circuits on the first invalid row rather than counting
|
|
278
|
+
* every one (an early-exit improvement over the pre-#2874 per-column
|
|
279
|
+
* `count(*)` probe, not just a batching change). PostgreSQL pushes the
|
|
280
|
+
* shape check into its regex operator; SQLite has no regex operator, but
|
|
281
|
+
* its case-sensitive `GLOB` can still express the fixed 36-character
|
|
282
|
+
* canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN} against
|
|
283
|
+
* `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check).
|
|
284
|
+
*
|
|
285
|
+
* Falls back to {@link allNonEmptyValuesUuidShapedSingle} per column on a
|
|
286
|
+
* batch failure, same posture as {@link columnsHaveNonEmptyValueBatch}
|
|
287
|
+
* (#2874 review finding F2).
|
|
288
|
+
*/
|
|
289
|
+
private columnsAllValuesUuidShapedBatch;
|
|
290
|
+
private columnsAllValuesUuidShapedBatchQuery;
|
|
291
|
+
/** Single-column fallback for {@link columnsAllValuesUuidShapedBatch}. */
|
|
292
|
+
private allNonEmptyValuesUuidShapedSingle;
|
|
261
293
|
/**
|
|
262
294
|
* Render the repair for one rename-data-pending pair (#2752): copy
|
|
263
295
|
* non-empty `oldColumn` into `newColumn` only where `newColumn` is still
|
|
@@ -272,7 +304,7 @@ export declare class SchemaComparer {
|
|
|
272
304
|
* for `integer`/`boolean`/`timestamp`/etc. before a same-type rename
|
|
273
305
|
* pair of one of those types ever copies anything. Fixed by comparing
|
|
274
306
|
* `CAST(col AS TEXT) = ''` instead, mirroring the same portable
|
|
275
|
-
* emptiness check {@link
|
|
307
|
+
* emptiness check {@link columnsHaveNonEmptyValueBatch} already uses for
|
|
276
308
|
* detection, so the predicate is valid for every column type.
|
|
277
309
|
* - (P2) An `UPDATE` unconditionally naming `oldColumn` fails once that
|
|
278
310
|
* column is gone, on either engine, regardless of the DROP's own
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/migrations/differ.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/migrations/differ.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA4BH,OAAO,KAAK,EAIV,YAAY,EACZ,gBAAgB,EAChB,UAAU,EACV,WAAW,EACZ,MAAM,oBAAoB,CAAC;AAkB5B,OAAO,KAAK,EAAE,iBAAiB,EAAsB,MAAM,YAAY,CAAC;AA8BxE;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;;;;;OAYG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,iDAAiD;IACjD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;CACxD;AAqBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,MAAM,CAER;AAgCD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAIlE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAQtE;AASD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAsCrE;AAiBD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,UAAU,EAAE,WAAW,GACtB;IACD,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;IACnE,GAAG,EAAE,MAAM,CAAC;CACb,CAyFA;AAED;;GAEG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,WAAW,CAAoC;IACvD;;;;;OAKG;IACH,OAAO,CAAC,WAAW,CAGf;IACJ,uEAAuE;IACvE,OAAO,CAAC,eAAe,CAAoC;gBAE/C,EAAE,EAAE,iBAAiB,EAAE,OAAO,GAAE,WAAgB;IAsB5D;;OAEG;IACG,OAAO,CACX,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAChD,OAAO,CAAC,UAAU,CAAC;IA6EtB;;OAEG;IACG,YAAY,CAChB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,EAC1B,eAAe,GAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAM,GACrD,OAAO,CAAC,YAAY,EAAE,CAAC;IA8D1B;;;;OAIG;YACW,aAAa;IAW3B;;;;;;OAMG;YACW,wBAAwB;IA+FtC;;;;OAIG;YACW,cAAc;IAsC5B;;;;;;;;OAQG;YACW,kBAAkB;IAQhC;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAgD9B;;;;OAIG;YACW,2BAA2B;YAqC3B,kBAAkB;YAuPlB,oBAAoB;YA2BpB,0BAA0B;IAwDxC;;;;;;;;;;;;;;OAcG;YACW,cAAc;IAub5B;;;;;;;;;;;;OAYG;YACW,uBAAuB;IA6NrC;;;;;;;;;;;;;;;;;;;;;OAqBG;YACW,6BAA6B;YAuB7B,kCAAkC;IA2BhD,wEAAwE;YAC1D,4BAA4B;IAa1C;;;;;;;;;;;;;;;;;;;OAmBG;YACW,+BAA+B;YA0B/B,oCAAoC;IA6BlD,0EAA0E;YAC5D,iCAAiC;IAmB/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACH,OAAO,CAAC,yBAAyB;IAgFjC;;;;;;;;OAQG;IACH,OAAO,CAAC,kCAAkC;IA2B1C;;;;;;;;;;;;;;;;;;;;;OAqBG;YACW,wBAAwB;IAwLtC;;;;;;;;OAQG;IACH,OAAO,CAAC,sBAAsB;IAgE9B;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IA2D5B;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,gBAAgB;IAWxB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,qBAAqB;IAe7B;;;;;OAKG;YACW,YAAY;IAe1B;;;;OAIG;YACW,cAAc;IAkB5B,OAAO,CAAC,0BAA0B;IAkBlC,OAAO,CAAC,gCAAgC;IAexC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAwCG;IACH,OAAO,CAAC,cAAc;IAyPtB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,OAAO,CAAC,iBAAiB;IAezB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,sBAAsB;IAI9B;;;;;;;;;;;;;;;;;;;OAmBG;YACW,oBAAoB;IAmDlC;;OAEG;YACW,iBAAiB;IAoB/B;;OAEG;IACH,OAAO,CAAC,aAAa;IAiErB;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,uBAAuB;IA8D/B;;;;;;;OAOG;IACH,OAAO,CAAC,sBAAsB;IAmJ9B;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB;;;;OAIG;IACH,OAAO,CAAC,mCAAmC;IAgB3C;;;;OAIG;IACH,OAAO,CAAC,qCAAqC;IAU7C,OAAO,CAAC,YAAY;IAIpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;YACW,wBAAwB;IAqHtC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAI7B;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,mBAAmB;IAc3B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,oBAAoB;CAG7B;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,iBAAiB,EACrB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACjD,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,UAAU,CAAC,CAGrB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAI9D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,CAIzD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,EAAE,CAU5E"}
|
|
@@ -982,82 +982,168 @@ var SchemaComparer = class {
|
|
|
982
982
|
const manifestColumnNames = new Set(Object.keys(manifest.columns));
|
|
983
983
|
const orphanColumnNames = [...dbColumnNames].filter((name) => !manifestColumnNames.has(name));
|
|
984
984
|
if (orphanColumnNames.length === 0) return [];
|
|
985
|
+
const declaredCandidateNames = Object.keys(manifest.columns).filter((colName) => dbSchema.columns[colName]);
|
|
986
|
+
if (declaredCandidateNames.length === 0) return [];
|
|
987
|
+
const declaredHasData = await this.columnsHaveNonEmptyValueBatch(tableName, declaredCandidateNames);
|
|
988
|
+
const emptyDeclaredNames = declaredCandidateNames.filter((colName) => !(declaredHasData.get(colName) ?? true));
|
|
989
|
+
if (emptyDeclaredNames.length === 0) return [];
|
|
990
|
+
const compatibleOrphanNames = orphanColumnNames.filter((orphanName) => {
|
|
991
|
+
const orphanNormalized = this.normalizeType(dbSchema.columns[orphanName].type);
|
|
992
|
+
return emptyDeclaredNames.some((colName) => {
|
|
993
|
+
const validatedType = isValidSQLDataType(manifest.columns[colName].type) ? manifest.columns[colName].type : "TEXT";
|
|
994
|
+
const declaredNormalized = this.normalizeType(this.ddlStrategy.mapType(validatedType));
|
|
995
|
+
return validatedType === "UUID" && orphanNormalized === "TEXT" || orphanNormalized === declaredNormalized;
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
if (compatibleOrphanNames.length === 0) return [];
|
|
999
|
+
const orphanHasData = await this.columnsHaveNonEmptyValueBatch(tableName, compatibleOrphanNames);
|
|
1000
|
+
const hasData = new Map([...declaredHasData, ...orphanHasData]);
|
|
985
1001
|
const changes = [];
|
|
1002
|
+
const pending = [];
|
|
1003
|
+
const shapeCheckOrphans = /* @__PURE__ */ new Set();
|
|
986
1004
|
for (const [colName, colDef] of Object.entries(manifest.columns)) {
|
|
987
1005
|
if (!dbSchema.columns[colName]) continue;
|
|
988
|
-
|
|
989
|
-
try {
|
|
990
|
-
declaredHasData = await this.columnHasNonEmptyValue(tableName, colName);
|
|
991
|
-
} catch {
|
|
992
|
-
continue;
|
|
993
|
-
}
|
|
994
|
-
if (declaredHasData) continue;
|
|
1006
|
+
if (hasData.get(colName) ?? true) continue;
|
|
995
1007
|
const validatedType = isValidSQLDataType(colDef.type) ? colDef.type : "TEXT";
|
|
996
1008
|
const isLogicalUuid = validatedType === "UUID";
|
|
997
1009
|
const declaredNormalized = this.normalizeType(this.ddlStrategy.mapType(validatedType));
|
|
998
|
-
const
|
|
999
|
-
for (const orphanName of orphanColumnNames) {
|
|
1010
|
+
for (const orphanName of compatibleOrphanNames) {
|
|
1000
1011
|
const orphanCol = dbSchema.columns[orphanName];
|
|
1001
1012
|
const orphanNormalized = this.normalizeType(orphanCol.type);
|
|
1002
1013
|
const requiresShapeCheck = isLogicalUuid && orphanNormalized === "TEXT";
|
|
1003
1014
|
if (!requiresShapeCheck && !(orphanNormalized === declaredNormalized)) continue;
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
continue;
|
|
1009
|
-
}
|
|
1010
|
-
if (!orphanHasData) continue;
|
|
1011
|
-
if (requiresShapeCheck) {
|
|
1012
|
-
let shaped;
|
|
1013
|
-
try {
|
|
1014
|
-
shaped = await this.allNonEmptyValuesUuidShaped(tableName, orphanName);
|
|
1015
|
-
} catch {
|
|
1016
|
-
continue;
|
|
1017
|
-
}
|
|
1018
|
-
if (!shaped) continue;
|
|
1019
|
-
}
|
|
1020
|
-
candidates.push({
|
|
1015
|
+
if (!(hasData.get(orphanName) ?? false)) continue;
|
|
1016
|
+
if (requiresShapeCheck) shapeCheckOrphans.add(orphanName);
|
|
1017
|
+
pending.push({
|
|
1018
|
+
colName,
|
|
1021
1019
|
orphanName,
|
|
1022
|
-
isUuidCast: declaredNormalized === "UUID"
|
|
1020
|
+
isUuidCast: declaredNormalized === "UUID",
|
|
1021
|
+
requiresShapeCheck
|
|
1023
1022
|
});
|
|
1024
1023
|
}
|
|
1025
|
-
if (candidates.length === 1) changes.push(this.describeRenameDataPending(tableName, colName, candidates[0].orphanName, candidates[0].isUuidCast));
|
|
1026
|
-
else if (candidates.length > 1) changes.push(this.describeRenameDataPendingAmbiguous(tableName, colName, candidates.map((c) => c.orphanName)));
|
|
1027
1024
|
}
|
|
1025
|
+
const shaped = shapeCheckOrphans.size > 0 ? await this.columnsAllValuesUuidShapedBatch(tableName, [...shapeCheckOrphans]) : /* @__PURE__ */ new Map();
|
|
1026
|
+
const candidatesByColumn = /* @__PURE__ */ new Map();
|
|
1027
|
+
for (const candidate of pending) {
|
|
1028
|
+
if (candidate.requiresShapeCheck && !(shaped.get(candidate.orphanName) ?? false)) continue;
|
|
1029
|
+
const list = candidatesByColumn.get(candidate.colName) ?? [];
|
|
1030
|
+
list.push({
|
|
1031
|
+
orphanName: candidate.orphanName,
|
|
1032
|
+
isUuidCast: candidate.isUuidCast
|
|
1033
|
+
});
|
|
1034
|
+
candidatesByColumn.set(candidate.colName, list);
|
|
1035
|
+
}
|
|
1036
|
+
for (const [colName, candidates] of candidatesByColumn) if (candidates.length === 1) changes.push(this.describeRenameDataPending(tableName, colName, candidates[0].orphanName, candidates[0].isUuidCast));
|
|
1037
|
+
else if (candidates.length > 1) changes.push(this.describeRenameDataPendingAmbiguous(tableName, colName, candidates.map((c) => c.orphanName)));
|
|
1028
1038
|
return changes;
|
|
1029
1039
|
}
|
|
1030
1040
|
/**
|
|
1031
|
-
* Live-data probe
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1041
|
+
* Live-data probe, batched across every column named: does each hold any
|
|
1042
|
+
* non-null, non-empty value? One round trip regardless of column count
|
|
1043
|
+
* (#2874) — one row of uncorrelated scalar subqueries,
|
|
1044
|
+
* `(SELECT 1 FROM t WHERE ... LIMIT 1) AS "col"`, portable across
|
|
1045
|
+
* PostgreSQL and SQLite. Deliberately *not* an aggregate
|
|
1046
|
+
* (`MAX(CASE WHEN ...)`) over the whole table: an aggregate forces a full
|
|
1047
|
+
* scan for every probed column even when the very first row already
|
|
1048
|
+
* answers it, which would turn a healthy, mostly-populated large table
|
|
1049
|
+
* into a guaranteed full scan on every comparison — worse than the
|
|
1050
|
+
* original per-column probe for exactly the schemas #2874 cares about
|
|
1051
|
+
* (#2874 review finding F1). Each subquery keeps the original
|
|
1052
|
+
* `LIMIT 1` early exit; only the round trip is batched, not the
|
|
1053
|
+
* per-column scan cost.
|
|
1054
|
+
*
|
|
1055
|
+
* Falls back to {@link columnHasNonEmptyValueSingle} per column when the
|
|
1056
|
+
* batched statement itself fails (a column dropped concurrently, or a
|
|
1057
|
+
* `CAST` the engine rejects) — #2874 review finding F2: a single bad
|
|
1058
|
+
* column must withhold only that column's result, not the whole table's
|
|
1059
|
+
* detection. A column absent from the returned map means "could not be
|
|
1060
|
+
* probed"; callers apply their own fail-closed default.
|
|
1034
1061
|
*/
|
|
1035
|
-
async
|
|
1062
|
+
async columnsHaveNonEmptyValueBatch(tableName, colNames) {
|
|
1063
|
+
if (colNames.length === 0) return /* @__PURE__ */ new Map();
|
|
1064
|
+
try {
|
|
1065
|
+
return await this.columnsHaveNonEmptyValueBatchQuery(tableName, colNames);
|
|
1066
|
+
} catch {
|
|
1067
|
+
const hasData = /* @__PURE__ */ new Map();
|
|
1068
|
+
for (const colName of colNames) try {
|
|
1069
|
+
hasData.set(colName, await this.columnHasNonEmptyValueSingle(tableName, colName));
|
|
1070
|
+
} catch {}
|
|
1071
|
+
return hasData;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async columnsHaveNonEmptyValueBatchQuery(tableName, colNames) {
|
|
1075
|
+
const quotedTable = this.quoteIdentifier(tableName);
|
|
1076
|
+
const selects = colNames.map((colName, index) => {
|
|
1077
|
+
const quotedCol = this.quoteIdentifier(colName);
|
|
1078
|
+
return `(SELECT 1 FROM ${quotedTable} WHERE ${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> '' LIMIT 1) AS c${index}`;
|
|
1079
|
+
});
|
|
1080
|
+
const row = (await this.db.query(`SELECT ${selects.join(", ")}`)).rows?.[0] ?? {};
|
|
1081
|
+
const hasData = /* @__PURE__ */ new Map();
|
|
1082
|
+
colNames.forEach((colName, index) => {
|
|
1083
|
+
hasData.set(colName, row[`c${index}`] != null);
|
|
1084
|
+
});
|
|
1085
|
+
return hasData;
|
|
1086
|
+
}
|
|
1087
|
+
/** Single-column fallback for {@link columnsHaveNonEmptyValueBatch}. */
|
|
1088
|
+
async columnHasNonEmptyValueSingle(tableName, colName) {
|
|
1036
1089
|
const quotedTable = this.quoteIdentifier(tableName);
|
|
1037
1090
|
const quotedCol = this.quoteIdentifier(colName);
|
|
1038
1091
|
return ((await this.db.query(`SELECT 1 AS present FROM ${quotedTable} WHERE ${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> '' LIMIT 1`)).rows?.length ?? 0) > 0;
|
|
1039
1092
|
}
|
|
1040
1093
|
/**
|
|
1041
|
-
* Live-data probe
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1094
|
+
* Live-data probe, batched across every orphan column named: are every
|
|
1095
|
+
* one of each column's non-empty values UUID-shaped
|
|
1096
|
+
* ({@link CANONICAL_UUID_PATTERN})? One round trip regardless of column
|
|
1097
|
+
* count (#2874), mirroring {@link columnsHaveNonEmptyValueBatch}: one row
|
|
1098
|
+
* of uncorrelated scalar subqueries, each
|
|
1099
|
+
* `(SELECT 1 FROM t WHERE <non-empty> AND <invalid> LIMIT 1)` — a live
|
|
1100
|
+
* value is absent from the result exactly when no invalid row exists, so
|
|
1101
|
+
* this also short-circuits on the first invalid row rather than counting
|
|
1102
|
+
* every one (an early-exit improvement over the pre-#2874 per-column
|
|
1103
|
+
* `count(*)` probe, not just a batching change). PostgreSQL pushes the
|
|
1104
|
+
* shape check into its regex operator; SQLite has no regex operator, but
|
|
1105
|
+
* its case-sensitive `GLOB` can still express the fixed 36-character
|
|
1106
|
+
* canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN} against
|
|
1107
|
+
* `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check).
|
|
1108
|
+
*
|
|
1109
|
+
* Falls back to {@link allNonEmptyValuesUuidShapedSingle} per column on a
|
|
1110
|
+
* batch failure, same posture as {@link columnsHaveNonEmptyValueBatch}
|
|
1111
|
+
* (#2874 review finding F2).
|
|
1050
1112
|
*/
|
|
1051
|
-
async
|
|
1113
|
+
async columnsAllValuesUuidShapedBatch(tableName, colNames) {
|
|
1114
|
+
if (colNames.length === 0) return /* @__PURE__ */ new Map();
|
|
1115
|
+
try {
|
|
1116
|
+
return await this.columnsAllValuesUuidShapedBatchQuery(tableName, colNames);
|
|
1117
|
+
} catch {
|
|
1118
|
+
const shaped = /* @__PURE__ */ new Map();
|
|
1119
|
+
for (const colName of colNames) try {
|
|
1120
|
+
shaped.set(colName, await this.allNonEmptyValuesUuidShapedSingle(tableName, colName));
|
|
1121
|
+
} catch {}
|
|
1122
|
+
return shaped;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async columnsAllValuesUuidShapedBatchQuery(tableName, colNames) {
|
|
1126
|
+
const quotedTable = this.quoteIdentifier(tableName);
|
|
1127
|
+
const selects = colNames.map((colName, index) => {
|
|
1128
|
+
const quotedCol = this.quoteIdentifier(colName);
|
|
1129
|
+
const nonEmptyPredicate = `${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> ''`;
|
|
1130
|
+
const invalidPredicate = this.engine === "postgres" ? `CAST(${quotedCol} AS TEXT) !~* '${CANONICAL_UUID_PATTERN}'` : `NOT (LENGTH(CAST(${quotedCol} AS TEXT)) = 36 AND LOWER(CAST(${quotedCol} AS TEXT)) GLOB '${CANONICAL_UUID_SQLITE_GLOB_PATTERN}')`;
|
|
1131
|
+
return `(SELECT 1 FROM ${quotedTable} WHERE ${nonEmptyPredicate} AND ${invalidPredicate} LIMIT 1) AS c${index}`;
|
|
1132
|
+
});
|
|
1133
|
+
const row = (await this.db.query(`SELECT ${selects.join(", ")}`)).rows?.[0] ?? {};
|
|
1134
|
+
const shaped = /* @__PURE__ */ new Map();
|
|
1135
|
+
colNames.forEach((colName, index) => {
|
|
1136
|
+
shaped.set(colName, row[`c${index}`] == null);
|
|
1137
|
+
});
|
|
1138
|
+
return shaped;
|
|
1139
|
+
}
|
|
1140
|
+
/** Single-column fallback for {@link columnsAllValuesUuidShapedBatch}. */
|
|
1141
|
+
async allNonEmptyValuesUuidShapedSingle(tableName, colName) {
|
|
1052
1142
|
const quotedTable = this.quoteIdentifier(tableName);
|
|
1053
1143
|
const quotedCol = this.quoteIdentifier(colName);
|
|
1054
1144
|
const nonEmptyPredicate = `${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> ''`;
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
return Number(row?.invalid_count ?? 0) === 0;
|
|
1058
|
-
}
|
|
1059
|
-
const row = (await this.db.query(`SELECT count(*) AS invalid_count FROM ${quotedTable} WHERE ${nonEmptyPredicate} AND NOT (LENGTH(CAST(${quotedCol} AS TEXT)) = 36 AND LOWER(CAST(${quotedCol} AS TEXT)) GLOB '${CANONICAL_UUID_SQLITE_GLOB_PATTERN}')`)).rows?.[0];
|
|
1060
|
-
return Number(row?.invalid_count ?? 0) === 0;
|
|
1145
|
+
const invalidPredicate = this.engine === "postgres" ? `CAST(${quotedCol} AS TEXT) !~* '${CANONICAL_UUID_PATTERN}'` : `NOT (LENGTH(CAST(${quotedCol} AS TEXT)) = 36 AND LOWER(CAST(${quotedCol} AS TEXT)) GLOB '${CANONICAL_UUID_SQLITE_GLOB_PATTERN}')`;
|
|
1146
|
+
return ((await this.db.query(`SELECT 1 AS invalid FROM ${quotedTable} WHERE ${nonEmptyPredicate} AND ${invalidPredicate} LIMIT 1`)).rows?.length ?? 0) === 0;
|
|
1061
1147
|
}
|
|
1062
1148
|
/**
|
|
1063
1149
|
* Render the repair for one rename-data-pending pair (#2752): copy
|
|
@@ -1073,7 +1159,7 @@ var SchemaComparer = class {
|
|
|
1073
1159
|
* for `integer`/`boolean`/`timestamp`/etc. before a same-type rename
|
|
1074
1160
|
* pair of one of those types ever copies anything. Fixed by comparing
|
|
1075
1161
|
* `CAST(col AS TEXT) = ''` instead, mirroring the same portable
|
|
1076
|
-
* emptiness check {@link
|
|
1162
|
+
* emptiness check {@link columnsHaveNonEmptyValueBatch} already uses for
|
|
1077
1163
|
* detection, so the predicate is valid for every column type.
|
|
1078
1164
|
* - (P2) An `UPDATE` unconditionally naming `oldColumn` fails once that
|
|
1079
1165
|
* column is gone, on either engine, regardless of the DROP's own
|