@happyvertical/smrt-core 0.49.8 → 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.
Files changed (54) hide show
  1. package/AGENTS.md +1 -0
  2. package/dist/dispatch/bus.d.ts.map +1 -1
  3. package/dist/dispatch/bus.js +15 -12
  4. package/dist/dispatch/bus.js.map +1 -1
  5. package/dist/dispatch/collections/DispatchSubscriptions.d.ts +7 -2
  6. package/dist/dispatch/collections/DispatchSubscriptions.d.ts.map +1 -1
  7. package/dist/dispatch/collections/DispatchSubscriptions.js +9 -8
  8. package/dist/dispatch/collections/DispatchSubscriptions.js.map +1 -1
  9. package/dist/dispatch/collections/Dispatches.d.ts +12 -2
  10. package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
  11. package/dist/dispatch/collections/Dispatches.js +14 -8
  12. package/dist/dispatch/collections/Dispatches.js.map +1 -1
  13. package/dist/dispatch/types.d.ts +16 -1
  14. package/dist/dispatch/types.d.ts.map +1 -1
  15. package/dist/index.js +2 -1
  16. package/dist/knowledge-graph.d.ts +95 -0
  17. package/dist/knowledge-graph.d.ts.map +1 -0
  18. package/dist/knowledge-graph.js +268 -0
  19. package/dist/knowledge-graph.js.map +1 -0
  20. package/dist/knowledge.d.ts +1 -0
  21. package/dist/knowledge.d.ts.map +1 -1
  22. package/dist/knowledge.js +57 -4
  23. package/dist/knowledge.js.map +1 -1
  24. package/dist/manifest/static-manifest.js +1 -1
  25. package/dist/manifest/static-manifest.js.map +1 -1
  26. package/dist/manifest/store.js +1 -1
  27. package/dist/manifest.json +1 -1
  28. package/dist/migrations/backfill-tracker.d.ts +7 -0
  29. package/dist/migrations/backfill-tracker.d.ts.map +1 -1
  30. package/dist/migrations/backfill-tracker.js +20 -1
  31. package/dist/migrations/backfill-tracker.js.map +1 -1
  32. package/dist/migrations/differ.d.ts +50 -18
  33. package/dist/migrations/differ.d.ts.map +1 -1
  34. package/dist/migrations/differ.js +136 -50
  35. package/dist/migrations/differ.js.map +1 -1
  36. package/dist/registry/types.d.ts +21 -1
  37. package/dist/registry/types.d.ts.map +1 -1
  38. package/dist/schema/live-parity.d.ts.map +1 -1
  39. package/dist/schema/live-parity.js +133 -36
  40. package/dist/schema/live-parity.js.map +1 -1
  41. package/dist/smrt-knowledge.json +12 -7
  42. package/dist/system/bootstrap.d.ts +23 -0
  43. package/dist/system/bootstrap.d.ts.map +1 -1
  44. package/dist/system/bootstrap.js +52 -17
  45. package/dist/system/bootstrap.js.map +1 -1
  46. package/dist/vite-plugin/index.d.ts +3 -1
  47. package/dist/vite-plugin/index.d.ts.map +1 -1
  48. package/dist/vite-plugin/index.js +9 -2
  49. package/dist/vite-plugin/index.js.map +1 -1
  50. package/dist/vite-plugin/sveltekit-generator.d.ts +26 -5
  51. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  52. package/dist/vite-plugin/sveltekit-generator.js +48 -14
  53. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  54. package/package.json +10 -10
