@happyvertical/smrt-core 0.47.0 → 0.47.2

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 (84) hide show
  1. package/AGENTS.md +2 -0
  2. package/agents/generators.md +19 -5
  3. package/agents/registry-snapshot.md +28 -0
  4. package/agents/schema-paths.md +12 -0
  5. package/agents/system-diagnostics.md +43 -0
  6. package/dist/browser.js +4 -2
  7. package/dist/change-feed.d.ts +8 -0
  8. package/dist/change-feed.d.ts.map +1 -1
  9. package/dist/change-feed.js +1 -1
  10. package/dist/change-feed.js.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +5 -2
  14. package/dist/manifest/static-manifest.js +1 -1
  15. package/dist/manifest/static-manifest.js.map +1 -1
  16. package/dist/manifest/store.js +1 -1
  17. package/dist/manifest.json +1 -1
  18. package/dist/migrations/differ.d.ts +78 -0
  19. package/dist/migrations/differ.d.ts.map +1 -1
  20. package/dist/migrations/differ.js +191 -1
  21. package/dist/migrations/differ.js.map +1 -1
  22. package/dist/migrations/generator.d.ts.map +1 -1
  23. package/dist/migrations/generator.js +2 -1
  24. package/dist/migrations/generator.js.map +1 -1
  25. package/dist/postgres-permissions.d.ts +3 -0
  26. package/dist/postgres-permissions.d.ts.map +1 -1
  27. package/dist/postgres-permissions.js +49 -6
  28. package/dist/postgres-permissions.js.map +1 -1
  29. package/dist/prebuild/index.d.ts.map +1 -1
  30. package/dist/prebuild/index.js +4 -3
  31. package/dist/prebuild/index.js.map +1 -1
  32. package/dist/registry/class-registration.d.ts.map +1 -1
  33. package/dist/registry/class-registration.js +3 -1
  34. package/dist/registry/class-registration.js.map +1 -1
  35. package/dist/registry/schema-builder.js +1 -1
  36. package/dist/registry.js +1 -1
  37. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  38. package/dist/scanner/manifest-generator.js +22 -1
  39. package/dist/scanner/manifest-generator.js.map +1 -1
  40. package/dist/schema/foreign-key-ddl.d.ts +18 -0
  41. package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
  42. package/dist/schema/foreign-key-ddl.js +15 -2
  43. package/dist/schema/foreign-key-ddl.js.map +1 -1
  44. package/dist/schema/foreign-key-orphan-report.d.ts +57 -0
  45. package/dist/schema/foreign-key-orphan-report.d.ts.map +1 -0
  46. package/dist/schema/foreign-key-orphan-report.js +142 -0
  47. package/dist/schema/foreign-key-orphan-report.js.map +1 -0
  48. package/dist/schema/index.d.ts +1 -0
  49. package/dist/schema/index.d.ts.map +1 -1
  50. package/dist/schema/index.js +2 -1
  51. package/dist/schema/live-parity.d.ts +8 -1
  52. package/dist/schema/live-parity.d.ts.map +1 -1
  53. package/dist/schema/live-parity.js +98 -2
  54. package/dist/schema/live-parity.js.map +1 -1
  55. package/dist/schema/types.d.ts +11 -1
  56. package/dist/schema/types.d.ts.map +1 -1
  57. package/dist/schema.js +2 -1
  58. package/dist/smrt-knowledge.json +22 -10
  59. package/dist/system/diagnostics.d.ts +299 -0
  60. package/dist/system/diagnostics.d.ts.map +1 -0
  61. package/dist/system/diagnostics.js +530 -0
  62. package/dist/system/diagnostics.js.map +1 -0
  63. package/dist/system/index.d.ts +2 -0
  64. package/dist/system/index.d.ts.map +1 -1
  65. package/dist/system/index.js +3 -1
  66. package/dist/system/registry-snapshot.d.ts +112 -0
  67. package/dist/system/registry-snapshot.d.ts.map +1 -0
  68. package/dist/system/registry-snapshot.js +171 -0
  69. package/dist/system/registry-snapshot.js.map +1 -0
  70. package/dist/testing/database.d.ts.map +1 -1
  71. package/dist/testing/database.js +10 -2
  72. package/dist/testing/database.js.map +1 -1
  73. package/dist/utils/scanner-module.d.ts +16 -0
  74. package/dist/utils/scanner-module.d.ts.map +1 -1
  75. package/dist/vite-plugin/index.d.ts +34 -1
  76. package/dist/vite-plugin/index.d.ts.map +1 -1
  77. package/dist/vite-plugin/index.js +85 -5
  78. package/dist/vite-plugin/index.js.map +1 -1
  79. package/dist/vite-plugin/web-collections.d.ts +6 -0
  80. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  81. package/dist/vite-plugin/web-collections.js +9 -4
  82. package/dist/vite-plugin/web-collections.js.map +1 -1
  83. package/dist/vite-plugin.js +2 -2
  84. package/package.json +10 -10
