@happyvertical/smrt-core 0.43.10 → 0.44.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/generators.md +70 -0
- package/agents/schema-paths.md +99 -0
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +1 -1
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/knowledge.d.ts +17 -1
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +33 -3
- 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 +49 -0
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +220 -4
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/orchestrate.d.ts +23 -0
- package/dist/migrations/orchestrate.d.ts.map +1 -1
- package/dist/migrations/orchestrate.js +12 -3
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/registry/types.d.ts +8 -3
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/foreign-key-ddl.d.ts +7 -0
- package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
- package/dist/schema/foreign-key-ddl.js +7 -1
- package/dist/schema/foreign-key-ddl.js.map +1 -1
- package/dist/schema/schema-manager.d.ts +15 -0
- package/dist/schema/schema-manager.d.ts.map +1 -1
- package/dist/schema/schema-manager.js +52 -9
- package/dist/schema/schema-manager.js.map +1 -1
- package/dist/schema/types.d.ts +10 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/uuid-convergence.d.ts +142 -0
- package/dist/schema/uuid-convergence.d.ts.map +1 -0
- package/dist/schema/uuid-convergence.js +0 -0
- package/dist/schema/uuid-convergence.js.map +1 -0
- package/dist/smrt-knowledge.json +7 -7
- package/dist/utils/scanner-module.d.ts +44 -0
- package/dist/utils/scanner-module.d.ts.map +1 -1
- package/dist/vite-plugin/index.d.ts +14 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +54 -2
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin.js +3 -2
- package/package.json +4 -4
|
@@ -2,6 +2,7 @@ import { normalizeForeignKeyAction, requireForeignKeyAction } from "../schema/fo
|
|
|
2
2
|
import { isJsonPathIndex, isStiSubtypeUniqueIndex, renderIndexTarget } from "../schema/index-utils.js";
|
|
3
3
|
import { foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyConstraint, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaForeignKeys, schemaForeignKeysForEngine } from "../schema/foreign-key-ddl.js";
|
|
4
4
|
import { detectEngine, getDDLStrategy } from "../schema/ddl/index.js";
|
|
5
|
+
import { describeForeignKeyTypeConflict, planUuidConvergence, renderUuidConvergenceRepairHint, renderUuidShapeProbe } from "../schema/uuid-convergence.js";
|
|
5
6
|
import "../schema/utils.js";
|
|
6
7
|
import { planSqliteTableRebuilds, sqliteRebuildPlaceholderSql } from "./sqlite-rebuild.js";
|
|
7
8
|
import { createLogger } from "@happyvertical/logger";
|
|
@@ -291,6 +292,15 @@ var SchemaComparer = class {
|
|
|
291
292
|
options;
|
|
292
293
|
engine;
|
|
293
294
|
ddlStrategy;
|
|
295
|
+
/**
|
|
296
|
+
* Live schemas read during one `compare()` run. The uuid convergence plan
|
|
297
|
+
* (#2608) needs the same introspection the per-table comparison does, and
|
|
298
|
+
* re-reading `getTableSchema` per relationship would multiply catalog
|
|
299
|
+
* round-trips on a wide schema.
|
|
300
|
+
*/
|
|
301
|
+
liveSchemas = /* @__PURE__ */ new Map();
|
|
302
|
+
/** Convergence plan for this run; `null` on non-PostgreSQL engines. */
|
|
303
|
+
uuidConvergence = null;
|
|
294
304
|
constructor(db, options = {}) {
|
|
295
305
|
this.db = db;
|
|
296
306
|
this.options = {
|
|
@@ -313,7 +323,13 @@ var SchemaComparer = class {
|
|
|
313
323
|
changes: [],
|
|
314
324
|
has_changes: false
|
|
315
325
|
};
|
|
326
|
+
this.liveSchemas.clear();
|
|
316
327
|
const existingTables = await this.getExistingTables();
|
|
328
|
+
this.uuidConvergence = await this.buildUuidConvergencePlan(manifestSchemas, existingTables);
|
|
329
|
+
for (const change of this.uuidConvergenceChanges(manifestSchemas)) {
|
|
330
|
+
diff.changes.push(change);
|
|
331
|
+
if (!isInfoOnlyChange(change)) diff.has_changes = true;
|
|
332
|
+
}
|
|
317
333
|
for (const [tableName, schema] of Object.entries(manifestSchemas)) if (!existingTables.has(tableName)) {
|
|
318
334
|
diff.added_tables.push(schema);
|
|
319
335
|
diff.has_changes = true;
|
|
@@ -342,7 +358,7 @@ var SchemaComparer = class {
|
|
|
342
358
|
*/
|
|
343
359
|
async compareTable(tableName, manifest, manifestSchemas = {}) {
|
|
344
360
|
const changes = [];
|
|
345
|
-
const dbSchema = await this.
|
|
361
|
+
const dbSchema = await this.getLiveSchema(tableName);
|
|
346
362
|
if (!dbSchema) return changes;
|
|
347
363
|
const columnChanges = await this.compareColumns(tableName, manifest, dbSchema);
|
|
348
364
|
changes.push(...this.engine === "sqlite" ? await planSqliteTableRebuilds({
|
|
@@ -357,6 +373,173 @@ var SchemaComparer = class {
|
|
|
357
373
|
changes.push(...await this.compareForeignKeys(tableName, manifest, dbSchema, manifestSchemas));
|
|
358
374
|
return changes;
|
|
359
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Read (and memoize for this run) one table's live schema. Returns
|
|
378
|
+
* `undefined` when the table does not exist or the adapter cannot
|
|
379
|
+
* introspect it.
|
|
380
|
+
*/
|
|
381
|
+
async getLiveSchema(tableName) {
|
|
382
|
+
if (this.liveSchemas.has(tableName)) return this.liveSchemas.get(tableName) ?? void 0;
|
|
383
|
+
const schema = await this.db.getTableSchema?.(tableName);
|
|
384
|
+
this.liveSchemas.set(tableName, schema);
|
|
385
|
+
return schema ?? void 0;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Plan the pre-R11 `text` -> `uuid` convergence (#2608).
|
|
389
|
+
*
|
|
390
|
+
* PostgreSQL only: SQLite stores UUIDs as text by design and DuckDB cannot
|
|
391
|
+
* rewrite a column type in place, so both engines return `null` and emit
|
|
392
|
+
* nothing for this drift.
|
|
393
|
+
*/
|
|
394
|
+
async buildUuidConvergencePlan(manifestSchemas, existingTables) {
|
|
395
|
+
if (this.engine !== "postgres") return null;
|
|
396
|
+
for (const tableName of Object.keys(manifestSchemas)) {
|
|
397
|
+
if (!existingTables.has(tableName)) continue;
|
|
398
|
+
await this.getLiveSchema(tableName);
|
|
399
|
+
}
|
|
400
|
+
const relationships = Object.entries(manifestSchemas).flatMap(([tableName, schema]) => schemaForeignKeysForEngine(schema, "postgres").map((foreignKey) => ({
|
|
401
|
+
childTable: tableName,
|
|
402
|
+
childColumn: foreignKey.column,
|
|
403
|
+
parentTable: foreignKey.referencesTable,
|
|
404
|
+
parentColumn: foreignKey.referencesColumn
|
|
405
|
+
})));
|
|
406
|
+
const conversionCandidates = /* @__PURE__ */ new Set();
|
|
407
|
+
for (const relationship of relationships) {
|
|
408
|
+
const childManifest = manifestSchemas[relationship.childTable]?.columns[relationship.childColumn];
|
|
409
|
+
const parentManifest = manifestSchemas[relationship.parentTable]?.columns[relationship.parentColumn];
|
|
410
|
+
if (childManifest?.type !== "UUID" || parentManifest?.type !== "UUID") continue;
|
|
411
|
+
for (const [table, column] of [[relationship.childTable, relationship.childColumn], [relationship.parentTable, relationship.parentColumn]]) {
|
|
412
|
+
if (!existingTables.has(table)) continue;
|
|
413
|
+
const live = this.liveSchemas.get(table)?.columns[column];
|
|
414
|
+
if (!live) continue;
|
|
415
|
+
if (this.normalizeType(live.type) !== "TEXT") continue;
|
|
416
|
+
conversionCandidates.add(`${table} ${column}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (conversionCandidates.size > 0) for (const tableName of existingTables) {
|
|
420
|
+
if (manifestSchemas[tableName]) continue;
|
|
421
|
+
if (tableName.startsWith("sqlite_")) continue;
|
|
422
|
+
await this.getLiveSchema(tableName);
|
|
423
|
+
}
|
|
424
|
+
const liveForeignKeys = [];
|
|
425
|
+
for (const [tableName, schema] of this.liveSchemas) for (const live of schema?.foreignKeys ?? []) liveForeignKeys.push({
|
|
426
|
+
table: tableName,
|
|
427
|
+
column: live.column,
|
|
428
|
+
referencesTable: live.referencesTable,
|
|
429
|
+
referencesColumn: live.referencesColumn
|
|
430
|
+
});
|
|
431
|
+
return planUuidConvergence({
|
|
432
|
+
manifestSchemas,
|
|
433
|
+
relationships,
|
|
434
|
+
liveForeignKeys,
|
|
435
|
+
getLiveColumn: (table, column) => {
|
|
436
|
+
if (!existingTables.has(table)) return void 0;
|
|
437
|
+
const live = this.liveSchemas.get(table)?.columns[column];
|
|
438
|
+
if (!live) return void 0;
|
|
439
|
+
return {
|
|
440
|
+
normalizedType: this.normalizeType(live.type),
|
|
441
|
+
rawType: live.type,
|
|
442
|
+
hasDefault: live.defaultValue !== null && live.defaultValue !== void 0,
|
|
443
|
+
exists: true
|
|
444
|
+
};
|
|
445
|
+
},
|
|
446
|
+
probeUuidShape: (table, column) => this.probeUuidShape(table, column)
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Count values a legacy `text` identifier column holds that cannot be
|
|
451
|
+
* reinterpreted as `uuid`. Any failure resolves to `unavailable`, which the
|
|
452
|
+
* planner treats as a block — SMRT never coerces identifier values.
|
|
453
|
+
*/
|
|
454
|
+
async probeUuidShape(tableName, columnName) {
|
|
455
|
+
try {
|
|
456
|
+
const result = await this.db.query(renderUuidShapeProbe(tableName, columnName));
|
|
457
|
+
const rows = Array.isArray(result) ? result : result.rows || [];
|
|
458
|
+
const count = Number(rows[0]?.invalid_count ?? 0);
|
|
459
|
+
if (!Number.isFinite(count)) return {
|
|
460
|
+
status: "unavailable",
|
|
461
|
+
reason: "probe returned a non-numeric count"
|
|
462
|
+
};
|
|
463
|
+
if (count === 0) return { status: "clean" };
|
|
464
|
+
const sample = rows[0]?.sample_value;
|
|
465
|
+
return {
|
|
466
|
+
status: "dirty",
|
|
467
|
+
count,
|
|
468
|
+
...typeof sample === "string" ? { sample } : {}
|
|
469
|
+
};
|
|
470
|
+
} catch (error) {
|
|
471
|
+
return {
|
|
472
|
+
status: "unavailable",
|
|
473
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Turn the convergence plan into diff entries: executable `type_upgrade`
|
|
479
|
+
* conversions marked `phase: 'pre_foreign_key'`, plus report-only warnings
|
|
480
|
+
* for components the planner refused.
|
|
481
|
+
*/
|
|
482
|
+
uuidConvergenceChanges(manifestSchemas) {
|
|
483
|
+
const plan = this.uuidConvergence;
|
|
484
|
+
if (!plan) return [];
|
|
485
|
+
const manifestColumn = (table, column) => manifestSchemas[table]?.columns[column] ?? { type: "UUID" };
|
|
486
|
+
const changes = [];
|
|
487
|
+
for (const conversion of plan.conversions) changes.push({
|
|
488
|
+
type: "type_upgrade",
|
|
489
|
+
table: conversion.table,
|
|
490
|
+
name: conversion.column,
|
|
491
|
+
column: manifestColumn(conversion.table, conversion.column),
|
|
492
|
+
phase: "pre_foreign_key",
|
|
493
|
+
mismatch: {
|
|
494
|
+
expected: "UUID",
|
|
495
|
+
actual: conversion.liveType
|
|
496
|
+
},
|
|
497
|
+
sql: conversion.statements[0],
|
|
498
|
+
sqlStatements: conversion.statements
|
|
499
|
+
});
|
|
500
|
+
for (const block of plan.blocks) {
|
|
501
|
+
const [first] = block.targets.length > 0 ? block.targets : block.nodes;
|
|
502
|
+
changes.push({
|
|
503
|
+
type: "type_upgrade",
|
|
504
|
+
table: first?.table ?? "",
|
|
505
|
+
name: first?.column,
|
|
506
|
+
column: manifestColumn(first?.table ?? "", first?.column ?? ""),
|
|
507
|
+
phase: "pre_foreign_key",
|
|
508
|
+
mismatch: {
|
|
509
|
+
expected: "UUID",
|
|
510
|
+
actual: "mixed uuid/text"
|
|
511
|
+
},
|
|
512
|
+
advisory: {
|
|
513
|
+
severity: "warning",
|
|
514
|
+
message: `blocked: incompatible column types. The manifest declares UUID for ${block.nodes.map((node) => `${node.table}.${node.column}`).join(", ")}, but ${block.reason}.`,
|
|
515
|
+
suggestedSql: block.suggestedSql
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return changes;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Live-type verdict for one foreign key once this run's conversions land.
|
|
523
|
+
* `undefined` means the pair is compatible (or unknowable) and the ordinary
|
|
524
|
+
* add path applies.
|
|
525
|
+
*/
|
|
526
|
+
async describeForeignKeyTypeBlock(tableName, dbSchema, foreignKey) {
|
|
527
|
+
if (this.engine !== "postgres") return void 0;
|
|
528
|
+
const parentSchema = foreignKey.referencesTable === tableName ? dbSchema : await this.getLiveSchema(foreignKey.referencesTable);
|
|
529
|
+
const childType = dbSchema.columns[foreignKey.column]?.type;
|
|
530
|
+
const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;
|
|
531
|
+
if (!childType || !parentType) return void 0;
|
|
532
|
+
const plan = this.uuidConvergence;
|
|
533
|
+
if ((plan?.effectiveType(tableName, foreignKey.column) ?? this.normalizeType(childType)) === (plan?.effectiveType(foreignKey.referencesTable, foreignKey.referencesColumn) ?? this.normalizeType(parentType))) return void 0;
|
|
534
|
+
return describeForeignKeyTypeConflict({
|
|
535
|
+
childTable: tableName,
|
|
536
|
+
childColumn: foreignKey.column,
|
|
537
|
+
childType,
|
|
538
|
+
parentTable: foreignKey.referencesTable,
|
|
539
|
+
parentColumn: foreignKey.referencesColumn,
|
|
540
|
+
parentType
|
|
541
|
+
});
|
|
542
|
+
}
|
|
360
543
|
async compareForeignKeys(tableName, manifest, dbSchema, manifestSchemas) {
|
|
361
544
|
const changes = [];
|
|
362
545
|
const liveForeignKeys = dbSchema.foreignKeys || [];
|
|
@@ -433,6 +616,29 @@ var SchemaComparer = class {
|
|
|
433
616
|
});
|
|
434
617
|
continue;
|
|
435
618
|
}
|
|
619
|
+
const typeBlock = await this.describeForeignKeyTypeBlock(tableName, dbSchema, foreignKey);
|
|
620
|
+
if (typeBlock) {
|
|
621
|
+
const textNodes = manifest.columns[foreignKey.column]?.type === "UUID" && manifestSchemas[foreignKey.referencesTable]?.columns[foreignKey.referencesColumn]?.type === "UUID" ? [this.normalizeType(dbSchema.columns[foreignKey.column]?.type ?? "") === "TEXT" ? {
|
|
622
|
+
table: tableName,
|
|
623
|
+
column: foreignKey.column
|
|
624
|
+
} : void 0, this.normalizeType((foreignKey.referencesTable === tableName ? dbSchema : this.liveSchemas.get(foreignKey.referencesTable))?.columns[foreignKey.referencesColumn]?.type ?? "") === "TEXT" ? {
|
|
625
|
+
table: foreignKey.referencesTable,
|
|
626
|
+
column: foreignKey.referencesColumn
|
|
627
|
+
} : void 0].filter((node) => node !== void 0) : [];
|
|
628
|
+
const suggestedSql = renderUuidConvergenceRepairHint(textNodes);
|
|
629
|
+
changes.push({
|
|
630
|
+
type: "add_foreign_key",
|
|
631
|
+
table: tableName,
|
|
632
|
+
name: foreignKeyConstraintName(tableName, foreignKey),
|
|
633
|
+
foreignKey,
|
|
634
|
+
advisory: {
|
|
635
|
+
severity: "warning",
|
|
636
|
+
message: `blocked: incompatible column types. ${typeBlock}. ` + (textNodes.length > 0 ? "Converge the columns to UUID, then rerun the migration." : "Align both columns on one physical type deliberately, then rerun the migration."),
|
|
637
|
+
...suggestedSql.length > 0 ? { suggestedSql } : {}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
436
642
|
if (Boolean(dbSchema.columns[foreignKey.column]) && await this.foreignKeyHasOrphans(tableName, foreignKey, orphanOptions)) {
|
|
437
643
|
changes.push({
|
|
438
644
|
type: "add_foreign_key",
|
|
@@ -480,7 +686,7 @@ var SchemaComparer = class {
|
|
|
480
686
|
nullable,
|
|
481
687
|
uuidComparison
|
|
482
688
|
};
|
|
483
|
-
const parentSchema = foreignKey.referencesTable === tableName ? dbSchema : await this.
|
|
689
|
+
const parentSchema = foreignKey.referencesTable === tableName ? dbSchema : await this.getLiveSchema(foreignKey.referencesTable);
|
|
484
690
|
const childType = dbSchema.columns[foreignKey.column]?.type;
|
|
485
691
|
const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;
|
|
486
692
|
if (!childType || !parentType) return {
|
|
@@ -1389,11 +1595,21 @@ function hasActionableChanges(diff) {
|
|
|
1389
1595
|
* they already do for `type_upgrade`.
|
|
1390
1596
|
*/
|
|
1391
1597
|
function getSQLFromDiff(diff) {
|
|
1598
|
+
return getSQLFromChanges(diff.changes);
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Executable SQL for an explicit subset of changes, in the order given.
|
|
1602
|
+
* Callers that have to interleave diff changes with statements produced
|
|
1603
|
+
* elsewhere — the orchestrator, which emits `CREATE TABLE` and deferred
|
|
1604
|
+
* foreign keys of its own — partition `diff.changes` by
|
|
1605
|
+
* {@link SchemaChange.phase} and render each part through this helper.
|
|
1606
|
+
*/
|
|
1607
|
+
function getSQLFromChanges(changes) {
|
|
1392
1608
|
const statements = [];
|
|
1393
|
-
for (const change of
|
|
1609
|
+
for (const change of changes) if (change.type !== "type_mismatch" && !isAdvisoryOnlyChange(change)) statements.push(...change.sqlStatements ?? (change.sql ? [change.sql] : []));
|
|
1394
1610
|
return statements;
|
|
1395
1611
|
}
|
|
1396
1612
|
//#endregion
|
|
1397
|
-
export { SchemaComparer, canonicalizeDefault, extractIndexPredicate, generateSchemaDiff, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, normalizeIndexPredicate, uniqueColumnIndexName };
|
|
1613
|
+
export { SchemaComparer, canonicalizeDefault, extractIndexPredicate, generateSchemaDiff, getSQLFromChanges, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, normalizeIndexPredicate, uniqueColumnIndexName };
|
|
1398
1614
|
|
|
1399
1615
|
//# sourceMappingURL=differ.js.map
|