@@ -1 +1 @@
1
- {"version":3,"file":"backfill-tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/backfill-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAU5D,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,KAAK,EAAE,OAAO;CAI3B;AAQD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC,OAAO,CAAC,iBAAiB,CAA8B;gBAE3C,OAAO,EAAE,sBAAsB;IAI3C;;;;OAIG;IACH,MAAM,CAAC,qBAAqB,CAC1B,EAAE,EAAE,iBAAiB,EACrB,MAAM,EAAE,iBAAiB,GACxB,IAAI;IAOP;;;;;;OAMG;IACH,MAAM,CAAC,oBAAoB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;IAOxD,mEAAmE;IACnE,MAAM,CAAC,wBAAwB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;YAI9C,WAAW;IASzB;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA8CjC,iDAAiD;IAC3C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ/C;;;;OAIG;IACG,aAAa,CACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,YAAY,CAAC,CAAC,EAClB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC;QAAE,GAAG,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,GAAG,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;IAOnE,kDAAkD;IAC5C,WAAW,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAYhC,kBAAkB;IAgBhC,OAAO,CAAC,mBAAmB;CAM5B"}
1
+ {"version":3,"file":"backfill-tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/backfill-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAU5D,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,KAAK,EAAE,OAAO;CAI3B;AAQD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC,OAAO,CAAC,iBAAiB,CAA8B;gBAE3C,OAAO,EAAE,sBAAsB;IAI3C;;;;OAIG;IACH,MAAM,CAAC,qBAAqB,CAC1B,EAAE,EAAE,iBAAiB,EACrB,MAAM,EAAE,iBAAiB,GACxB,IAAI;IAOP;;;;;;OAMG;IACH,MAAM,CAAC,oBAAoB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;IAOxD,mEAAmE;IACnE,MAAM,CAAC,wBAAwB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;YAI9C,WAAW;IASzB;;;;;OAKG;YACW,sBAAsB;IAWpC;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAiDjC,iDAAiD;IAC3C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ/C;;;;OAIG;IACG,aAAa,CACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,YAAY,CAAC,CAAC,EAClB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC;QAAE,GAAG,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,GAAG,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;IAOnE,kDAAkD;IAC5C,WAAW,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAYhC,kBAAkB;IAgBhC,OAAO,CAAC,mBAAmB;CAM5B"}
@@ -49,6 +49,22 @@ var BackfillTracker = class {
49
49
  await assertPostgresSystemTimestampsCurrent(this.db);
50
50
  }
51
51
  /**
52
+ * A cold runtime process must be able to adopt a migration-created tracker
53
+ * using its ordinary read privilege. PostgreSQL requires schema CREATE even
54
+ * for CREATE TABLE IF NOT EXISTS, so do not issue that DDL until absence is
55
+ * positively classified.
56
+ */
57
+ async hasUsableExistingTable() {
58
+ try {
59
+ await this.db.query("SELECT 1 FROM _smrt_backfills LIMIT 1");
60
+ await assertPostgresSystemTimestampsCurrent(this.db);
61
+ return true;
62
+ } catch (error) {
63
+ if (isMissingBackfillTableError(error)) return false;
64
+ throw error;
65
+ }
66
+ }
67
+ /**
52
68
  * Create the `_smrt_backfills` table if it doesn't already exist.
53
69
  * Safe to call repeatedly and concurrently.
54
70
  */
@@ -69,7 +85,10 @@ var BackfillTracker = class {
69
85
  }
70
86
  }