@@ -1 +1 @@
1
- {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/migrations/generator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/migrations/generator.ts"],"names":[],"mappings":"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"}
@@ -230,7 +230,8 @@ ${downStatementsStr}
230
230
  break;
231
231
  case "orphan_column":
232
232
  case "orphan_index":
233
- up.push(`-- ${change.advisory?.severity === "warning" ? "WARNING" : "NOTE"}: ${change.type === "orphan_column" ? "Orphan column" : "Orphan unique constraint"} ${change.table}.${change.name} (${change.mismatch?.actual ?? "unknown shape"})`);
233
+ case "rename_data_pending":
234
+ up.push(`-- ${change.advisory?.severity === "warning" ? "WARNING" : "NOTE"}: ${change.type === "orphan_column" ? "Orphan column" : change.type === "orphan_index" ? "Orphan unique constraint" : "Rename data pending"} ${change.table}.${change.name} (${change.mismatch?.actual ?? "unknown shape"})`);
234
235
  if (change.advisory?.message) up.push(`-- ${change.advisory.message}`);
235
236
  for (const suggestion of change.advisory?.suggestedSql ?? []) up.push(`-- suggested: ${suggestion}`);
236
237
  break;
@@ -1 +1 @@
1
- {"version":3,"file":"generator.js","names":[],"sources":["../../src/migrations/generator.ts"],"sourcesContent":["/**\n * Migration Generator\n *\n * Generates migration files from schema diffs.\n * Supports both SQL and TypeScript formats.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { getDDLStrategy } from '../schema/ddl/index.js';\nimport { materializeManifestDDLForEngine } from '../schema/ddl/materialize-manifest.js';\nimport {\n planForeignKeyCreation,\n renderDeferredForeignKeyDrop,\n} from '../schema/foreign-key-planner.js';\nimport { cachedDdlTableConstraints } from '../schema/manifest-schema.js';\nimport type {\n MigrationDefinition,\n SchemaChange,\n SchemaDefinition,\n SchemaDiff,\n} from '../schema/types.js';\nimport { renderIndexTarget } from '../schema/utils.js';\nimport { computeChecksum } from './checksum.js';\nimport type { DatabaseEngine } from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Options for migration generation\n */\nexport interface GenerateMigrationOptions {\n /** Migration name (without sequence number) */\n name: string;\n /** Output format */\n format?: 'sql' | 'typescript';\n /** Database engine for engine-specific SQL */\n engine?: DatabaseEngine;\n /** Include generated DOWN statements */\n includeDown?: boolean;\n /** Package name for metadata */\n packageName?: string;\n /** Description for the migration */\n description?: string;\n}\n\n/**\n * Result of migration generation\n */\nexport interface GeneratedMigration {\n /** Full migration ID (e.g., '0001_add_users') */\n id: string;\n /** Filename (e.g., '0001_add_users.sql') */\n filename: string;\n /** File content */\n content: string;\n /** UP SQL statements */\n up: string[];\n /** DOWN SQL statements (if includeDown is true) */\n down: string[];\n /** Checksum of the migration */\n checksum: string;\n}\n\n/**\n * MigrationGenerator class for creating migration files\n */\nexport class MigrationGenerator {\n private engine: DatabaseEngine;\n private format: 'sql' | 'typescript';\n private includeDown: boolean;\n private packageName?: string;\n private materializeStructuredSchema: boolean;\n\n constructor(\n options: {\n engine?: DatabaseEngine;\n format?: 'sql' | 'typescript';\n includeDown?: boolean;\n packageName?: string;\n /**\n * Render new tables from their structured columns through the engine's\n * DDL strategy (default, `true`) — the same renderer `db:migrate` uses,\n * so column types, indexes and triggers match what the differ expects\n * on the next run.\n *\n * @deprecated Passing `false` re-enables the retired cached-`ddl` string\n * path for the CREATE TABLE only (#2358): it emits the manifest's\n * engine-neutral CREATE TABLE with only bare PostgreSQL `TIMESTAMP`\n * rewritten, so `REAL`/`JSON` drift on PostgreSQL. The table's indexes\n * and triggers are still rendered through the engine strategy either\n * way — the flag never suppresses them. Schemas that expose no\n * structured columns always fall back to their cached `ddl` regardless\n * of this flag.\n */\n materializeStructuredSchema?: boolean;\n } = {},\n ) {\n this.engine = options.engine || 'sqlite';\n this.format = options.format || 'sql';\n this.includeDown = options.includeDown ?? true;\n this.packageName = options.packageName;\n this.materializeStructuredSchema =\n options.materializeStructuredSchema ?? true;\n }\n\n /**\n * Generate a migration from a schema diff\n */\n generateFromDiff(\n diff: SchemaDiff,\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n const upStatements: string[] = [];\n const downStatements: string[] = [];\n const addedTableDownStatements: string[] = [];\n\n // Handle new tables: CREATE TABLE plus the table's indexes and triggers,\n // exactly as the migration orchestrator emits them (#2358). The differ\n // only produces `add_index` changes for tables that already exist, so\n // there is no double emission against `diff.changes`.\n const tablePlan = planForeignKeyCreation(diff.added_tables, this.engine);\n for (const schema of tablePlan.schemas) {\n upStatements.push(this.generateCreateTable(schema));\n upStatements.push(...this.generateTableIndexes(schema));\n upStatements.push(...this.generateTableTriggers(schema));\n }\n upStatements.push(...tablePlan.deferredStatements);\n if (this.includeDown || options.includeDown) {\n if (this.engine === 'sqlite' && tablePlan.cyclicTables.length > 0) {\n // SQLite cannot drop either side of a populated mutual FK cycle with\n // immediate checks. Defer checks until the transaction commits, after\n // every member has been removed.\n addedTableDownStatements.push('PRAGMA defer_foreign_keys = ON');\n }\n // PostgreSQL mutual cycles are made acyclic first; every table can then\n // be dropped in the exact reverse of the parent-first creation order.\n addedTableDownStatements.push(\n ...[...tablePlan.deferredConstraints]\n .reverse()\n .map(({ table, foreignKey }) =>\n renderDeferredForeignKeyDrop(table, foreignKey),\n ),\n );\n addedTableDownStatements.push(\n ...[...tablePlan.schemas]\n .reverse()\n .map((schema) => this.generateDropTable(schema.tableName)),\n );\n }\n\n // Handle column and index changes\n for (const change of diff.changes) {\n const { up, down } = this.generateChangeSQL(change);\n upStatements.push(...up);\n if ((this.includeDown || options.includeDown) && down.length > 0) {\n downStatements.push(...down);\n }\n }\n\n // Changes are applied after table creation, so undo them before removing\n // the new tables they may reference. The table block is itself already\n // ordered as deferred-constraint drops followed by child-first drops.\n downStatements.push(...addedTableDownStatements);\n\n // Handle dropped tables (already in SQL form if present)\n for (const tableName of diff.dropped_tables) {\n upStatements.push(this.generateDropTable(tableName));\n // No automatic DOWN for drops - too dangerous\n }\n\n return this.generateMigration(upStatements, downStatements, options);\n }\n\n /**\n * Generate a migration from explicit SQL statements\n */\n generateFromSQL(\n upStatements: string[],\n downStatements: string[],\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n return this.generateMigration(upStatements, downStatements, options);\n }\n\n /**\n * Generate an empty migration template\n */\n generateEmpty(options: GenerateMigrationOptions): GeneratedMigration {\n return this.generateMigration([], [], options);\n }\n\n /**\n * Core migration generation logic\n */\n private generateMigration(\n upStatements: string[],\n downStatements: string[],\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n const id = options.name;\n const description = options.description || `Migration: ${options.name}`;\n const format = options.format || this.format;\n const filename = `${id}.${format === 'sql' ? 'sql' : 'ts'}`;\n const checksum = computeChecksum(upStatements);\n\n let content: string;\n if (format === 'sql') {\n content = this.generateSQLFile(\n id,\n description,\n upStatements,\n downStatements,\n );\n } else {\n content = this.generateTypeScriptFile(\n id,\n description,\n upStatements,\n downStatements,\n );\n }\n\n return {\n id,\n filename,\n content,\n up: upStatements,\n down: downStatements,\n checksum,\n };\n }\n\n /**\n * Generate SQL migration file content\n */\n private generateSQLFile(\n id: string,\n description: string,\n upStatements: string[],\n downStatements: string[],\n ): string {\n const lines: string[] = [\n `-- @id: ${id}`,\n `-- @description: ${description}`,\n `-- @generated: ${new Date().toISOString()}`,\n ];\n\n if (this.packageName) {\n lines.push(`-- @package: ${this.packageName}`);\n }\n\n lines.push('');\n lines.push('-- @up');\n\n if (upStatements.length === 0) {\n lines.push('-- Add your UP statements here');\n } else {\n for (const stmt of upStatements) {\n lines.push(this.terminateStatement(stmt));\n }\n }\n\n lines.push('');\n lines.push('-- @down');\n\n if (downStatements.length === 0) {\n lines.push('-- Add your DOWN statements here');\n } else {\n for (const stmt of downStatements) {\n lines.push(this.terminateStatement(stmt));\n }\n }\n\n lines.push('');\n return lines.join('\\n');\n }\n\n /**\n * Generate TypeScript migration file content\n */\n private generateTypeScriptFile(\n id: string,\n description: string,\n upStatements: string[],\n downStatements: string[],\n ): string {\n const upStatementsStr =\n upStatements.length > 0\n ? upStatements\n .map((s) => ` await db.query(\\`${this.escapeTemplate(s)}\\`);`)\n .join('\\n')\n : ' // Add your UP statements here';\n\n const downStatementsStr =\n downStatements.length > 0\n ? downStatements\n .map((s) => ` await db.query(\\`${this.escapeTemplate(s)}\\`);`)\n .join('\\n')\n : ' // Add your DOWN statements here';\n\n return `/**\n * Migration: ${id}\n * ${description}\n *\n * Generated: ${new Date().toISOString()}\n */\n\nimport type { Migration } from '@happyvertical/smrt-core';\n\nexport default {\n id: '${id}',\n description: '${description.replace(/'/g, \"\\\\'\")}',\n version: '1.0.0',\n\n up: async (db) => {\n${upStatementsStr}\n },\n\n down: async (db) => {\n${downStatementsStr}\n },\n} satisfies Migration;\n`;\n }\n\n /**\n * Generate CREATE TABLE SQL from schema definition\n */\n private generateCreateTable(schema: SchemaDefinition): string {\n if (Object.keys(schema.columns).length === 0 && schema.ddl?.trim()) {\n // Structured materialization cannot preserve constraints or vendor SQL\n // when an older/custom manifest exposes only executable DDL. Retain the\n // only valid representation even when materialization was requested.\n return materializeManifestDDLForEngine(schema.ddl, this.engine);\n }\n\n if (!this.materializeStructuredSchema && schema.ddl?.trim()) {\n // Deprecated opt-out (#2358): the cached string is engine-neutral and\n // index-less; only bare PostgreSQL TIMESTAMP is rewritten.\n return materializeManifestDDLForEngine(schema.ddl, this.engine);\n }\n\n // The manifest's pre-generated `ddl` is deliberately engine-neutral and\n // may contain abstract TIMESTAMP/JSON/UUID types. Materialize the\n // structured columns through the selected engine strategy; executing the\n // cached string on PostgreSQL would otherwise bypass TIMESTAMPTZ and lose\n // JavaScript Date offsets (#2069). This matches the migration orchestrator\n // and also prevents JSON/REAL/UUID drift after a create.\n if (schema.ddl?.trim()) {\n // Table-level constraints that live only in a hand-authored cached\n // string cannot be represented structurally and are dropped — as\n // `db:migrate` already drops them. Say so instead of losing them\n // silently (#2358).\n const constraints = cachedDdlTableConstraints(schema.ddl);\n if (constraints.length > 0) {\n logger.warn(\n `[MigrationGenerator] cached ddl for table \"${schema.tableName}\" declares ` +\n `${constraints.length} table constraint(s) that structured columns cannot ` +\n `carry (${constraints.join('; ')}); the structured schema is authoritative ` +\n `and they are not rendered (#2358).`,\n );\n }\n }\n return getDDLStrategy(this.engine).generateCreateTable(schema);\n }\n\n /**\n * Generate the CREATE INDEX statements for a new table through the engine\n * strategy (partial `WHERE`, JSON-path targets, inline-UNIQUE engines).\n */\n private generateTableIndexes(schema: SchemaDefinition): string[] {\n return getDDLStrategy(this.engine).generateIndexes(schema);\n }\n\n /**\n * Generate the CREATE TRIGGER statements for a new table (engines without\n * trigger support return none).\n */\n private generateTableTriggers(schema: SchemaDefinition): string[] {\n return getDDLStrategy(this.engine).generateTriggers(schema);\n }\n\n /**\n * Generate DROP TABLE SQL\n */\n private generateDropTable(tableName: string): string {\n return `DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`;\n }\n\n /**\n * Generate SQL for a schema change\n */\n private generateChangeSQL(change: SchemaChange): {\n up: string[];\n down: string[];\n } {\n const up: string[] = [];\n const down: string[] = [];\n const sqlStatements =\n change.sqlStatements ?? (change.sql ? [change.sql] : []);\n\n switch (change.type) {\n case 'add_column':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.column && change.name) {\n up.push(\n this.generateAddColumn(change.table, change.name, change.column),\n );\n }\n if (change.name) {\n down.push(this.generateDropColumn(change.table, change.name));\n }\n break;\n\n case 'drop_column':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.name) {\n up.push(this.generateDropColumn(change.table, change.name));\n }\n // No automatic DOWN for drops\n break;\n\n case 'add_index':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.index) {\n up.push(this.generateAddIndex(change.table, change.index));\n }\n if (change.name) {\n down.push(this.generateDropIndex(change.name));\n }\n break;\n\n case 'add_foreign_key':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.advisory) {\n up.push(\n `-- ${change.advisory.severity === 'warning' ? 'WARNING' : 'NOTE'}: Foreign-key constraint ${change.table}.${change.name ?? 'unknown'}`,\n );\n if (change.advisory.message) {\n up.push(`-- ${change.advisory.message}`);\n }\n for (const suggestion of change.advisory.suggestedSql ?? []) {\n up.push(`-- suggested: ${suggestion}`);\n }\n }\n // Constraint rollback is intentionally manual: names on older live\n // databases may not match the deterministic current generator name.\n break;\n\n case 'drop_foreign_key':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.advisory) {\n up.push(\n `-- ${change.advisory.severity === 'warning' ? 'WARNING' : 'NOTE'}: Foreign-key constraint ${change.table}.${change.name ?? 'unknown'}`,\n );\n up.push(`-- ${change.advisory.message}`);\n }\n // Re-adding a removed constraint requires a fresh orphan check and is\n // therefore intentionally not generated as an automatic rollback.\n break;\n\n case 'drop_index':\n if (change.name) {\n up.push(this.generateDropIndex(change.name));\n }\n // No automatic DOWN for index drops\n break;\n\n case 'type_upgrade':\n // Type upgrades are safe conversions that SMRT can handle automatically\n // The SQL is generated by the differ based on engine-specific requirements\n if (sqlStatements.length > 0) {\n for (const sql of sqlStatements) {\n // Skip comment-only SQL (indicates a no-op migration)\n if (!sql.startsWith('--')) {\n up.push(sql);\n }\n }\n }\n // No automatic DOWN for type changes - could lose data\n break;\n\n case 'type_mismatch':\n // Type mismatches require manual intervention\n up.push(`-- WARNING: Type mismatch for ${change.table}.${change.name}`);\n up.push(\n `-- Expected: ${change.mismatch?.expected}, Actual: ${change.mismatch?.actual}`,\n );\n up.push(`-- Manual migration required`);\n break;\n\n case 'alter_column':\n // Nullability/default repairs (#2369). Executable statements come\n // from the differ (backfill + ALTER COLUMN); comment-only SQL marks\n // an in-place limitation (SQLite) and is surfaced as a note. Report-\n // only advisories (relaxations not opted into) are not emitted.\n if (change.advisory && sqlStatements.length === 0) {\n break;\n }\n for (const sql of sqlStatements) {\n up.push(sql);\n }\n // No automatic DOWN: the previous nullability/default is not\n // captured with enough fidelity to restore it safely.\n break;\n\n case 'orphan_column':\n case 'orphan_index':\n // Report-only: surface the advisory as comments so a generated\n // migration file documents the drift without acting on it.\n up.push(\n `-- ${change.advisory?.severity === 'warning' ? 'WARNING' : 'NOTE'}: ${change.type === 'orphan_column' ? 'Orphan column' : 'Orphan unique constraint'} ${change.table}.${change.name} (${change.mismatch?.actual ?? 'unknown shape'})`,\n );\n if (change.advisory?.message) {\n up.push(`-- ${change.advisory.message}`);\n }\n for (const suggestion of change.advisory?.suggestedSql ?? []) {\n up.push(`-- suggested: ${suggestion}`);\n }\n break;\n\n default:\n // Unknown change type\n break;\n }\n\n return { up, down };\n }\n\n /**\n * Generate ADD COLUMN SQL\n */\n private generateAddColumn(\n tableName: string,\n colName: string,\n colDef: import('../schema/types.js').ColumnDefinition,\n ): string {\n const parts: string[] = [this.quoteIdentifier(colName), colDef.type];\n\n if (colDef.notNull) {\n parts.push('NOT NULL');\n }\n if (colDef.unique) {\n parts.push('UNIQUE');\n }\n if (colDef.defaultValue !== undefined) {\n parts.push(`DEFAULT ${this.formatDefaultValue(colDef.defaultValue)}`);\n }\n\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${parts.join(' ')}`;\n }\n\n /**\n * Generate DROP COLUMN SQL\n */\n private generateDropColumn(tableName: string, colName: string): string {\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP COLUMN ${this.quoteIdentifier(colName)}`;\n }\n\n /**\n * Generate ADD INDEX SQL\n */\n private generateAddIndex(\n tableName: string,\n index: import('../schema/types.js').IndexDefinition,\n ): string {\n const uniqueStr = index.unique ? 'UNIQUE ' : '';\n const target = renderIndexTarget(index, this.engine);\n // Partial-index predicate. DuckDB (and the JSON adapter backed by it)\n // rejects partial indexes, so the declaration degrades to a full index\n // there — matching the differ and the DuckDB DDL strategy.\n const whereClause =\n this.engine !== 'duckdb' && index.where ? ` WHERE ${index.where}` : '';\n\n if (this.engine === 'postgres') {\n // PostgreSQL: use CONCURRENTLY for production safety\n return `CREATE ${uniqueStr}INDEX CONCURRENTLY IF NOT EXISTS ${this.quoteIdentifier(index.name)} ON ${this.quoteIdentifier(tableName)} (${target})${whereClause}`;\n }\n\n return `CREATE ${uniqueStr}INDEX IF NOT EXISTS ${this.quoteIdentifier(index.name)} ON ${this.quoteIdentifier(tableName)} (${target})${whereClause}`;\n }\n\n /**\n * Generate DROP INDEX SQL\n */\n private generateDropIndex(indexName: string): string {\n if (this.engine === 'postgres') {\n return `DROP INDEX CONCURRENTLY IF EXISTS ${this.quoteIdentifier(indexName)}`;\n }\n return `DROP INDEX IF EXISTS ${this.quoteIdentifier(indexName)}`;\n }\n\n /**\n * Quote a SQL identifier\n */\n private quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n }\n\n /**\n * Format a default value for SQL\n */\n private formatDefaultValue(value: unknown): string {\n if (value === null) return 'NULL';\n if (typeof value === 'string') return `'${value.replace(/'/g, \"''\")}'`;\n if (typeof value === 'boolean') return value ? '1' : '0';\n if (typeof value === 'number') return String(value);\n return String(value);\n }\n\n /**\n * Terminate a statement with exactly one `;` for the SQL file format.\n *\n * Strategy-rendered statements (`generateCreateTable`, `generateIndexes`,\n * `generateTriggers`) already end with `;`, while differ-produced ALTERs do\n * not — appending unconditionally wrote `;;` (#2406 review).\n */\n private terminateStatement(stmt: string): string {\n return stmt.trimEnd().endsWith(';') ? stmt : `${stmt};`;\n }\n\n /**\n * Escape template literal content\n */\n private escapeTemplate(str: string): string {\n return str.replace(/`/g, '\\\\`').replace(/\\$/g, '\\\\$');\n }\n}\n\n/**\n * Generate a migration definition from UP and DOWN statements\n */\nexport function createMigrationDefinition(\n id: string,\n upStatements: string[],\n downStatements: string[] = [],\n options: {\n description?: string;\n version?: string;\n packageName?: string;\n sourceFile?: string;\n } = {},\n): MigrationDefinition {\n return {\n id,\n description: options.description || `Migration: ${id}`,\n version: options.version || '1.0.0',\n up: upStatements,\n down: downStatements,\n package_name: options.packageName,\n source_file: options.sourceFile,\n };\n}\n\n/**\n * Generate a sequence number for a new migration\n */\nexport function generateMigrationSequence(existingIds: string[]): string {\n const sequences = existingIds\n .map((id) => {\n const match = id.match(/^(\\d+)_/);\n return match ? parseInt(match[1], 10) : 0;\n })\n .filter((n) => !Number.isNaN(n));\n\n const maxSequence = sequences.length > 0 ? Math.max(...sequences) : 0;\n return String(maxSequence + 1).padStart(4, '0');\n}\n\n/**\n * Generate a timestamp-based migration ID\n */\nexport function generateMigrationTimestamp(): string {\n const now = new Date();\n return [\n now.getFullYear(),\n String(now.getMonth() + 1).padStart(2, '0'),\n String(now.getDate()).padStart(2, '0'),\n String(now.getHours()).padStart(2, '0'),\n String(now.getMinutes()).padStart(2, '0'),\n String(now.getSeconds()).padStart(2, '0'),\n ].join('');\n}\n"],"mappings":";;;;;;;;;;;;;;;AAyBA,IAAM,SAAS,aAAa,EAAE,OAAO,OAAO,CAAC;;;;AAyC7C,IAAa,qBAAb,MAAgC;CAC9B;CACA;CACA;CACA;CACA;CAEA,YACE,UAqBI,CAAC,GACL;EACA,KAAK,SAAS,QAAQ,UAAU;EAChC,KAAK,SAAS,QAAQ,UAAU;EAChC,KAAK,cAAc,QAAQ,eAAe;EAC1C,KAAK,cAAc,QAAQ;EAC3B,KAAK,8BACH,QAAQ,+BAA+B;CAC3C;;;;CAKA,iBACE,MACA,SACoB;EACpB,MAAM,eAAyB,CAAC;EAChC,MAAM,iBAA2B,CAAC;EAClC,MAAM,2BAAqC,CAAC;EAM5C,MAAM,YAAY,uBAAuB,KAAK,cAAc,KAAK,MAAM;EACvE,KAAK,MAAM,UAAU,UAAU,SAAS;GACtC,aAAa,KAAK,KAAK,oBAAoB,MAAM,CAAC;GAClD,aAAa,KAAK,GAAG,KAAK,qBAAqB,MAAM,CAAC;GACtD,aAAa,KAAK,GAAG,KAAK,sBAAsB,MAAM,CAAC;EACzD;EACA,aAAa,KAAK,GAAG,UAAU,kBAAkB;EACjD,IAAI,KAAK,eAAe,QAAQ,aAAa;GAC3C,IAAI,KAAK,WAAW,YAAY,UAAU,aAAa,SAAS,GAI9D,yBAAyB,KAAK,gCAAgC;GAIhE,yBAAyB,KACvB,GAAG,CAAC,GAAG,UAAU,mBAAmB,CAAC,CAClC,QAAQ,CAAC,CACT,KAAK,EAAE,OAAO,iBACb,6BAA6B,OAAO,UAAU,CAChD,CACJ;GACA,yBAAyB,KACvB,GAAG,CAAC,GAAG,UAAU,OAAO,CAAC,CACtB,QAAQ,CAAC,CACT,KAAK,WAAW,KAAK,kBAAkB,OAAO,SAAS,CAAC,CAC7D;EACF;EAGA,KAAK,MAAM,UAAU,KAAK,SAAS;GACjC,MAAM,EAAE,IAAI,SAAS,KAAK,kBAAkB,MAAM;GAClD,aAAa,KAAK,GAAG,EAAE;GACvB,KAAK,KAAK,eAAe,QAAQ,gBAAgB,KAAK,SAAS,GAC7D,eAAe,KAAK,GAAG,IAAI;EAE/B;EAKA,eAAe,KAAK,GAAG,wBAAwB;EAG/C,KAAK,MAAM,aAAa,KAAK,gBAC3B,aAAa,KAAK,KAAK,kBAAkB,SAAS,CAAC;EAIrD,OAAO,KAAK,kBAAkB,cAAc,gBAAgB,OAAO;CACrE;;;;CAKA,gBACE,cACA,gBACA,SACoB;EACpB,OAAO,KAAK,kBAAkB,cAAc,gBAAgB,OAAO;CACrE;;;;CAKA,cAAc,SAAuD;EACnE,OAAO,KAAK,kBAAkB,CAAC,GAAG,CAAC,GAAG,OAAO;CAC/C;;;;CAKA,kBACE,cACA,gBACA,SACoB;EACpB,MAAM,KAAK,QAAQ;EACnB,MAAM,cAAc,QAAQ,eAAe,cAAc,QAAQ;EACjE,MAAM,SAAS,QAAQ,UAAU,KAAK;EACtC,MAAM,WAAW,GAAG,GAAG,GAAG,WAAW,QAAQ,QAAQ;EACrD,MAAM,WAAW,gBAAgB,YAAY;EAE7C,IAAI;EACJ,IAAI,WAAW,OACb,UAAU,KAAK,gBACb,IACA,aACA,cACA,cACF;OAEA,UAAU,KAAK,uBACb,IACA,aACA,cACA,cACF;EAGF,OAAO;GACL;GACA;GACA;GACA,IAAI;GACJ,MAAM;GACN;EACF;CACF;;;;CAKA,gBACE,IACA,aACA,cACA,gBACQ;EACR,MAAM,QAAkB;GACtB,WAAW;GACX,oBAAoB;GACpB,mCAAkB,IAAI,KAAK,EAAA,CAAE,YAAY;EAC3C;EAEA,IAAI,KAAK,aACP,MAAM,KAAK,gBAAgB,KAAK,aAAa;EAG/C,MAAM,KAAK,EAAE;EACb,MAAM,KAAK,QAAQ;EAEnB,IAAI,aAAa,WAAW,GAC1B,MAAM,KAAK,gCAAgC;OAE3C,KAAK,MAAM,QAAQ,cACjB,MAAM,KAAK,KAAK,mBAAmB,IAAI,CAAC;EAI5C,MAAM,KAAK,EAAE;EACb,MAAM,KAAK,UAAU;EAErB,IAAI,eAAe,WAAW,GAC5B,MAAM,KAAK,kCAAkC;OAE7C,KAAK,MAAM,QAAQ,gBACjB,MAAM,KAAK,KAAK,mBAAmB,IAAI,CAAC;EAI5C,MAAM,KAAK,EAAE;EACb,OAAO,MAAM,KAAK,IAAI;CACxB;;;;CAKA,uBACE,IACA,aACA,cACA,gBACQ;EACR,MAAM,kBACJ,aAAa,SAAS,IAClB,aACG,KAAK,MAAM,0BAA0B,KAAK,eAAe,CAAC,EAAE,KAAK,CAAC,CAClE,KAAK,IAAI,IACZ;EAEN,MAAM,oBACJ,eAAe,SAAS,IACpB,eACG,KAAK,MAAM,0BAA0B,KAAK,eAAe,CAAC,EAAE,KAAK,CAAC,CAClE,KAAK,IAAI,IACZ;EAEN,OAAO;gBACK,GAAG;KACd,YAAY;;iCAED,IAAI,KAAK,EAAA,CAAE,YAAY,EAAE;;;;;;SAMhC,GAAG;kBACM,YAAY,QAAQ,MAAM,KAAK,EAAE;;;;EAIjD,gBAAgB;;;;EAIhB,kBAAkB;;;;CAIlB;;;;CAKA,oBAA4B,QAAkC;EAC5D,IAAI,OAAO,KAAK,OAAO,OAAO,CAAC,CAAC,WAAW,KAAK,OAAO,KAAK,KAAK,GAI/D,OAAO,gCAAgC,OAAO,KAAK,KAAK,MAAM;EAGhE,IAAI,CAAC,KAAK,+BAA+B,OAAO,KAAK,KAAK,GAGxD,OAAO,gCAAgC,OAAO,KAAK,KAAK,MAAM;EAShE,IAAI,OAAO,KAAK,KAAK,GAAG;GAKtB,MAAM,cAAc,0BAA0B,OAAO,GAAG;GACxD,IAAI,YAAY,SAAS,GACvB,OAAO,KACL,8CAA8C,OAAO,UAAU,aAC1D,YAAY,OAAO,6DACZ,YAAY,KAAK,IAAI,EAAE,6EAErC;EAEJ;EACA,OAAO,eAAe,KAAK,MAAM,CAAC,CAAC,oBAAoB,MAAM;CAC/D;;;;;CAMA,qBAA6B,QAAoC;EAC/D,OAAO,eAAe,KAAK,MAAM,CAAC,CAAC,gBAAgB,MAAM;CAC3D;;;;;CAMA,sBAA8B,QAAoC;EAChE,OAAO,eAAe,KAAK,MAAM,CAAC,CAAC,iBAAiB,MAAM;CAC5D;;;;CAKA,kBAA0B,WAA2B;EACnD,OAAO,wBAAwB,KAAK,gBAAgB,SAAS;CAC/D;;;;CAKA,kBAA0B,QAGxB;EACA,MAAM,KAAe,CAAC;EACtB,MAAM,OAAiB,CAAC;EACxB,MAAM,gBACJ,OAAO,kBAAkB,OAAO,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;EAExD,QAAQ,OAAO,MAAf;GACE,KAAK;IACH,IAAI,cAAc,SAAS,GACzB,GAAG,KAAK,GAAG,aAAa;SACnB,IAAI,OAAO,UAAU,OAAO,MACjC,GAAG,KACD,KAAK,kBAAkB,OAAO,OAAO,OAAO,MAAM,OAAO,MAAM,CACjE;IAEF,IAAI,OAAO,MACT,KAAK,KAAK,KAAK,mBAAmB,OAAO,OAAO,OAAO,IAAI,CAAC;IAE9D;GAEF,KAAK;IACH,IAAI,cAAc,SAAS,GACzB,GAAG,KAAK,GAAG,aAAa;SACnB,IAAI,OAAO,MAChB,GAAG,KAAK,KAAK,mBAAmB,OAAO,OAAO,OAAO,IAAI,CAAC;IAG5D;GAEF,KAAK;IACH,IAAI,cAAc,SAAS,GACzB,GAAG,KAAK,GAAG,aAAa;SACnB,IAAI,OAAO,OAChB,GAAG,KAAK,KAAK,iBAAiB,OAAO,OAAO,OAAO,KAAK,CAAC;IAE3D,IAAI,OAAO,MACT,KAAK,KAAK,KAAK,kBAAkB,OAAO,IAAI,CAAC;IAE/C;GAEF,KAAK;IACH,IAAI,cAAc,SAAS,GACzB,GAAG,KAAK,GAAG,aAAa;SACnB,IAAI,OAAO,UAAU;KAC1B,GAAG,KACD,MAAM,OAAO,SAAS,aAAa,YAAY,YAAY,OAAO,2BAA2B,OAAO,MAAM,GAAG,OAAO,QAAQ,WAC9H;KACA,IAAI,OAAO,SAAS,SAClB,GAAG,KAAK,MAAM,OAAO,SAAS,SAAS;KAEzC,KAAK,MAAM,cAAc,OAAO,SAAS,gBAAgB,CAAC,GACxD,GAAG,KAAK,iBAAiB,YAAY;IAEzC;IAGA;GAEF,KAAK;IACH,IAAI,cAAc,SAAS,GACzB,GAAG,KAAK,GAAG,aAAa;SACnB,IAAI,OAAO,UAAU;KAC1B,GAAG,KACD,MAAM,OAAO,SAAS,aAAa,YAAY,YAAY,OAAO,2BAA2B,OAAO,MAAM,GAAG,OAAO,QAAQ,WAC9H;KACA,GAAG,KAAK,MAAM,OAAO,SAAS,SAAS;IACzC;IAGA;GAEF,KAAK;IACH,IAAI,OAAO,MACT,GAAG,KAAK,KAAK,kBAAkB,OAAO,IAAI,CAAC;IAG7C;GAEF,KAAK;IAGH,IAAI,cAAc,SAAS;UACpB,MAAM,OAAO,eAEhB,IAAI,CAAC,IAAI,WAAW,IAAI,GACtB,GAAG,KAAK,GAAG;IAAA;IAKjB;GAEF,KAAK;IAEH,GAAG,KAAK,iCAAiC,OAAO,MAAM,GAAG,OAAO,MAAM;IACtE,GAAG,KACD,gBAAgB,OAAO,UAAU,SAAS,YAAY,OAAO,UAAU,QACzE;IACA,GAAG,KAAK,8BAA8B;IACtC;GAEF,KAAK;IAKH,IAAI,OAAO,YAAY,cAAc,WAAW,GAC9C;IAEF,KAAK,MAAM,OAAO,eAChB,GAAG,KAAK,GAAG;IAIb;GAEF,KAAK;GACL,KAAK;IAGH,GAAG,KACD,MAAM,OAAO,UAAU,aAAa,YAAY,YAAY,OAAO,IAAI,OAAO,SAAS,kBAAkB,kBAAkB,2BAA2B,GAAG,OAAO,MAAM,GAAG,OAAO,KAAK,IAAI,OAAO,UAAU,UAAU,gBAAgB,EACtO;IACA,IAAI,OAAO,UAAU,SACnB,GAAG,KAAK,MAAM,OAAO,SAAS,SAAS;IAEzC,KAAK,MAAM,cAAc,OAAO,UAAU,gBAAgB,CAAC,GACzD,GAAG,KAAK,iBAAiB,YAAY;IAEvC;GAEF,SAEE;EACJ;EAEA,OAAO;GAAE;GAAI;EAAK;CACpB;;;;CAKA,kBACE,WACA,SACA,QACQ;EACR,MAAM,QAAkB,CAAC,KAAK,gBAAgB,OAAO,GAAG,OAAO,IAAI;EAEnE,IAAI,OAAO,SACT,MAAM,KAAK,UAAU;EAEvB,IAAI,OAAO,QACT,MAAM,KAAK,QAAQ;EAErB,IAAI,OAAO,iBAAiB,KAAA,GAC1B,MAAM,KAAK,WAAW,KAAK,mBAAmB,OAAO,YAAY,GAAG;EAGtE,OAAO,eAAe,KAAK,gBAAgB,SAAS,EAAE,cAAc,MAAM,KAAK,GAAG;CACpF;;;;CAKA,mBAA2B,WAAmB,SAAyB;EACrE,OAAO,eAAe,KAAK,gBAAgB,SAAS,EAAE,eAAe,KAAK,gBAAgB,OAAO;CACnG;;;;CAKA,iBACE,WACA,OACQ;EACR,MAAM,YAAY,MAAM,SAAS,YAAY;EAC7C,MAAM,SAAS,kBAAkB,OAAO,KAAK,MAAM;EAInD,MAAM,cACJ,KAAK,WAAW,YAAY,MAAM,QAAQ,UAAU,MAAM,UAAU;EAEtE,IAAI,KAAK,WAAW,YAElB,OAAO,UAAU,UAAU,mCAAmC,KAAK,gBAAgB,MAAM,IAAI,EAAE,MAAM,KAAK,gBAAgB,SAAS,EAAE,IAAI,OAAO,GAAG;EAGrJ,OAAO,UAAU,UAAU,sBAAsB,KAAK,gBAAgB,MAAM,IAAI,EAAE,MAAM,KAAK,gBAAgB,SAAS,EAAE,IAAI,OAAO,GAAG;CACxI;;;;CAKA,kBAA0B,WAA2B;EACnD,IAAI,KAAK,WAAW,YAClB,OAAO,qCAAqC,KAAK,gBAAgB,SAAS;EAE5E,OAAO,wBAAwB,KAAK,gBAAgB,SAAS;CAC/D;;;;CAKA,gBAAwB,MAAsB;EAC5C,OAAO,IAAI,KAAK,QAAQ,MAAM,MAAI,EAAE;CACtC;;;;CAKA,mBAA2B,OAAwB;EACjD,IAAI,UAAU,MAAM,OAAO;EAC3B,IAAI,OAAO,UAAU,UAAU,OAAO,IAAI,MAAM,QAAQ,MAAM,IAAI,EAAE;EACpE,IAAI,OAAO,UAAU,WAAW,OAAO,QAAQ,MAAM;EACrD,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK;EAClD,OAAO,OAAO,KAAK;CACrB;;;;;;;;CASA,mBAA2B,MAAsB;EAC/C,OAAO,KAAK,QAAQ,CAAC,CAAC,SAAS,GAAG,IAAI,OAAO,GAAG,KAAK;CACvD;;;;CAKA,eAAuB,KAAqB;EAC1C,OAAO,IAAI,QAAQ,MAAM,KAAK,CAAC,CAAC,QAAQ,OAAO,KAAK;CACtD;AACF;;;;AAKA,SAAgB,0BACd,IACA,cACA,iBAA2B,CAAC,GAC5B,UAKI,CAAC,GACgB;CACrB,OAAO;EACL;EACA,aAAa,QAAQ,eAAe,cAAc;EAClD,SAAS,QAAQ,WAAW;EAC5B,IAAI;EACJ,MAAM;EACN,cAAc,QAAQ;EACtB,aAAa,QAAQ;CACvB;AACF;;;;AAKA,SAAgB,0BAA0B,aAA+B;CACvE,MAAM,YAAY,YACf,KAAK,OAAO;EACX,MAAM,QAAQ,GAAG,MAAM,SAAS;EAChC,OAAO,QAAQ,SAAS,MAAM,IAAI,EAAE,IAAI;CAC1C,CAAC,CAAC,CACD,QAAQ,MAAM,CAAC,OAAO,MAAM,CAAC,CAAC;CAEjC,MAAM,cAAc,UAAU,SAAS,IAAI,KAAK,IAAI,GAAG,SAAS,IAAI;CACpE,OAAO,OAAO,cAAc,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;AAChD;;;;AAKA,SAAgB,6BAAqC;CACnD,MAAM,sBAAM,IAAI,KAAK;CACrB,OAAO;EACL,IAAI,YAAY;EAChB,OAAO,IAAI,SAAS,IAAI,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;EAC1C,OAAO,IAAI,QAAQ,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;EACrC,OAAO,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;EACtC,OAAO,IAAI,WAAW,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;EACxC,OAAO,IAAI,WAAW,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;CAC1C,CAAC,CAAC,KAAK,EAAE;AACX"}
1
+ {"version":3,"file":"generator.js","names":[],"sources":["../../src/migrations/generator.ts"],"sourcesContent":["/**\n * Migration Generator\n *\n * Generates migration files from schema diffs.\n * Supports both SQL and TypeScript formats.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { getDDLStrategy } from '../schema/ddl/index.js';\nimport { materializeManifestDDLForEngine } from '../schema/ddl/materialize-manifest.js';\nimport {\n planForeignKeyCreation,\n renderDeferredForeignKeyDrop,\n} from '../schema/foreign-key-planner.js';\nimport { cachedDdlTableConstraints } from '../schema/manifest-schema.js';\nimport type {\n MigrationDefinition,\n SchemaChange,\n SchemaDefinition,\n SchemaDiff,\n} from '../schema/types.js';\nimport { renderIndexTarget } from '../schema/utils.js';\nimport { computeChecksum } from './checksum.js';\nimport type { DatabaseEngine } from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Options for migration generation\n */\nexport interface GenerateMigrationOptions {\n /** Migration name (without sequence number) */\n name: string;\n /** Output format */\n format?: 'sql' | 'typescript';\n /** Database engine for engine-specific SQL */\n engine?: DatabaseEngine;\n /** Include generated DOWN statements */\n includeDown?: boolean;\n /** Package name for metadata */\n packageName?: string;\n /** Description for the migration */\n description?: string;\n}\n\n/**\n * Result of migration generation\n */\nexport interface GeneratedMigration {\n /** Full migration ID (e.g., '0001_add_users') */\n id: string;\n /** Filename (e.g., '0001_add_users.sql') */\n filename: string;\n /** File content */\n content: string;\n /** UP SQL statements */\n up: string[];\n /** DOWN SQL statements (if includeDown is true) */\n down: string[];\n /** Checksum of the migration */\n checksum: string;\n}\n\n/**\n * MigrationGenerator class for creating migration files\n */\nexport class MigrationGenerator {\n private engine: DatabaseEngine;\n private format: 'sql' | 'typescript';\n private includeDown: boolean;\n private packageName?: string;\n private materializeStructuredSchema: boolean;\n\n constructor(\n options: {\n engine?: DatabaseEngine;\n format?: 'sql' | 'typescript';\n includeDown?: boolean;\n packageName?: string;\n /**\n * Render new tables from their structured columns through the engine's\n * DDL strategy (default, `true`) — the same renderer `db:migrate` uses,\n * so column types, indexes and triggers match what the differ expects\n * on the next run.\n *\n * @deprecated Passing `false` re-enables the retired cached-`ddl` string\n * path for the CREATE TABLE only (#2358): it emits the manifest's\n * engine-neutral CREATE TABLE with only bare PostgreSQL `TIMESTAMP`\n * rewritten, so `REAL`/`JSON` drift on PostgreSQL. The table's indexes\n * and triggers are still rendered through the engine strategy either\n * way — the flag never suppresses them. Schemas that expose no\n * structured columns always fall back to their cached `ddl` regardless\n * of this flag.\n */\n materializeStructuredSchema?: boolean;\n } = {},\n ) {\n this.engine = options.engine || 'sqlite';\n this.format = options.format || 'sql';\n this.includeDown = options.includeDown ?? true;\n this.packageName = options.packageName;\n this.materializeStructuredSchema =\n options.materializeStructuredSchema ?? true;\n }\n\n /**\n * Generate a migration from a schema diff\n */\n generateFromDiff(\n diff: SchemaDiff,\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n const upStatements: string[] = [];\n const downStatements: string[] = [];\n const addedTableDownStatements: string[] = [];\n\n // Handle new tables: CREATE TABLE plus the table's indexes and triggers,\n // exactly as the migration orchestrator emits them (#2358). The differ\n // only produces `add_index` changes for tables that already exist, so\n // there is no double emission against `diff.changes`.\n const tablePlan = planForeignKeyCreation(diff.added_tables, this.engine);\n for (const schema of tablePlan.schemas) {\n upStatements.push(this.generateCreateTable(schema));\n upStatements.push(...this.generateTableIndexes(schema));\n upStatements.push(...this.generateTableTriggers(schema));\n }\n upStatements.push(...tablePlan.deferredStatements);\n if (this.includeDown || options.includeDown) {\n if (this.engine === 'sqlite' && tablePlan.cyclicTables.length > 0) {\n // SQLite cannot drop either side of a populated mutual FK cycle with\n // immediate checks. Defer checks until the transaction commits, after\n // every member has been removed.\n addedTableDownStatements.push('PRAGMA defer_foreign_keys = ON');\n }\n // PostgreSQL mutual cycles are made acyclic first; every table can then\n // be dropped in the exact reverse of the parent-first creation order.\n addedTableDownStatements.push(\n ...[...tablePlan.deferredConstraints]\n .reverse()\n .map(({ table, foreignKey }) =>\n renderDeferredForeignKeyDrop(table, foreignKey),\n ),\n );\n addedTableDownStatements.push(\n ...[...tablePlan.schemas]\n .reverse()\n .map((schema) => this.generateDropTable(schema.tableName)),\n );\n }\n\n // Handle column and index changes\n for (const change of diff.changes) {\n const { up, down } = this.generateChangeSQL(change);\n upStatements.push(...up);\n if ((this.includeDown || options.includeDown) && down.length > 0) {\n downStatements.push(...down);\n }\n }\n\n // Changes are applied after table creation, so undo them before removing\n // the new tables they may reference. The table block is itself already\n // ordered as deferred-constraint drops followed by child-first drops.\n downStatements.push(...addedTableDownStatements);\n\n // Handle dropped tables (already in SQL form if present)\n for (const tableName of diff.dropped_tables) {\n upStatements.push(this.generateDropTable(tableName));\n // No automatic DOWN for drops - too dangerous\n }\n\n return this.generateMigration(upStatements, downStatements, options);\n }\n\n /**\n * Generate a migration from explicit SQL statements\n */\n generateFromSQL(\n upStatements: string[],\n downStatements: string[],\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n return this.generateMigration(upStatements, downStatements, options);\n }\n\n /**\n * Generate an empty migration template\n */\n generateEmpty(options: GenerateMigrationOptions): GeneratedMigration {\n return this.generateMigration([], [], options);\n }\n\n /**\n * Core migration generation logic\n */\n private generateMigration(\n upStatements: string[],\n downStatements: string[],\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n const id = options.name;\n const description = options.description || `Migration: ${options.name}`;\n const format = options.format || this.format;\n const filename = `${id}.${format === 'sql' ? 'sql' : 'ts'}`;\n const checksum = computeChecksum(upStatements);\n\n let content: string;\n if (format === 'sql') {\n content = this.generateSQLFile(\n id,\n description,\n upStatements,\n downStatements,\n );\n } else {\n content = this.generateTypeScriptFile(\n id,\n description,\n upStatements,\n downStatements,\n );\n }\n\n return {\n id,\n filename,\n content,\n up: upStatements,\n down: downStatements,\n checksum,\n };\n }\n\n /**\n * Generate SQL migration file content\n */\n private generateSQLFile(\n id: string,\n description: string,\n upStatements: string[],\n downStatements: string[],\n ): string {\n const lines: string[] = [\n `-- @id: ${id}`,\n `-- @description: ${description}`,\n `-- @generated: ${new Date().toISOString()}`,\n ];\n\n if (this.packageName) {\n lines.push(`-- @package: ${this.packageName}`);\n }\n\n lines.push('');\n lines.push('-- @up');\n\n if (upStatements.length === 0) {\n lines.push('-- Add your UP statements here');\n } else {\n for (const stmt of upStatements) {\n lines.push(this.terminateStatement(stmt));\n }\n }\n\n lines.push('');\n lines.push('-- @down');\n\n if (downStatements.length === 0) {\n lines.push('-- Add your DOWN statements here');\n } else {\n for (const stmt of downStatements) {\n lines.push(this.terminateStatement(stmt));\n }\n }\n\n lines.push('');\n return lines.join('\\n');\n }\n\n /**\n * Generate TypeScript migration file content\n */\n private generateTypeScriptFile(\n id: string,\n description: string,\n upStatements: string[],\n downStatements: string[],\n ): string {\n const upStatementsStr =\n upStatements.length > 0\n ? upStatements\n .map((s) => ` await db.query(\\`${this.escapeTemplate(s)}\\`);`)\n .join('\\n')\n : ' // Add your UP statements here';\n\n const downStatementsStr =\n downStatements.length > 0\n ? downStatements\n .map((s) => ` await db.query(\\`${this.escapeTemplate(s)}\\`);`)\n .join('\\n')\n : ' // Add your DOWN statements here';\n\n return `/**\n * Migration: ${id}\n * ${description}\n *\n * Generated: ${new Date().toISOString()}\n */\n\nimport type { Migration } from '@happyvertical/smrt-core';\n\nexport default {\n id: '${id}',\n description: '${description.replace(/'/g, \"\\\\'\")}',\n version: '1.0.0',\n\n up: async (db) => {\n${upStatementsStr}\n },\n\n down: async (db) => {\n${downStatementsStr}\n },\n} satisfies Migration;\n`;\n }\n\n /**\n * Generate CREATE TABLE SQL from schema definition\n */\n private generateCreateTable(schema: SchemaDefinition): string {\n if (Object.keys(schema.columns).length === 0 && schema.ddl?.trim()) {\n // Structured materialization cannot preserve constraints or vendor SQL\n // when an older/custom manifest exposes only executable DDL. Retain the\n // only valid representation even when materialization was requested.\n return materializeManifestDDLForEngine(schema.ddl, this.engine);\n }\n\n if (!this.materializeStructuredSchema && schema.ddl?.trim()) {\n // Deprecated opt-out (#2358): the cached string is engine-neutral and\n // index-less; only bare PostgreSQL TIMESTAMP is rewritten.\n return materializeManifestDDLForEngine(schema.ddl, this.engine);\n }\n\n // The manifest's pre-generated `ddl` is deliberately engine-neutral and\n // may contain abstract TIMESTAMP/JSON/UUID types. Materialize the\n // structured columns through the selected engine strategy; executing the\n // cached string on PostgreSQL would otherwise bypass TIMESTAMPTZ and lose\n // JavaScript Date offsets (#2069). This matches the migration orchestrator\n // and also prevents JSON/REAL/UUID drift after a create.\n if (schema.ddl?.trim()) {\n // Table-level constraints that live only in a hand-authored cached\n // string cannot be represented structurally and are dropped — as\n // `db:migrate` already drops them. Say so instead of losing them\n // silently (#2358).\n const constraints = cachedDdlTableConstraints(schema.ddl);\n if (constraints.length > 0) {\n logger.warn(\n `[MigrationGenerator] cached ddl for table \"${schema.tableName}\" declares ` +\n `${constraints.length} table constraint(s) that structured columns cannot ` +\n `carry (${constraints.join('; ')}); the structured schema is authoritative ` +\n `and they are not rendered (#2358).`,\n );\n }\n }\n return getDDLStrategy(this.engine).generateCreateTable(schema);\n }\n\n /**\n * Generate the CREATE INDEX statements for a new table through the engine\n * strategy (partial `WHERE`, JSON-path targets, inline-UNIQUE engines).\n */\n private generateTableIndexes(schema: SchemaDefinition): string[] {\n return getDDLStrategy(this.engine).generateIndexes(schema);\n }\n\n /**\n * Generate the CREATE TRIGGER statements for a new table (engines without\n * trigger support return none).\n */\n private generateTableTriggers(schema: SchemaDefinition): string[] {\n return getDDLStrategy(this.engine).generateTriggers(schema);\n }\n\n /**\n * Generate DROP TABLE SQL\n */\n private generateDropTable(tableName: string): string {\n return `DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`;\n }\n\n /**\n * Generate SQL for a schema change\n */\n private generateChangeSQL(change: SchemaChange): {\n up: string[];\n down: string[];\n } {\n const up: string[] = [];\n const down: string[] = [];\n const sqlStatements =\n change.sqlStatements ?? (change.sql ? [change.sql] : []);\n\n switch (change.type) {\n case 'add_column':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.column && change.name) {\n up.push(\n this.generateAddColumn(change.table, change.name, change.column),\n );\n }\n if (change.name) {\n down.push(this.generateDropColumn(change.table, change.name));\n }\n break;\n\n case 'drop_column':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.name) {\n up.push(this.generateDropColumn(change.table, change.name));\n }\n // No automatic DOWN for drops\n break;\n\n case 'add_index':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.index) {\n up.push(this.generateAddIndex(change.table, change.index));\n }\n if (change.name) {\n down.push(this.generateDropIndex(change.name));\n }\n break;\n\n case 'add_foreign_key':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.advisory) {\n up.push(\n `-- ${change.advisory.severity === 'warning' ? 'WARNING' : 'NOTE'}: Foreign-key constraint ${change.table}.${change.name ?? 'unknown'}`,\n );\n if (change.advisory.message) {\n up.push(`-- ${change.advisory.message}`);\n }\n for (const suggestion of change.advisory.suggestedSql ?? []) {\n up.push(`-- suggested: ${suggestion}`);\n }\n }\n // Constraint rollback is intentionally manual: names on older live\n // databases may not match the deterministic current generator name.\n break;\n\n case 'drop_foreign_key':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.advisory) {\n up.push(\n `-- ${change.advisory.severity === 'warning' ? 'WARNING' : 'NOTE'}: Foreign-key constraint ${change.table}.${change.name ?? 'unknown'}`,\n );\n up.push(`-- ${change.advisory.message}`);\n }\n // Re-adding a removed constraint requires a fresh orphan check and is\n // therefore intentionally not generated as an automatic rollback.\n break;\n\n case 'drop_index':\n if (change.name) {\n up.push(this.generateDropIndex(change.name));\n }\n // No automatic DOWN for index drops\n break;\n\n case 'type_upgrade':\n // Type upgrades are safe conversions that SMRT can handle automatically\n // The SQL is generated by the differ based on engine-specific requirements\n if (sqlStatements.length > 0) {\n for (const sql of sqlStatements) {\n // Skip comment-only SQL (indicates a no-op migration)\n if (!sql.startsWith('--')) {\n up.push(sql);\n }\n }\n }\n // No automatic DOWN for type changes - could lose data\n break;\n\n case 'type_mismatch':\n // Type mismatches require manual intervention\n up.push(`-- WARNING: Type mismatch for ${change.table}.${change.name}`);\n up.push(\n `-- Expected: ${change.mismatch?.expected}, Actual: ${change.mismatch?.actual}`,\n );\n up.push(`-- Manual migration required`);\n break;\n\n case 'alter_column':\n // Nullability/default repairs (#2369). Executable statements come\n // from the differ (backfill + ALTER COLUMN); comment-only SQL marks\n // an in-place limitation (SQLite) and is surfaced as a note. Report-\n // only advisories (relaxations not opted into) are not emitted.\n if (change.advisory && sqlStatements.length === 0) {\n break;\n }\n for (const sql of sqlStatements) {\n up.push(sql);\n }\n // No automatic DOWN: the previous nullability/default is not\n // captured with enough fidelity to restore it safely.\n break;\n\n case 'orphan_column':\n case 'orphan_index':\n case 'rename_data_pending':\n // Report-only: surface the advisory as comments so a generated\n // migration file documents the drift without acting on it.\n up.push(\n `-- ${change.advisory?.severity === 'warning' ? 'WARNING' : 'NOTE'}: ${\n change.type === 'orphan_column'\n ? 'Orphan column'\n : change.type === 'orphan_index'\n ? 'Orphan unique constraint'\n : 'Rename data pending'\n } ${change.table}.${change.name} (${change.mismatch?.actual ?? 'unknown shape'})`,\n );\n if (change.advisory?.message) {\n up.push(`-- ${change.advisory.message}`);\n }\n for (const suggestion of change.advisory?.suggestedSql ?? []) {\n up.push(`-- suggested: ${suggestion}`);\n }\n break;\n\n default:\n // Unknown change type\n break;\n }\n\n return { up, down };\n }\n\n /**\n * Generate ADD COLUMN SQL\n */\n private generateAddColumn(\n tableName: string,\n colName: string,\n colDef: import('../schema/types.js').ColumnDefinition,\n ): string {\n const parts: string[] = [this.quoteIdentifier(colName), colDef.type];\n\n if (colDef.notNull) {\n parts.push('NOT NULL');\n }\n if (colDef.unique) {\n parts.push('UNIQUE');\n }\n if (colDef.defaultValue !== undefined) {\n parts.push(`DEFAULT ${this.formatDefaultValue(colDef.defaultValue)}`);\n }\n\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${parts.join(' ')}`;\n }\n\n /**\n * Generate DROP COLUMN SQL\n */\n private generateDropColumn(tableName: string, colName: string): string {\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP COLUMN ${this.quoteIdentifier(colName)}`;\n }\n\n /**\n * Generate ADD INDEX SQL\n */\n private generateAddIndex(\n tableName: string,\n index: import('../schema/types.js').IndexDefinition,\n ): string {\n const uniqueStr = index.unique ? 'UNIQUE ' : '';\n const target = renderIndexTarget(index, this.engine);\n // Partial-index predicate. DuckDB (and the JSON adapter backed by it)\n // rejects partial indexes, so the declaration degrades to a full index\n // there — matching the differ and the DuckDB DDL strategy.\n const whereClause =\n this.engine !== 'duckdb' && index.where ? ` WHERE ${index.where}` : '';\n\n if (this.engine === 'postgres') {\n // PostgreSQL: use CONCURRENTLY for production safety\n return `CREATE ${uniqueStr}INDEX CONCURRENTLY IF NOT EXISTS ${this.quoteIdentifier(index.name)} ON ${this.quoteIdentifier(tableName)} (${target})${whereClause}`;\n }\n\n return `CREATE ${uniqueStr}INDEX IF NOT EXISTS ${this.quoteIdentifier(index.name)} ON ${this.quoteIdentifier(tableName)} (${target})${whereClause}`;\n }\n\n /**\n * Generate DROP INDEX SQL\n */\n private generateDropIndex(indexName: string): string {\n if (this.engine === 'postgres') {\n return `DROP INDEX CONCURRENTLY IF EXISTS ${this.quoteIdentifier(indexName)}`;\n }\n return `DROP INDEX IF EXISTS ${this.quoteIdentifier(indexName)}`;\n }\n\n /**\n * Quote a SQL identifier\n */\n private quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n }\n\n /**\n * Format a default value for SQL\n */\n private formatDefaultValue(value: unknown): string {\n if (value === null) return 'NULL';\n if (typeof value === 'string') return `'${value.replace(/'/g, \"''\")}'`;\n if (typeof value === 'boolean') return value ? '1' : '0';\n if (typeof value === 'number') return String(value);\n return String(value);\n }\n\n /**\n * Terminate a statement with exactly one `;` for the SQL file format.\n *\n * Strategy-rendered statements (`generateCreateTable`, `generateIndexes`,\n * `generateTriggers`) already end with `;`, while differ-produced ALTERs do\n * not — appending unconditionally wrote `;;` (#2406 review).\n */\n private terminateStatement(stmt: string): string {\n return stmt.trimEnd().endsWith(';') ? stmt : `${stmt};`;\n }\n\n /**\n * Escape template literal content\n */\n private escapeTemplate(str: string): string {\n return str.replace(/`/g, '\\\\`').replace(/\\$/g, '\\\\$');\n }\n}\n\n/**\n * Generate a migration definition from UP and DOWN statements\n */\nexport function createMigrationDefinition(\n id: string,\n upStatements: string[],\n downStatements: string[] = [],\n options: {\n description?: string;\n version?: string;\n packageName?: string;\n sourceFile?: string;\n } = {},\n): MigrationDefinition {\n return {\n id,\n description: options.description || `Migration: ${id}`,\n version: options.version || '1.0.0',\n up: upStatements,\n down: downStatements,\n package_name: options.packageName,\n source_file: options.sourceFile,\n };\n}\n\n/**\n * Generate a sequence number for a new migration\n */\nexport function generateMigrationSequence(existingIds: string[]): string {\n const sequences = existingIds\n .map((id) => {\n const match = id.match(/^(\\d+)_/);\n return match ? parseInt(match[1], 10) : 0;\n })\n .filter((n) => !Number.isNaN(n));\n\n const maxSequence = sequences.length > 0 ? Math.max(...sequences) : 0;\n return String(maxSequence + 1).padStart(4, '0');\n}\n\n/**\n * Generate a timestamp-based migration ID\n */\nexport function generateMigrationTimestamp(): string {\n const now = new Date();\n return [\n now.getFullYear(),\n String(now.getMonth() + 1).padStart(2, '0'),\n String(now.getDate()).padStart(2, '0'),\n String(now.getHours()).padStart(2, '0'),\n String(now.getMinutes()).padStart(2, '0'),\n String(now.getSeconds()).padStart(2, '0'),\n ].join('');\n}\n"],"mappings":"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"}
@@ -7,6 +7,8 @@ export interface PostgresPermissionContract {
7
7
  migrationOwner: string;
8
8
  runtimeRole: string;
9
9
  managedTables: string[];
10
+ /** Existing operator-owned tables retained outside the runtime data surface. */
11
+ retainedTables?: string[];
10
12
  /** Exact zero-argument trigger functions bound only to managed tables. */
11
13
  managedTriggerFunctions?: string[];
12
14
  monitor?: {
@@ -31,6 +33,7 @@ export interface PostgresPermissionPlan {
31
33
  }
32
34
  type NormalizedPostgresPermissionContract = PostgresPermissionContract & {
33
35
  managedTriggerFunctions: string[];
36
+ retainedTables: string[];
34
37
  };
35
38
  type Executor = Pick<DatabaseInterface, 'query'>;
36
39
  /** Validate configuration before opening a connection; unknown options fail closed. */
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-permissions.d.ts","sourceRoot":"","sources":["../src/postgres-permissions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAS5D,0EAA0E;AAC1E,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,eAAe,EAAE,IAAI,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,0EAA0E;IAC1E,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;CAC9D;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,SAAS,GAAG,WAAW,GAAG,aAAa,CAAC;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,0BAA0B,CAAC;IACrC,WAAW,EAAE,4BAA4B,EAAE,CAAC;IAC5C,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,KAAK,oCAAoC,GAAG,0BAA0B,GAAG;IACvE,uBAAuB,EAAE,MAAM,EAAE,CAAC;CACnC,CAAC;AAEF,KAAK,QAAQ,GAAG,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;AAkJjD,uFAAuF;AACvF,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,OAAO,GACb,oCAAoC,CA0FtC;AAu2BD,oFAAoF;AACpF,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,QAAQ,EACZ,QAAQ,EAAE,0BAA0B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAEjC;AAED,iFAAiF;AACjF,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,0BAA0B,EACpC,OAAO,EAAE;IAAE,mBAAmB,EAAE,MAAM,CAAA;CAAE,GACvC,OAAO,CAAC,sBAAsB,CAAC,CA8CjC"}
1
+ {"version":3,"file":"postgres-permissions.d.ts","sourceRoot":"","sources":["../src/postgres-permissions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAS5D,0EAA0E;AAC1E,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,eAAe,EAAE,IAAI,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,0EAA0E;IAC1E,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;CAC9D;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,SAAS,GAAG,WAAW,GAAG,aAAa,CAAC;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,0BAA0B,CAAC;IACrC,WAAW,EAAE,4BAA4B,EAAE,CAAC;IAC5C,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,KAAK,oCAAoC,GAAG,0BAA0B,GAAG;IACvE,uBAAuB,EAAE,MAAM,EAAE,CAAC;IAClC,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B,CAAC;AAEF,KAAK,QAAQ,GAAG,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;AA2JjD,uFAAuF;AACvF,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,OAAO,GACb,oCAAoC,CA2GtC;AA2+BD,oFAAoF;AACpF,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,QAAQ,EACZ,QAAQ,EAAE,0BAA0B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAEjC;AAED,iFAAiF;AACjF,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,0BAA0B,EACpC,OAAO,EAAE;IAAE,mBAAmB,EAAE,MAAM,CAAA;CAAE,GACvC,OAAO,CAAC,sBAAsB,CAAC,CA8CjC"}
@@ -51,6 +51,7 @@ function validatePostgresPermissionContract(value) {
51
51
  "migrationOwner",
52
52
  "runtimeRole",
53
53
  "managedTables",
54
+ "retainedTables",
54
55
  "managedTriggerFunctions",
55
56
  "monitor"
56
57
  ], "postgresPermissions");
@@ -61,6 +62,10 @@ function validatePostgresPermissionContract(value) {
61
62
  const runtimeRole = name(input.runtimeRole, "runtimeRole");
62
63
  if (!Array.isArray(input.managedTables)) throw new Error("managedTables must be an array.");
63
64
  const managedTables = [...new Set(input.managedTables.map((table) => name(table, "managedTables entry")))].sort();
65
+ if (input.retainedTables !== void 0 && !Array.isArray(input.retainedTables)) throw new Error("retainedTables must be an array.");
66
+ const retainedTables = [...new Set((input.retainedTables ?? []).map((table) => name(table, "retainedTables entry")))].sort();
67
+ const overlap = retainedTables.find((table) => managedTables.includes(table));
68
+ if (overlap) throw new Error(`Table ${overlap} cannot be both managed and retained.`);
64
69
  if (input.managedTriggerFunctions !== void 0 && !Array.isArray(input.managedTriggerFunctions)) throw new Error("managedTriggerFunctions must be an array.");
65
70
  const managedTriggerFunctions = [...new Set((input.managedTriggerFunctions ?? []).map((routine) => name(routine, "managedTriggerFunctions entry")))].sort();
66
71
  let monitor;
@@ -91,6 +96,7 @@ function validatePostgresPermissionContract(value) {
91
96
  migrationOwner,
92
97
  runtimeRole,
93
98
  managedTables,
99
+ retainedTables,
94
100
  managedTriggerFunctions,
95
101
  ...monitor ? { monitor } : {}
96
102
  };
@@ -111,7 +117,10 @@ async function snapshot(db, contract) {
111
117
  memberships: `SELECT pg_get_userbyid(member) AS member, pg_get_userbyid(roleid) AS parent FROM pg_auth_members WHERE member IN (SELECT oid FROM pg_roles WHERE rolname IN (${roleNames})) OR roleid IN (SELECT oid FROM pg_roles WHERE rolname IN (${roleNames})) ORDER BY member, roleid`,
112
118
  database: `SELECT datname AS name, pg_get_userbyid(datdba) AS owner, ${acl("COALESCE(datacl, acldefault('d',datdba))")} AS acl FROM pg_database WHERE datname=current_database()`,
113
119
  schemas: `SELECT nspname AS name, pg_get_userbyid(nspowner) AS owner, ${acl("COALESCE(nspacl, acldefault('n',nspowner))")} AS acl FROM pg_namespace WHERE left(nspname,3) <> 'pg_' AND nspname <> 'information_schema' ORDER BY nspname`,
114
- relations: `SELECT c.oid::text, n.nspname AS schema, c.relname AS name, c.relkind AS kind, pg_get_userbyid(c.relowner) AS owner, c.relrowsecurity AS rls, ${acl("COALESCE(c.relacl, acldefault(CASE WHEN c.relkind='S' THEN 's'::\"char\" ELSE 'r'::\"char\" END,c.relowner))")} AS acl FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' AND c.relkind IN ('r','p','v','m','f','S') ORDER BY n.nspname,c.relname`,
120
+ relations: `SELECT c.oid::text, n.nspname AS schema, c.relname AS name, c.relkind AS kind, pg_get_userbyid(c.relowner) AS owner, c.relrowsecurity AS rls, (SELECT parent.relname FROM pg_depend d JOIN pg_class parent ON parent.oid=d.refobjid JOIN pg_namespace parent_schema ON parent_schema.oid=parent.relnamespace WHERE d.classid='pg_class'::regclass AND d.objid=c.oid AND d.refclassid='pg_class'::regclass AND d.deptype IN ('a','i') AND parent_schema.oid=n.oid LIMIT 1) AS parent_table, ${acl("COALESCE(c.relacl, acldefault(CASE WHEN c.relkind='S' THEN 's'::\"char\" ELSE 'r'::\"char\" END,c.relowner))")} AS acl FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' AND c.relkind IN ('r','p','v','m','f','S') ORDER BY n.nspname,c.relname`,
121
+ inherits: `SELECT child_schema.nspname AS child_schema, child.relname AS child_table, parent_schema.nspname AS parent_schema, parent.relname AS parent_table FROM pg_inherits i JOIN pg_class child ON child.oid=i.inhrelid JOIN pg_namespace child_schema ON child_schema.oid=child.relnamespace JOIN pg_class parent ON parent.oid=i.inhparent JOIN pg_namespace parent_schema ON parent_schema.oid=parent.relnamespace ORDER BY child_schema.nspname,child.relname,parent_schema.nspname,parent.relname`,
122
+ foreignKeys: `SELECT child_schema.nspname AS child_schema, child.relname AS child_table, parent_schema.nspname AS parent_schema, parent.relname AS parent_table, con.confdeltype::text AS delete_action, con.confupdtype::text AS update_action FROM pg_constraint con JOIN pg_class child ON child.oid=con.conrelid JOIN pg_namespace child_schema ON child_schema.oid=child.relnamespace JOIN pg_class parent ON parent.oid=con.confrelid JOIN pg_namespace parent_schema ON parent_schema.oid=parent.relnamespace WHERE con.contype='f' ORDER BY child_schema.nspname,child.relname,parent_schema.nspname,parent.relname,con.conname`,
123
+ rewriteRules: `WITH RECURSIVE rule_dependencies AS (SELECT r.oid AS rule_oid, r.ev_class AS source_oid, r.rulename AS rule_name, d.refobjid AS referenced_oid FROM pg_rewrite r JOIN pg_depend d ON d.classid='pg_rewrite'::regclass AND d.objid=r.oid AND d.refclassid='pg_class'::regclass WHERE r.rulename <> '_RETURN' UNION SELECT dependencies.rule_oid, dependencies.source_oid, dependencies.rule_name, d.refobjid FROM rule_dependencies dependencies JOIN pg_rewrite view_rule ON view_rule.ev_class=dependencies.referenced_oid AND view_rule.rulename='_RETURN' JOIN pg_depend d ON d.classid='pg_rewrite'::regclass AND d.objid=view_rule.oid AND d.refclassid='pg_class'::regclass) SELECT n.nspname AS schema, c.relname AS table_name, dependencies.rule_name, referenced_schema.nspname AS referenced_schema, referenced.relname AS referenced_table FROM rule_dependencies dependencies JOIN pg_class c ON c.oid=dependencies.source_oid JOIN pg_namespace n ON n.oid=c.relnamespace JOIN pg_class referenced ON referenced.oid=dependencies.referenced_oid JOIN pg_namespace referenced_schema ON referenced_schema.oid=referenced.relnamespace ORDER BY n.nspname,c.relname,dependencies.rule_name,referenced_schema.nspname,referenced.relname`,
115
124
  columns: `SELECT c.oid::text AS relation, a.attname AS name, ${acl("a.attacl")} AS acl FROM pg_attribute a JOIN pg_class c ON c.oid=a.attrelid JOIN pg_namespace n ON n.oid=c.relnamespace WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' AND c.relkind IN ('r','p','v','m','f') AND a.attnum>0 AND NOT a.attisdropped ORDER BY c.oid,a.attnum`,
116
125
  routines: `SELECT n.nspname AS schema, p.oid::text, p.proname AS name, format('%I.%I(%s)', n.nspname, p.proname, oidvectortypes(p.proargtypes)) AS identity, oidvectortypes(p.proargtypes) AS argument_types, pg_get_expr(p.proargdefaults,0) AS argument_defaults, p.prokind AS kind, p.prorettype::regtype::text AS return_type, pg_get_function_result(p.oid) AS result, p.procost::text AS cost, p.prorows::text AS rows, p.prosupport::regproc::text AS support, p.pronargs::text AS argument_count, l.lanname AS language, p.prosecdef AS security_definer, p.provolatile::text AS volatility, p.proparallel::text AS parallel, p.proleakproof AS leakproof, p.proisstrict AS strict, COALESCE(to_json(p.proconfig),'[]'::json) AS config, p.prosrc AS source, CASE WHEN p.prokind='f' THEN pg_get_functiondef(p.oid) END AS definition, pg_get_userbyid(p.proowner) AS owner, ${acl("COALESCE(p.proacl, acldefault('f',p.proowner))")} AS acl FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace JOIN pg_language l ON l.oid=p.prolang WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' ORDER BY n.nspname,p.oid`,
117
126
  triggers: `SELECT t.oid::text, n.nspname AS schema, c.relname AS table_name, t.tgname AS name, t.tgfoid::text AS function_oid, t.tgenabled AS enabled, t.tgisinternal AS internal, t.tgtype::text AS type, encode(t.tgargs,'hex') AS arguments, pg_get_triggerdef(t.oid, true) AS definition FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid JOIN pg_namespace n ON n.oid=c.relnamespace WHERE left(n.nspname,3) <> 'pg_' AND n.nspname <> 'information_schema' ORDER BY n.nspname,c.oid,t.oid`,
@@ -273,11 +282,40 @@ async function plan(db, input) {
273
282
  const tables = state.relations.filter((entry) => entry.schema === contract.schema && entry.kind !== "S");
274
283
  for (const table of bookkeeping) if (!tables.some((entry) => entry.name === table)) unsupported("missing-bookkeeping-table", qualified(contract.schema, table), "Bootstrap all framework bookkeeping tables as the migration owner before permission setup. Stop runtime and monitoring access during migrations or restores, then reconcile before reactivation.");
275
284
  const declared = new Set(contract.managedTables);
285
+ const retained = new Set(contract.retainedTables);
276
286
  for (const table of tables) if (table.name.startsWith("_smrt_")) declared.add(table.name);
287
+ for (const table of retained) if (declared.has(table)) unsupported("retained-managed-table", qualified(contract.schema, table), "Retained tables must be outside the managed and framework table surfaces.");
277
288
  for (const table of declared) if (!tables.some((entry) => entry.name === table)) unsupported("missing-table", qualified(contract.schema, table), "Run supported schema migrations before reconciling permissions.");
289
+ for (const table of retained) if (!tables.some((entry) => entry.name === table)) unsupported("missing-retained-table", qualified(contract.schema, table), "Retained tables must already exist before permission reconciliation.");
290
+ for (const relation of state.inherits) {
291
+ const managedParent = relation.parent_schema === contract.schema && declared.has(relation.parent_table ?? "");
292
+ const childRetained = relation.child_schema === contract.schema && retained.has(relation.child_table);
293
+ const parentRetained = relation.parent_schema === contract.schema && retained.has(relation.parent_table ?? "");
294
+ if (managedParent && relation.child_schema !== contract.schema) unsupported("managed-inheritance", qualified(relation.child_schema, relation.child_table), "Managed tables may not have external inheritance or partition children: parent table privileges can invoke external triggers outside the retained-table isolation boundary.");
295
+ else if (childRetained !== parentRetained) unsupported("retained-inheritance", qualified(relation.child_schema, relation.child_table), "Retained tables may inherit only from retained tables in the dedicated schema; an accessible parent can otherwise expose retained rows.");
296
+ }
297
+ for (const relation of state.foreignKeys) {
298
+ const managedParent = relation.parent_schema === contract.schema && declared.has(relation.parent_table ?? "");
299
+ const childRetained = relation.child_schema === contract.schema && retained.has(relation.child_table);
300
+ const parentRetained = relation.parent_schema === contract.schema && retained.has(relation.parent_table ?? "");
301
+ if (managedParent && relation.child_schema !== contract.schema) {
302
+ unsupported("managed-foreign-key", `${qualified(relation.child_schema, relation.child_table)} -> ${qualified(relation.parent_schema, relation.parent_table ?? "")}`, "Managed tables may not have external foreign-key children: referential actions can invoke external triggers outside the retained-table isolation boundary.");
303
+ continue;
304
+ }
305
+ if (childRetained === parentRetained) continue;
306
+ unsupported("retained-foreign-key", `${qualified(relation.child_schema, relation.child_table)} -> ${qualified(relation.parent_schema, relation.parent_table ?? "")}`, "Retained tables may reference only retained tables; referential actions from an accessible table can otherwise mutate retained rows.");
307
+ }
308
+ for (const rule of state.rewriteRules) {
309
+ const managedSource = rule.schema === contract.schema && declared.has(rule.table_name);
310
+ const retainedSource = rule.schema === contract.schema && retained.has(rule.table_name);
311
+ const managedTarget = rule.referenced_schema === contract.schema && declared.has(rule.referenced_table);
312
+ if (retainedSource && managedTarget) unsupported("user-rewrite-rule", `${qualified(rule.schema, rule.table_name)} (${rule.rule_name})`, "Retained tables may not reference managed tables through rewrite rules.");
313
+ else if (managedSource && rule.referenced_schema !== contract.schema) unsupported("user-rewrite-rule", `${qualified(rule.schema, rule.table_name)} (${rule.rule_name})`, "Managed rewrite rules may not reference external relations: rule-owner table privileges can invoke external triggers outside the retained-table isolation boundary.");
314
+ else if (rule.referenced_schema === contract.schema && retained.has(rule.referenced_table)) unsupported("user-rewrite-rule", `${qualified(rule.schema, rule.table_name)} (${rule.rule_name})`, "User-defined rewrite rules are unsupported because rule-owner privileges can write retained tables indirectly.");
315
+ }
278
316
  for (const [table, columns] of Object.entries(contract.monitor?.tables ?? {})) {
279
317
  const relation = tables.find((entry) => entry.name === table);
280
- if (!declared.has(table) || !relation) unsupported("monitor-table", table, "Monitor tables must exist and belong to managedTables or framework tables.");
318
+ if (!declared.has(table) || retained.has(table) || !relation) unsupported("monitor-table", table, "Monitor tables must exist and belong to the managed or framework table surface, never retained tables.");
281
319
  for (const column of columns) if (!relation || !state.columns.some((entry) => entry.relation === relation.oid && entry.name === column)) unsupported("monitor-column", `${table}.${column}`, "The declared monitoring column does not exist.");
282
320
  }
283
321
  for (const relation of state.relations) {
@@ -288,7 +326,8 @@ async function plan(db, input) {
288
326
  continue;
289
327
  }
290
328
  if (relation.owner !== contract.migrationOwner) unsupported("resource-owner", resource, `Managed resources must be owned by ${contract.migrationOwner}; ownership is never changed.`);
291
- if (relation.kind !== "S" && !declared.has(relation.name)) unsupported("undeclared-table", resource, "The dedicated schema contains an undeclared table; defaults cannot safely cover a mixed schema.");
329
+ if (relation.kind !== "S" && !declared.has(relation.name) && !retained.has(relation.name)) unsupported("undeclared-table", resource, "The dedicated schema contains an undeclared table; defaults cannot safely cover a mixed schema.");
330
+ if (relation.kind === "S" && (!relation.parent_table || !declared.has(relation.parent_table) && !retained.has(relation.parent_table))) unsupported("undeclared-sequence", resource, "Sequences must belong to a declared managed or retained table before permission reconciliation.");
292
331
  if (![
293
332
  "r",
294
333
  "p",
@@ -297,7 +336,7 @@ async function plan(db, input) {
297
336
  if (relation.rls) unsupported("row-level-security", resource, "RLS policies require a separate application authorization review; ACL verification cannot establish allowed row operations.");
298
337
  for (const role of roles) {
299
338
  const isRuntime = role === contract.runtimeRole;
300
- const desired = isRuntime ? relation.kind === "S" ? ["USAGE"] : bookkeeping.has(relation.name) ? ["SELECT"] : [
339
+ const desired = isRuntime ? relation.kind === "S" ? retained.has(relation.parent_table ?? "") ? [] : ["USAGE"] : retained.has(relation.name) ? [] : bookkeeping.has(relation.name) ? ["SELECT"] : [
301
340
  "SELECT",
302
341
  "INSERT",
303
342
  "UPDATE",
@@ -305,13 +344,14 @@ async function plan(db, input) {
305
344
  ] : [];
306
345
  reconcile(relation.kind === "S" ? "SEQUENCE" : "TABLE", resource, relation.acl, role, desired);
307
346
  for (const column of columns) {
308
- const wanted = !isRuntime && (contract.monitor?.tables[relation.name] ?? []).includes(column.name) ? ["SELECT"] : [];
347
+ const wanted = !isRuntime && !retained.has(relation.name) && (contract.monitor?.tables[relation.name] ?? []).includes(column.name) ? ["SELECT"] : [];
309
348
  reconcile("TABLE", resource, column.acl, role, wanted, ` (${identifier(column.name)})`);
310
349
  }
311
350
  }
312
351
  }
313
352
  const triggerRoutineNames = new Set(contract.managedTriggerFunctions);
314
353
  const acceptedTriggerRoutineNames = /* @__PURE__ */ new Set();
354
+ const acceptedTriggerRoutineOids = /* @__PURE__ */ new Set();
315
355
  const routineConfigIsEmpty = (value) => Array.isArray(value) && value.length === 0 || value === "[]";
316
356
  for (const resource of state.routines) {
317
357
  const resourceName = qualified(resource.schema, resource.name);
@@ -324,12 +364,14 @@ async function plan(db, input) {
324
364
  const bindings = state.triggers.filter((trigger) => trigger.function_oid === resource.oid);
325
365
  if (resource.schema === contract.schema && triggerRoutineNames.has(resource.name) && resource.kind === "f" && resource.return_type === "trigger" && resource.argument_types === "" && resource.argument_count === "0" && resource.owner === contract.migrationOwner && resource.language === "plpgsql" && resource.security_definer === false && routineConfigIsEmpty(resource.config) && bindings.length > 0 && bindings.every((trigger) => trigger.schema === contract.schema && trigger.internal === false && (trigger.enabled === "O" || trigger.enabled === "A") && declared.has(trigger.table_name))) {
326
366
  acceptedTriggerRoutineNames.add(resource.name);
367
+ acceptedTriggerRoutineOids.add(resource.oid);
327
368
  reconcileRoutine(resource.identity, resource.acl, /* @__PURE__ */ new Map());
328
369
  continue;
329
370
  }
330
371
  for (const role of roles) if (resource.schema === contract.schema || resource.owner === role || relevant(resource.acl, role).length) unsupported("unsupported-routines", resourceName, triggerRoutineNames.has(resource.name) ? "Declared trigger routines must be migration-owner invoker PL/pgSQL functions returning trigger with no declared arguments, no function settings, and enabled non-internal bindings only to declared managed tables." : "User-defined routines are outside the supported table/sequence and declared trigger contract; review their effective privileges explicitly.", role);
331
372
  }
332
373
  for (const name of triggerRoutineNames) if (!acceptedTriggerRoutineNames.has(name)) unsupported("missing-managed-trigger-function", qualified(contract.schema, name), "Run the migration that creates this exact managed trigger function and its enabled binding before reconciling permissions.");
374
+ for (const trigger of state.triggers) if (trigger.schema === contract.schema && declared.has(trigger.table_name) && trigger.internal === false && (trigger.enabled === "O" || trigger.enabled === "A") && !acceptedTriggerRoutineOids.has(trigger.function_oid)) unsupported("unsupported-managed-trigger", `${qualified(trigger.schema, trigger.table_name)} (${trigger.name})`, "Enabled managed-table triggers must use an exact declared invoker trigger routine; undeclared or SECURITY DEFINER trigger functions can bypass retained-table isolation.");
333
375
  for (const resource of state.types) for (const role of roles) if (resource.schema === contract.schema || resource.owner === role || relevant(resource.acl, role).length) unsupported("unsupported-types", qualified(resource.schema, resource.name), "User-defined types are outside the supported table/sequence contract; review their effective privileges explicitly.", role);
334
376
  for (const defaults of state.defaults) {
335
377
  const resource = `DEFAULT ${defaults.kind} (${defaults.schema || "global"})`;
@@ -384,7 +426,8 @@ async function plan(db, input) {
384
426
  "Only PostgreSQL table, column, sequence, schema and database ACLs are qualified; application authorization and RLS are separate. Large-object ownership/access is unsupported, and lo_compat_privileges must remain off for deployed roles.",
385
427
  "Future user-defined routines and types are unsupported. PostgreSQL implicit global defaults grant PUBLIC EXECUTE/USAGE; rerun diagnostics after every migration before activating runtime roles.",
386
428
  "Pause migrations and external ACL/role writers during planning and apply. The advisory lock coordinates SMRT permission writers only.",
387
- "All framework bookkeeping tables must exist before setup. Stop runtime and monitor access throughout migrations, restores and repair: recreated bookkeeping tables temporarily receive creator CRUD defaults and require explicit reconciliation before either role is reactivated."
429
+ "All framework bookkeeping tables must exist before setup. Stop runtime and monitor access throughout migrations, restores and repair: recreated bookkeeping tables temporarily receive creator CRUD defaults and require explicit reconciliation before either role is reactivated.",
430
+ "Retained tables are operator-owned data outside the runtime and monitor surfaces. Creator defaults initially grant runtime access to every new table and sequence, so create or restore every retained table first, then apply its reviewed plan and verify access is revoked before reactivating restricted roles."
388
431
  ],
389
432
  statements: [...new Set(statements)],
390
433
  canApply: !diagnostics.some((entry) => entry.severity === "unsupported"),