71
87
  if (isRootDatabase(this.db)) {
72
- const initialization = this.createTable().then(() => void 0).catch((error) => {
88
+ const initialization = (async () => {
89
+ if (await this.hasUsableExistingTable()) return;
90
+ await this.createTable();
91
+ })().then(() => void 0).catch((error) => {
73
92
  if (getSharedInitialization(target) === initialization) deleteSharedInitialization(target);
74
93
  throw error;
75
94
  });
@@ -1 +1 @@
1
- {"version":3,"file":"backfill-tracker.js","names":[],"sources":["../../src/migrations/backfill-tracker.ts"],"sourcesContent":["/**\n * Tracks application-specific data backfills (not schema migrations).\n *\n * Sits next to `MigrationTracker` but is intentionally minimal — backfills\n * have no checksum, no rollback semantics, no DDL. They're \"this data\n * correction has run, don't run it again\" markers, keyed by a stable\n * backfill name the app chooses.\n *\n * Backed by the `_smrt_backfills` system table (see\n * `../system/schema.ts`). The table is created lazily on first use —\n * every public method (`isApplied`, `recordApplied`, `runIfPending`,\n * `listApplied`) awaits `initialize()` itself, so consumers never need\n * to call `initialize()` explicitly.\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n assertPostgresSystemTimestampsCurrent,\n getDatabaseEngine,\n} from '../system/compatibility.js';\nimport {\n CREATE_SMRT_BACKFILLS_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\n\nexport interface BackfillTrackerOptions {\n db: DatabaseInterface;\n}\n\nexport interface BackfillRecord {\n name: string;\n appliedAt: string;\n description: string | null;\n packageName: string | null;\n}\n\n/**\n * A tracker query proved that its inherited root initialization is stale.\n *\n * Transaction coordinators must roll back the current transaction, refresh\n * the root tracker outside that transaction, and retry with a fresh handle.\n */\nexport class BackfillTableUnavailableError extends Error {\n constructor(cause: unknown) {\n super('The shared _smrt_backfills table is unavailable.', { cause });\n this.name = 'BackfillTableUnavailableError';\n }\n}\n\n// Only root adapters can establish a durable shared initialization. A tracker\n// created around a transaction handle may have its DDL rolled back later, so\n// transaction-local successes must never poison this cache.\nconst initializationByObject = new WeakMap<object, Promise<void>>();\nconst inheritedInitializationByHandle = new WeakMap<object, object>();\n\nexport class BackfillTracker {\n private readonly db: DatabaseInterface;\n private initializePromise: Promise<void> | null = null;\n\n constructor(options: BackfillTrackerOptions) {\n this.db = options.db;\n }\n\n /**\n * Mark a transaction handle as inheriting a table initialized durably by its\n * root adapter. Framework transaction coordinators use this after root DDL\n * commits so transaction-local trackers can skip redundant `CREATE TABLE`.\n */\n static inheritInitialization(\n db: DatabaseInterface,\n rootDb: DatabaseInterface,\n ): void {\n inheritedInitializationByHandle.set(\n db as object,\n databaseTargetIdentity(rootDb),\n );\n }\n\n /**\n * Suppress lazy DDL on a caller-owned transaction.\n *\n * Marker reads may proceed when the table already exists. A missing table\n * raises `BackfillTableUnavailableError` so the caller can fail closed or\n * retry with a root adapter.\n */\n static requireExistingTable(db: DatabaseInterface): void {\n inheritedInitializationByHandle.set(\n db as object,\n databaseTargetIdentity(db),\n );\n }\n\n /** Forget a root initialization after a schema lifecycle reset. */\n static invalidateInitialization(db: DatabaseInterface): void {\n deleteSharedInitialization(databaseTargetIdentity(db));\n }\n\n private async createTable(): Promise<void> {\n const ddl = getSystemTableDDLForEngine(\n CREATE_SMRT_BACKFILLS_TABLE,\n getDatabaseEngine(this.db),\n );\n await this.db.query(ddl);\n await assertPostgresSystemTimestampsCurrent(this.db);\n }\n\n /**\n * Create the `_smrt_backfills` table if it doesn't already exist.\n * Safe to call repeatedly and concurrently.\n */\n async initialize(): Promise<void> {\n if (inheritedInitializationByHandle.has(this.db as object)) {\n return;\n }\n\n const target = databaseTargetIdentity(this.db);\n const existing = getSharedInitialization(target);\n if (existing) {\n await existing;\n if (!isRootDatabase(this.db)) return;\n try {\n await this.db.query('SELECT 1 FROM _smrt_backfills LIMIT 1');\n return;\n } catch (error) {\n if (!isMissingBackfillTableError(error)) throw error;\n if (getSharedInitialization(target) === existing) {\n deleteSharedInitialization(target);\n }\n return this.initialize();\n }\n }\n\n if (isRootDatabase(this.db)) {\n const initialization = this.createTable()\n .then(() => undefined)\n .catch((error) => {\n if (getSharedInitialization(target) === initialization) {\n deleteSharedInitialization(target);\n }\n throw error;\n });\n setSharedInitialization(target, initialization);\n return initialization;\n }\n\n if (!this.initializePromise) {\n this.initializePromise = this.createTable()\n .then(() => undefined)\n .catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n /** Has the backfill named `name` already run? */\n async isApplied(name: string): Promise<boolean> {\n const result = await this.queryBackfillTable(\n 'SELECT 1 FROM _smrt_backfills WHERE name = ? LIMIT 1',\n name,\n );\n return result.rows.length > 0;\n }\n\n /**\n * Record that the backfill named `name` ran. Uses INSERT ... ON CONFLICT\n * so callers don't need to check `isApplied` first if they don't care\n * about distinguishing \"already applied\" from \"just applied\".\n */\n async recordApplied(\n name: string,\n options: { description?: string; packageName?: string } = {},\n ): Promise<void> {\n await this.queryBackfillTable(\n `INSERT INTO _smrt_backfills (name, description, package_name)\n VALUES (?, ?, ?)\n ON CONFLICT (name) DO NOTHING`,\n name,\n options.description ?? null,\n options.packageName ?? null,\n );\n }\n\n /**\n * Convenience wrapper: if `name` hasn't been applied, run `fn` and\n * record the result. Returns `{ ran, result }`:\n * - `{ ran: true, result: T }` — backfill was pending and `fn` ran\n * - `{ ran: false, result: null }` — backfill was already applied\n *\n * The discriminator avoids the `T | null` ambiguity an earlier shape had\n * (a backfill that legitimately returns `null` was indistinguishable\n * from \"already applied\"). Callers check `ran` to decide whether `fn`\n * executed; `result` carries whatever `fn` returned.\n *\n * This is the common pattern an app's migrate orchestration follows:\n * a list of `(name, runner)` pairs, each guarded by isApplied/run/record.\n *\n * **Single-runner only.** This is a check-then-act pattern with no\n * locking: two processes that call `runIfPending` for the same `name`\n * concurrently can both observe `isApplied === false`, both execute\n * `fn`, and only then race to `recordApplied`. The `ON CONFLICT DO\n * NOTHING` in `recordApplied` keeps the table clean but does NOT prevent\n * `fn` from running twice. Acceptable when backfills are run from a\n * single deploy-time migration step (the common case); if you need to\n * fan out backfills across multiple workers, wrap the call in your own\n * mutex (e.g. a Postgres advisory lock keyed by backfill name).\n *\n * **`fn` MUST be idempotent.** The order is `fn` → `recordApplied`, with\n * no surrounding transaction. If `fn` succeeds but `recordApplied`\n * throws (DB connection drops, transient error), the work has happened\n * but is not recorded — the next call sees `isApplied === false` and\n * re-runs `fn`. For non-idempotent work (slug rewrites that consume\n * source rows, one-shot lookups), wrap the entire call in a transaction\n * yourself or structure `fn` so a second execution is a no-op.\n */\n async runIfPending<T>(\n name: string,\n fn: () => Promise<T>,\n options: { description?: string; packageName?: string } = {},\n ): Promise<{ ran: false; result: null } | { ran: true; result: T }> {\n if (await this.isApplied(name)) return { ran: false, result: null };\n const result = await fn();\n await this.recordApplied(name, options);\n return { ran: true, result };\n }\n\n /** List every recorded backfill, oldest first. */\n async listApplied(): Promise<BackfillRecord[]> {\n const result = await this.queryBackfillTable(\n 'SELECT name, applied_at, description, package_name FROM _smrt_backfills ORDER BY applied_at ASC, name ASC',\n );\n return result.rows.map((row) => ({\n name: String(row.name ?? ''),\n appliedAt: normalizeAppliedAt(row.applied_at),\n description: row.description == null ? null : String(row.description),\n packageName: row.package_name == null ? null : String(row.package_name),\n }));\n }\n\n private async queryBackfillTable(sql: string, ...params: unknown[]) {\n await this.initialize();\n try {\n return await this.db.query(sql, ...params);\n } catch (error) {\n if (!isMissingBackfillTableError(error)) throw error;\n const inheritedRoot = inheritedInitializationByHandle.get(\n this.db as object,\n );\n this.resetInitialization(inheritedRoot);\n if (inheritedRoot) throw new BackfillTableUnavailableError(error);\n await this.initialize();\n return this.db.query(sql, ...params);\n }\n }\n\n private resetInitialization(inheritedRoot?: object): void {\n this.initializePromise = null;\n inheritedInitializationByHandle.delete(this.db as object);\n deleteSharedInitialization(databaseTargetIdentity(this.db));\n if (inheritedRoot) deleteSharedInitialization(inheritedRoot);\n }\n}\n\nfunction databaseTargetIdentity(db: DatabaseInterface): object {\n const client = db.client;\n return client !== null &&\n (typeof client === 'object' || typeof client === 'function')\n ? client\n : db;\n}\n\nfunction isRootDatabase(db: DatabaseInterface): boolean {\n return typeof db.beginTransaction === 'function';\n}\n\nfunction getSharedInitialization(target: object): Promise<void> | undefined {\n return initializationByObject.get(target);\n}\n\nfunction setSharedInitialization(\n target: object,\n initialization: Promise<void>,\n): void {\n initializationByObject.set(target, initialization);\n}\n\nfunction deleteSharedInitialization(target: object): void {\n initializationByObject.delete(target);\n}\n\nfunction isMissingBackfillTableError(error: unknown): boolean {\n const pending: unknown[] = [error];\n const seen = new Set<unknown>();\n const missingPattern =\n /(?:no such table|does not exist|doesn't exist|unknown table|not found)/iu;\n while (pending.length > 0) {\n const current = pending.pop();\n if (typeof current === 'string') {\n if (current.includes('_smrt_backfills') && missingPattern.test(current)) {\n return true;\n }\n continue;\n }\n if (!current || typeof current !== 'object' || seen.has(current)) continue;\n seen.add(current);\n if (\n current instanceof Error &&\n current.message.includes('_smrt_backfills') &&\n missingPattern.test(current.message)\n ) {\n return true;\n }\n const details = current as {\n cause?: unknown;\n context?: unknown;\n details?: unknown;\n originalError?: unknown;\n };\n pending.push(\n details.cause,\n details.context,\n details.details,\n details.originalError,\n );\n if (current instanceof AggregateError) pending.push(...current.errors);\n }\n return false;\n}\n\n/**\n * Convert a raw `applied_at` cell into an ISO 8601 string.\n *\n * Different DB drivers return this column in different shapes:\n * - Postgres `node-pg` returns a JS `Date` directly.\n * - SQLite drivers typically return the `CURRENT_TIMESTAMP` literal\n * `'YYYY-MM-DD HH:MM:SS'` (no `T`, no timezone). V8's `Date`\n * constructor parses this as local time, but that's\n * implementation-defined and other runtimes may return Invalid Date.\n * - A corrupted row could return any string.\n *\n * If the value parses to a valid Date we return `toISOString()`. If it\n * doesn't (or the column is null/empty), we fall back to the raw string —\n * better to surface a non-ISO timestamp than crash `listApplied()` with a\n * `RangeError` and take down the caller.\n */\nfunction normalizeAppliedAt(value: unknown): string {\n if (value == null || value === '') return '';\n const raw = String(value);\n const parsed = new Date(raw);\n if (Number.isNaN(parsed.getTime())) {\n return raw;\n }\n return parsed.toISOString();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"backfill-tracker.js","names":[],"sources":["../../src/migrations/backfill-tracker.ts"],"sourcesContent":["/**\n * Tracks application-specific data backfills (not schema migrations).\n *\n * Sits next to `MigrationTracker` but is intentionally minimal — backfills\n * have no checksum, no rollback semantics, no DDL. They're \"this data\n * correction has run, don't run it again\" markers, keyed by a stable\n * backfill name the app chooses.\n *\n * Backed by the `_smrt_backfills` system table (see\n * `../system/schema.ts`). The table is created lazily on first use —\n * every public method (`isApplied`, `recordApplied`, `runIfPending`,\n * `listApplied`) awaits `initialize()` itself, so consumers never need\n * to call `initialize()` explicitly.\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n assertPostgresSystemTimestampsCurrent,\n getDatabaseEngine,\n} from '../system/compatibility.js';\nimport {\n CREATE_SMRT_BACKFILLS_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\n\nexport interface BackfillTrackerOptions {\n db: DatabaseInterface;\n}\n\nexport interface BackfillRecord {\n name: string;\n appliedAt: string;\n description: string | null;\n packageName: string | null;\n}\n\n/**\n * A tracker query proved that its inherited root initialization is stale.\n *\n * Transaction coordinators must roll back the current transaction, refresh\n * the root tracker outside that transaction, and retry with a fresh handle.\n */\nexport class BackfillTableUnavailableError extends Error {\n constructor(cause: unknown) {\n super('The shared _smrt_backfills table is unavailable.', { cause });\n this.name = 'BackfillTableUnavailableError';\n }\n}\n\n// Only root adapters can establish a durable shared initialization. A tracker\n// created around a transaction handle may have its DDL rolled back later, so\n// transaction-local successes must never poison this cache.\nconst initializationByObject = new WeakMap<object, Promise<void>>();\nconst inheritedInitializationByHandle = new WeakMap<object, object>();\n\nexport class BackfillTracker {\n private readonly db: DatabaseInterface;\n private initializePromise: Promise<void> | null = null;\n\n constructor(options: BackfillTrackerOptions) {\n this.db = options.db;\n }\n\n /**\n * Mark a transaction handle as inheriting a table initialized durably by its\n * root adapter. Framework transaction coordinators use this after root DDL\n * commits so transaction-local trackers can skip redundant `CREATE TABLE`.\n */\n static inheritInitialization(\n db: DatabaseInterface,\n rootDb: DatabaseInterface,\n ): void {\n inheritedInitializationByHandle.set(\n db as object,\n databaseTargetIdentity(rootDb),\n );\n }\n\n /**\n * Suppress lazy DDL on a caller-owned transaction.\n *\n * Marker reads may proceed when the table already exists. A missing table\n * raises `BackfillTableUnavailableError` so the caller can fail closed or\n * retry with a root adapter.\n */\n static requireExistingTable(db: DatabaseInterface): void {\n inheritedInitializationByHandle.set(\n db as object,\n databaseTargetIdentity(db),\n );\n }\n\n /** Forget a root initialization after a schema lifecycle reset. */\n static invalidateInitialization(db: DatabaseInterface): void {\n deleteSharedInitialization(databaseTargetIdentity(db));\n }\n\n private async createTable(): Promise<void> {\n const ddl = getSystemTableDDLForEngine(\n CREATE_SMRT_BACKFILLS_TABLE,\n getDatabaseEngine(this.db),\n );\n await this.db.query(ddl);\n await assertPostgresSystemTimestampsCurrent(this.db);\n }\n\n /**\n * A cold runtime process must be able to adopt a migration-created tracker\n * using its ordinary read privilege. PostgreSQL requires schema CREATE even\n * for CREATE TABLE IF NOT EXISTS, so do not issue that DDL until absence is\n * positively classified.\n */\n private async hasUsableExistingTable(): Promise<boolean> {\n try {\n await this.db.query('SELECT 1 FROM _smrt_backfills LIMIT 1');\n await assertPostgresSystemTimestampsCurrent(this.db);\n return true;\n } catch (error) {\n if (isMissingBackfillTableError(error)) return false;\n throw error;\n }\n }\n\n /**\n * Create the `_smrt_backfills` table if it doesn't already exist.\n * Safe to call repeatedly and concurrently.\n */\n async initialize(): Promise<void> {\n if (inheritedInitializationByHandle.has(this.db as object)) {\n return;\n }\n\n const target = databaseTargetIdentity(this.db);\n const existing = getSharedInitialization(target);\n if (existing) {\n await existing;\n if (!isRootDatabase(this.db)) return;\n try {\n await this.db.query('SELECT 1 FROM _smrt_backfills LIMIT 1');\n return;\n } catch (error) {\n if (!isMissingBackfillTableError(error)) throw error;\n if (getSharedInitialization(target) === existing) {\n deleteSharedInitialization(target);\n }\n return this.initialize();\n }\n }\n\n if (isRootDatabase(this.db)) {\n const initialization = (async () => {\n if (await this.hasUsableExistingTable()) return;\n await this.createTable();\n })()\n .then(() => undefined)\n .catch((error) => {\n if (getSharedInitialization(target) === initialization) {\n deleteSharedInitialization(target);\n }\n throw error;\n });\n setSharedInitialization(target, initialization);\n return initialization;\n }\n\n if (!this.initializePromise) {\n this.initializePromise = this.createTable()\n .then(() => undefined)\n .catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n /** Has the backfill named `name` already run? */\n async isApplied(name: string): Promise<boolean> {\n const result = await this.queryBackfillTable(\n 'SELECT 1 FROM _smrt_backfills WHERE name = ? LIMIT 1',\n name,\n );\n return result.rows.length > 0;\n }\n\n /**\n * Record that the backfill named `name` ran. Uses INSERT ... ON CONFLICT\n * so callers don't need to check `isApplied` first if they don't care\n * about distinguishing \"already applied\" from \"just applied\".\n */\n async recordApplied(\n name: string,\n options: { description?: string; packageName?: string } = {},\n ): Promise<void> {\n await this.queryBackfillTable(\n `INSERT INTO _smrt_backfills (name, description, package_name)\n VALUES (?, ?, ?)\n ON CONFLICT (name) DO NOTHING`,\n name,\n options.description ?? null,\n options.packageName ?? null,\n );\n }\n\n /**\n * Convenience wrapper: if `name` hasn't been applied, run `fn` and\n * record the result. Returns `{ ran, result }`:\n * - `{ ran: true, result: T }` — backfill was pending and `fn` ran\n * - `{ ran: false, result: null }` — backfill was already applied\n *\n * The discriminator avoids the `T | null` ambiguity an earlier shape had\n * (a backfill that legitimately returns `null` was indistinguishable\n * from \"already applied\"). Callers check `ran` to decide whether `fn`\n * executed; `result` carries whatever `fn` returned.\n *\n * This is the common pattern an app's migrate orchestration follows:\n * a list of `(name, runner)` pairs, each guarded by isApplied/run/record.\n *\n * **Single-runner only.** This is a check-then-act pattern with no\n * locking: two processes that call `runIfPending` for the same `name`\n * concurrently can both observe `isApplied === false`, both execute\n * `fn`, and only then race to `recordApplied`. The `ON CONFLICT DO\n * NOTHING` in `recordApplied` keeps the table clean but does NOT prevent\n * `fn` from running twice. Acceptable when backfills are run from a\n * single deploy-time migration step (the common case); if you need to\n * fan out backfills across multiple workers, wrap the call in your own\n * mutex (e.g. a Postgres advisory lock keyed by backfill name).\n *\n * **`fn` MUST be idempotent.** The order is `fn` → `recordApplied`, with\n * no surrounding transaction. If `fn` succeeds but `recordApplied`\n * throws (DB connection drops, transient error), the work has happened\n * but is not recorded — the next call sees `isApplied === false` and\n * re-runs `fn`. For non-idempotent work (slug rewrites that consume\n * source rows, one-shot lookups), wrap the entire call in a transaction\n * yourself or structure `fn` so a second execution is a no-op.\n */\n async runIfPending<T>(\n name: string,\n fn: () => Promise<T>,\n options: { description?: string; packageName?: string } = {},\n ): Promise<{ ran: false; result: null } | { ran: true; result: T }> {\n if (await this.isApplied(name)) return { ran: false, result: null };\n const result = await fn();\n await this.recordApplied(name, options);\n return { ran: true, result };\n }\n\n /** List every recorded backfill, oldest first. */\n async listApplied(): Promise<BackfillRecord[]> {\n const result = await this.queryBackfillTable(\n 'SELECT name, applied_at, description, package_name FROM _smrt_backfills ORDER BY applied_at ASC, name ASC',\n );\n return result.rows.map((row) => ({\n name: String(row.name ?? ''),\n appliedAt: normalizeAppliedAt(row.applied_at),\n description: row.description == null ? null : String(row.description),\n packageName: row.package_name == null ? null : String(row.package_name),\n }));\n }\n\n private async queryBackfillTable(sql: string, ...params: unknown[]) {\n await this.initialize();\n try {\n return await this.db.query(sql, ...params);\n } catch (error) {\n if (!isMissingBackfillTableError(error)) throw error;\n const inheritedRoot = inheritedInitializationByHandle.get(\n this.db as object,\n );\n this.resetInitialization(inheritedRoot);\n if (inheritedRoot) throw new BackfillTableUnavailableError(error);\n await this.initialize();\n return this.db.query(sql, ...params);\n }\n }\n\n private resetInitialization(inheritedRoot?: object): void {\n this.initializePromise = null;\n inheritedInitializationByHandle.delete(this.db as object);\n deleteSharedInitialization(databaseTargetIdentity(this.db));\n if (inheritedRoot) deleteSharedInitialization(inheritedRoot);\n }\n}\n\nfunction databaseTargetIdentity(db: DatabaseInterface): object {\n const client = db.client;\n return client !== null &&\n (typeof client === 'object' || typeof client === 'function')\n ? client\n : db;\n}\n\nfunction isRootDatabase(db: DatabaseInterface): boolean {\n return typeof db.beginTransaction === 'function';\n}\n\nfunction getSharedInitialization(target: object): Promise<void> | undefined {\n return initializationByObject.get(target);\n}\n\nfunction setSharedInitialization(\n target: object,\n initialization: Promise<void>,\n): void {\n initializationByObject.set(target, initialization);\n}\n\nfunction deleteSharedInitialization(target: object): void {\n initializationByObject.delete(target);\n}\n\nfunction isMissingBackfillTableError(error: unknown): boolean {\n const pending: unknown[] = [error];\n const seen = new Set<unknown>();\n const missingPattern =\n /(?:no such table|does not exist|doesn't exist|unknown table|not found)/iu;\n while (pending.length > 0) {\n const current = pending.pop();\n if (typeof current === 'string') {\n if (current.includes('_smrt_backfills') && missingPattern.test(current)) {\n return true;\n }\n continue;\n }\n if (!current || typeof current !== 'object' || seen.has(current)) continue;\n seen.add(current);\n if (\n current instanceof Error &&\n current.message.includes('_smrt_backfills') &&\n missingPattern.test(current.message)\n ) {\n return true;\n }\n const details = current as {\n cause?: unknown;\n context?: unknown;\n details?: unknown;\n originalError?: unknown;\n };\n pending.push(\n details.cause,\n details.context,\n details.details,\n details.originalError,\n );\n if (current instanceof AggregateError) pending.push(...current.errors);\n }\n return false;\n}\n\n/**\n * Convert a raw `applied_at` cell into an ISO 8601 string.\n *\n * Different DB drivers return this column in different shapes:\n * - Postgres `node-pg` returns a JS `Date` directly.\n * - SQLite drivers typically return the `CURRENT_TIMESTAMP` literal\n * `'YYYY-MM-DD HH:MM:SS'` (no `T`, no timezone). V8's `Date`\n * constructor parses this as local time, but that's\n * implementation-defined and other runtimes may return Invalid Date.\n * - A corrupted row could return any string.\n *\n * If the value parses to a valid Date we return `toISOString()`. If it\n * doesn't (or the column is null/empty), we fall back to the raw string —\n * better to surface a non-ISO timestamp than crash `listApplied()` with a\n * `RangeError` and take down the caller.\n */\nfunction normalizeAppliedAt(value: unknown): string {\n if (value == null || value === '') return '';\n const raw = String(value);\n const parsed = new Date(raw);\n if (Number.isNaN(parsed.getTime())) {\n return raw;\n }\n return parsed.toISOString();\n}\n"],"mappings":"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"}
@@ -241,23 +241,55 @@ export declare class SchemaComparer {
241
241
  */
242
242
  private detectRenameDataPending;
243
243
  /**
244
- * Live-data probe: does the column hold any non-null, non-empty value?
245
- * Used by {@link detectRenameDataPending}. Errors propagate to the
246
- * caller, which skips the pair rather than guessing (#2752).
247
- */
248
- private columnHasNonEmptyValue;
249
- /**
250
- * Live-data probe: are every one of a column's non-empty values
251
- * UUID-shaped ({@link CANONICAL_UUID_PATTERN})? PostgreSQL pushes the
252
- * check into the query with its regex operator; SQLite has no regex
253
- * operator, but its case-sensitive `GLOB` can still express the fixed
254
- * 36-character canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN}
255
- * against `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check), so
256
- * both engines run one server-side aggregate `count(*)` rather than
257
- * fetching every non-empty value into JS to test in a loop — important on
258
- * a production table with many rows (#2767 review).
259
- */
260
- private allNonEmptyValuesUuidShaped;
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 columnHasNonEmptyValue} already uses for
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
- let declaredHasData;
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 candidates = [];
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
- let orphanHasData;
1005
- try {
1006
- orphanHasData = await this.columnHasNonEmptyValue(tableName, orphanName);
1007
- } catch {
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: does the column hold any non-null, non-empty value?
1032
- * Used by {@link detectRenameDataPending}. Errors propagate to the
1033
- * caller, which skips the pair rather than guessing (#2752).
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 columnHasNonEmptyValue(tableName, colName) {
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: are every one of a column's non-empty values
1042
- * UUID-shaped ({@link CANONICAL_UUID_PATTERN})? PostgreSQL pushes the
1043
- * check into the query with its regex operator; SQLite has no regex
1044
- * operator, but its case-sensitive `GLOB` can still express the fixed
1045
- * 36-character canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN}
1046
- * against `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check), so
1047
- * both engines run one server-side aggregate `count(*)` rather than
1048
- * fetching every non-empty value into JS to test in a loop — important on
1049
- * a production table with many rows (#2767 review).
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 allNonEmptyValuesUuidShaped(tableName, colName) {
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
- if (this.engine === "postgres") {
1056
- const row = (await this.db.query(`SELECT count(*) AS invalid_count FROM ${quotedTable} WHERE ${nonEmptyPredicate} AND CAST(${quotedCol} AS TEXT) !~* '${CANONICAL_UUID_PATTERN}'`)).rows?.[0];
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 columnHasNonEmptyValue} already uses for
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