@warlock.js/cascade 5.21.0 → 5.22.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-migration-driver.mjs","names":[],"sources":["../../../../../../../../cascade/src/drivers/postgres/postgres-migration-driver.ts"],"sourcesContent":["/**\n * PostgreSQL Migration Driver\n *\n * Implements the MigrationDriverContract for PostgreSQL DDL operations.\n * Provides methods for creating/dropping tables, columns, indexes,\n * and constraints.\n *\n * @module cascade/drivers/postgres\n */\n\nimport { databaseTransactionContext } from \"../../context/database-transaction-context\";\nimport type {\n ColumnDefinition,\n ColumnType,\n ForeignKeyDefinition,\n FullTextIndexOptions,\n GeoIndexOptions,\n IndexDefinition,\n MigrationDriverContract,\n TableIndexInformation,\n VectorIndexOptions,\n} from \"../../contracts/migration-driver.contract\";\nimport type { MigrationDefaults, UuidStrategy } from \"../../types\";\nimport type { PostgresDriver } from \"./postgres-driver\";\n\n/**\n * PostgreSQL Migration Driver.\n *\n * Handles database schema operations for PostgreSQL including:\n * - Table creation and deletion\n * - Column management\n * - Index creation (B-tree, GIN, GiST, etc.)\n * - Constraint management (foreign keys, unique, etc.)\n *\n * @example\n * ```typescript\n * const migrationDriver = driver.migrationDriver();\n *\n * // Create a table\n * await migrationDriver.createTable('users');\n *\n * // Add columns\n * await migrationDriver.addColumn('users', {\n * name: 'email',\n * type: 'string',\n * length: 255,\n * nullable: false\n * });\n *\n * // Create unique index\n * await migrationDriver.createUniqueIndex('users', ['email']);\n * ```\n */\nexport class PostgresMigrationDriver implements MigrationDriverContract {\n /**\n * Active transaction client (if any).\n */\n private get transactionClient(): unknown {\n return databaseTransactionContext.getSession();\n }\n\n /**\n * Create a new migration driver.\n *\n * @param driver - The PostgreSQL driver instance\n */\n public constructor(public readonly driver: PostgresDriver) {}\n\n // ============================================================================\n // TABLE OPERATIONS\n // ============================================================================\n\n /**\n * Create a new table with a default id column.\n *\n * @param table - Table name\n */\n public async createTable(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n // Create empty table - columns are added via addColumn operations\n await this.execute(`CREATE TABLE ${quotedTable} ()`);\n }\n\n /**\n * Create table if it doesn't exist.\n *\n * @param table - Table name\n */\n public async createTableIfNotExists(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n // Create empty table if not exists - columns are added via addColumn operations\n await this.execute(`CREATE TABLE IF NOT EXISTS ${quotedTable} ()`);\n }\n\n /**\n * Drop an existing table.\n *\n * @param table - Table name\n */\n public async dropTable(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n await this.execute(`DROP TABLE ${quotedTable} CASCADE`);\n }\n\n /**\n * Drop table if it exists.\n *\n * @param table - Table name\n */\n public async dropTableIfExists(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n await this.execute(`DROP TABLE IF EXISTS ${quotedTable} CASCADE`);\n }\n\n /**\n * Rename a table.\n *\n * @param from - Current table name\n * @param to - New table name\n */\n public async renameTable(from: string, to: string): Promise<void> {\n const quotedFrom = this.driver.dialect.quoteIdentifier(from);\n const quotedTo = this.driver.dialect.quoteIdentifier(to);\n await this.execute(`ALTER TABLE ${quotedFrom} RENAME TO ${quotedTo}`);\n }\n\n /**\n * Truncate a table — remove all rows efficiently.\n *\n * @param table - Table name\n */\n public async truncateTable(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n await this.execute(`TRUNCATE TABLE ${quotedTable}`);\n }\n\n /**\n * Check if a table exists.\n *\n * @param table - Table name\n * @returns Whether the table exists\n */\n public async tableExists(table: string): Promise<boolean> {\n const result = await this.driver.query<{ exists: boolean }>(\n `SELECT EXISTS (\n SELECT FROM information_schema.tables\n WHERE table_schema = current_schema()\n AND table_name = $1\n )`,\n [table],\n );\n return result.rows[0]?.exists ?? false;\n }\n\n /**\n * List all columns in a table.\n *\n * @param table - Table name\n * @returns Array of column definitions\n */\n public async listColumns(table: string): Promise<ColumnDefinition[]> {\n const result = await this.driver.query<{\n column_name: string;\n data_type: string;\n character_maximum_length: number | null;\n numeric_precision: number | null;\n numeric_scale: number | null;\n is_nullable: string;\n column_default: string | null;\n }>(\n `SELECT\n column_name,\n data_type,\n character_maximum_length,\n numeric_precision,\n numeric_scale,\n is_nullable,\n column_default\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name = $1\n ORDER BY ordinal_position`,\n [table],\n );\n\n return result.rows.map((row) => ({\n name: row.column_name,\n type: this.mapPostgresTypeToColumnType(row.data_type),\n length: row.character_maximum_length ?? undefined,\n precision: row.numeric_precision ?? undefined,\n scale: row.numeric_scale ?? undefined,\n nullable: row.is_nullable === \"YES\",\n defaultValue: row.column_default ?? undefined,\n }));\n }\n\n /**\n * List all tables in the current database.\n *\n * @returns Array of table names\n */\n public async listTables(): Promise<string[]> {\n const result = await this.driver.query<{ table_name: string }>(\n `SELECT table_name\n FROM information_schema.tables\n WHERE table_schema = current_schema()\n ORDER BY table_name`,\n );\n\n return result.rows.map((row) => row.table_name);\n }\n\n /**\n * Ensure the migrations tracking table exists.\n *\n * Creates the table with proper schema if it doesn't exist.\n *\n * @param tableName - Name of the migrations table\n */\n public async ensureMigrationsTable(tableName: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(tableName);\n\n await this.execute(`\n CREATE TABLE IF NOT EXISTS ${quotedTable} (\n \"id\" SERIAL PRIMARY KEY,\n \"name\" VARCHAR(255) NOT NULL UNIQUE,\n \"batch\" INTEGER NOT NULL,\n \"executedAt\" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),\n \"createdAt\" TIMESTAMP WITH TIME ZONE\n )\n `);\n }\n\n // ============================================================================\n // COLUMN OPERATIONS\n // ============================================================================\n\n /**\n * Add a column to an existing table.\n *\n * @param table - Table name\n * @param column - Column definition\n */\n public async addColumn(table: string, column: ColumnDefinition): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column.name);\n\n // For auto-increment integers, use SERIAL/BIGSERIAL instead of INTEGER/BIGINT\n let sqlType: string;\n if (column.autoIncrement) {\n if (column.type === \"bigInteger\") {\n sqlType = \"BIGSERIAL\";\n } else {\n sqlType = \"SERIAL\";\n }\n } else {\n sqlType = this.driver.dialect.getSqlType(column.type, {\n length: column.length,\n precision: column.precision,\n scale: column.scale,\n dimensions: column.dimensions,\n });\n }\n\n let sql = `ALTER TABLE ${quotedTable} ADD COLUMN ${quotedColumn} ${sqlType}`;\n\n // Handle generated columns\n if (column.generated) {\n sql += ` GENERATED ALWAYS AS (${column.generated.expression})`;\n if (column.generated.stored) {\n sql += \" STORED\";\n }\n // PostgreSQL only supports STORED, not VIRTUAL\n // If virtual is requested, it's simply ignored (PostgreSQL doesn't support it)\n } else {\n // SERIAL/BIGSERIAL are always NOT NULL, so skip for those\n if (!column.autoIncrement && column.nullable === false) {\n sql += \" NOT NULL\";\n }\n\n if (column.defaultValue !== undefined) {\n // Check for special CURRENT_TIMESTAMP marker\n if (\n typeof column.defaultValue === \"object\" &&\n column.defaultValue !== null &&\n (column.defaultValue as any).__type === \"CURRENT_TIMESTAMP\"\n ) {\n sql += \" DEFAULT NOW()\";\n } else if (column.isRawDefault === false) {\n // Explicit string literal - escape it properly\n const escaped = String(column.defaultValue).replace(/'/g, \"''\");\n sql += ` DEFAULT '${escaped}'`;\n } else if (typeof column.defaultValue === \"boolean\") {\n // Boolean values\n sql += ` DEFAULT ${column.defaultValue ? \"TRUE\" : \"FALSE\"}`;\n } else if (typeof column.defaultValue === \"number\") {\n // Numeric values\n sql += ` DEFAULT ${column.defaultValue}`;\n } else {\n // Raw SQL expression (default behavior when isRawDefault is true or undefined)\n sql += ` DEFAULT ${column.defaultValue}`;\n }\n }\n\n // Handle primary key\n if (column.primary) {\n sql += \" PRIMARY KEY\";\n }\n\n // Handle unique constraint\n if (column.unique) {\n sql += \" UNIQUE\";\n }\n }\n\n await this.execute(sql);\n }\n\n /**\n * Drop a column from a table.\n *\n * @param table - Table name\n * @param column - Column name\n */\n public async dropColumn(table: string, column: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column);\n await this.execute(`ALTER TABLE ${quotedTable} DROP COLUMN ${quotedColumn}`);\n }\n\n /**\n * Drop multiple columns from a table.\n *\n * @param table - Table name\n * @param columns - Column names\n */\n public async dropColumns(table: string, columns: string[]): Promise<void> {\n for (const column of columns) {\n await this.dropColumn(table, column);\n }\n }\n\n /**\n * Rename a column.\n *\n * @param table - Table name\n * @param from - Current column name\n * @param to - New column name\n */\n public async renameColumn(table: string, from: string, to: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedFrom = this.driver.dialect.quoteIdentifier(from);\n const quotedTo = this.driver.dialect.quoteIdentifier(to);\n await this.execute(`ALTER TABLE ${quotedTable} RENAME COLUMN ${quotedFrom} TO ${quotedTo}`);\n }\n\n /**\n * Modify an existing column.\n *\n * @param table - Table name\n * @param column - New column definition\n */\n public async modifyColumn(table: string, column: ColumnDefinition): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column.name);\n const sqlType = this.driver.dialect.getSqlType(column.type, {\n length: column.length,\n precision: column.precision,\n scale: column.scale,\n dimensions: column.dimensions,\n });\n\n // PostgreSQL requires separate ALTER statements for type and nullability\n await this.execute(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} TYPE ${sqlType}`);\n\n if (column.nullable === false) {\n await this.execute(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} SET NOT NULL`);\n } else if (column.nullable === true) {\n await this.execute(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} DROP NOT NULL`);\n }\n\n if (column.defaultValue !== undefined) {\n let defaultVal: string;\n\n // Check for special CURRENT_TIMESTAMP marker\n if (\n typeof column.defaultValue === \"object\" &&\n column.defaultValue !== null &&\n (column.defaultValue as any).__type === \"CURRENT_TIMESTAMP\"\n ) {\n defaultVal = \"NOW()\";\n } else if (typeof column.defaultValue === \"string\") {\n defaultVal = `'${column.defaultValue}'`;\n } else {\n defaultVal = String(column.defaultValue);\n }\n\n await this.execute(\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} SET DEFAULT ${defaultVal}`,\n );\n }\n }\n\n /**\n * Create standard timestamp columns (created_at, updated_at).\n *\n * PostgreSQL implementation creates TIMESTAMPTZ columns with NOW() defaults.\n *\n * @param table - Table name\n */\n public async createTimestampColumns(table: string): Promise<void> {\n await this.addColumn(table, {\n name: \"created_at\",\n type: \"timestamp\",\n nullable: false,\n defaultValue: \"NOW()\",\n isRawDefault: true,\n });\n\n await this.addColumn(table, {\n name: \"updated_at\",\n type: \"timestamp\",\n nullable: false,\n defaultValue: \"NOW()\",\n isRawDefault: true,\n });\n }\n\n // ============================================================================\n // INDEX OPERATIONS\n // ============================================================================\n\n /**\n * Create an index on one or more columns.\n *\n * Supports:\n * - Regular column indexes\n * - Expression-based indexes (e.g., `lower(email)`)\n * - Covering indexes (INCLUDE clause)\n * - Concurrent index creation (CONCURRENTLY keyword)\n *\n * @param table - Table name\n * @param index - Index definition\n */\n public async createIndex(table: string, index: IndexDefinition): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const indexName = index.name ?? `idx_${table}_${index.columns.join(\"_\")}`;\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n const uniqueKeyword = index.unique ? \"UNIQUE \" : \"\";\n const concurrentlyKeyword = index.concurrently ? \"CONCURRENTLY \" : \"\";\n\n let columnsPart: string;\n\n // Handle expression-based indexes\n if (index.expressions && index.expressions.length > 0) {\n // Wrap each expression in parentheses\n columnsPart = index.expressions.map((expr) => `(${expr})`).join(\", \");\n } else {\n // Regular column indexes\n const columns = index.columns.map((col, i) => {\n const quotedCol = this.driver.dialect.quoteIdentifier(col);\n const direction = index.directions?.[i]?.toUpperCase() ?? \"\";\n return direction ? `${quotedCol} ${direction}` : quotedCol;\n });\n columnsPart = columns.join(\", \");\n }\n\n let sql = `CREATE ${uniqueKeyword}INDEX ${concurrentlyKeyword}${quotedIndexName} ON ${quotedTable} (${columnsPart})`;\n\n // Add INCLUDE clause for covering indexes\n if (index.include && index.include.length > 0) {\n const includeCols = index.include\n .map((col) => this.driver.dialect.quoteIdentifier(col))\n .join(\", \");\n sql += ` INCLUDE (${includeCols})`;\n }\n\n // Add partial index condition\n if (index.where && Object.keys(index.where).length > 0) {\n const conditions = Object.entries(index.where)\n .map(([key, value]) => {\n const quotedKey = this.driver.dialect.quoteIdentifier(key);\n return typeof value === \"string\"\n ? `${quotedKey} = '${value}'`\n : `${quotedKey} = ${value}`;\n })\n .join(\" AND \");\n sql += ` WHERE ${conditions}`;\n }\n\n await this.execute(sql);\n }\n\n /**\n * Drop an index.\n *\n * @param table - Table name\n * @param indexNameOrColumns - Index name or columns\n */\n public async dropIndex(table: string, indexNameOrColumns: string | string[]): Promise<void> {\n let indexName: string;\n\n if (typeof indexNameOrColumns === \"string\") {\n indexName = indexNameOrColumns;\n } else {\n indexName = `idx_${table}_${indexNameOrColumns.join(\"_\")}`;\n }\n\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n await this.execute(`DROP INDEX IF EXISTS ${quotedIndexName}`);\n }\n\n /**\n * Create a unique index.\n *\n * @param table - Table name\n * @param columns - Columns to include\n * @param name - Optional index name\n */\n public async createUniqueIndex(table: string, columns: string[], name?: string): Promise<void> {\n await this.createIndex(table, { columns, unique: true, name });\n }\n\n /**\n * Drop a unique index.\n *\n * @param table - Table name\n * @param columns - Columns in the index\n */\n public async dropUniqueIndex(table: string, columns: string[]): Promise<void> {\n await this.dropIndex(table, columns);\n }\n\n // ============================================================================\n // SPECIALIZED INDEXES\n // ============================================================================\n\n /**\n * Create a full-text search index using GIN.\n *\n * @param table - Table name\n * @param columns - Columns to index\n * @param options - Full-text options\n */\n public async createFullTextIndex(\n table: string,\n columns: string[],\n options?: FullTextIndexOptions,\n ): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const indexName = options?.name ?? `idx_${table}_fulltext_${columns.join(\"_\")}`;\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n const language = options?.language ?? \"english\";\n\n const tsvectors = columns.map((col) => {\n const weight = options?.weights?.[col] ?? \"A\";\n return `setweight(to_tsvector('${language}', COALESCE(${this.driver.dialect.quoteIdentifier(col)}, '')), '${weight}')`;\n });\n\n await this.execute(\n `CREATE INDEX ${quotedIndexName} ON ${quotedTable} USING GIN ((${tsvectors.join(\" || \")}))`,\n );\n }\n\n /**\n * Drop a full-text search index.\n *\n * @param table - Table name\n * @param name - Index name\n */\n public async dropFullTextIndex(table: string, name: string): Promise<void> {\n await this.dropIndex(table, name);\n }\n\n /**\n * Create a geo-spatial index using GiST.\n *\n * @param table - Table name\n * @param column - Geo column\n * @param options - Geo index options\n */\n public async createGeoIndex(\n table: string,\n column: string,\n options?: GeoIndexOptions,\n ): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column);\n const indexName = options?.name ?? `idx_${table}_geo_${column}`;\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n\n await this.execute(\n `CREATE INDEX ${quotedIndexName} ON ${quotedTable} USING GIST (${quotedColumn})`,\n );\n }\n\n /**\n * Drop a geo-spatial index.\n *\n * @param table - Table name\n * @param column - Geo column\n */\n public async dropGeoIndex(table: string, column: string): Promise<void> {\n await this.dropIndex(table, `idx_${table}_geo_${column}`);\n }\n\n /**\n * Create a vector search index (requires pgvector extension).\n *\n * @param table - Table name\n * @param column - Vector column\n * @param options - Vector index options\n */\n public async createVectorIndex(\n table: string,\n column: string,\n options: VectorIndexOptions,\n ): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column);\n const indexName = options.name ?? `idx_${table}_vector_${column}`;\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n\n // Map similarity to pgvector operator class\n const opClass =\n options.similarity === \"euclidean\"\n ? \"vector_l2_ops\"\n : options.similarity === \"dotProduct\"\n ? \"vector_ip_ops\"\n : \"vector_cosine_ops\";\n\n const lists = options.lists ?? 100;\n\n await this.execute(\n `CREATE INDEX ${quotedIndexName} ON ${quotedTable} USING ivfflat (${quotedColumn} ${opClass}) WITH (lists = ${lists})`,\n );\n }\n\n /**\n * Drop a vector search index.\n *\n * @param table - Table name\n * @param column - Vector column\n */\n public async dropVectorIndex(table: string, column: string): Promise<void> {\n await this.dropIndex(table, `idx_${table}_vector_${column}`);\n }\n\n /**\n * Create a TTL index (not natively supported in PostgreSQL).\n *\n * Note: PostgreSQL doesn't have native TTL indexes like MongoDB.\n * This creates a plain index (a `NOW()` predicate is not IMMUTABLE, so a\n * partial index cannot be built) and requires a scheduled job for cleanup.\n *\n * @param table - Table name\n * @param column - Date column\n * @param expireAfterSeconds - Expiration time in seconds\n */\n public async createTTLIndex(\n table: string,\n column: string,\n expireAfterSeconds: number,\n ): Promise<void> {\n // Plain index that keeps the purge `DELETE ... WHERE column < ...` efficient\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column);\n const indexName = `idx_${table}_ttl_${column}`;\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n\n await this.execute(\n `CREATE INDEX ${quotedIndexName} ON ${quotedTable} (${quotedColumn})`,\n );\n\n // Note: User must set up a scheduled job (pg_cron, etc.) to:\n // DELETE FROM table WHERE column < NOW() - INTERVAL 'X seconds'\n }\n\n /**\n * Drop a TTL index.\n *\n * @param table - Table name\n * @param column - Column with TTL index\n */\n public async dropTTLIndex(table: string, column: string): Promise<void> {\n await this.dropIndex(table, `idx_${table}_ttl_${column}`);\n }\n\n /**\n * List all indexes on a table.\n *\n * @param table - Table name\n * @returns Array of index metadata\n */\n public async listIndexes(table: string): Promise<TableIndexInformation[]> {\n const result = await this.driver.query<{ indexname: string; indexdef: string }>(\n `SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = current_schema() AND tablename = $1`,\n [table],\n );\n\n return result.rows.map((row) => {\n const isUnique = row.indexdef.includes(\"UNIQUE\");\n const isPrimary = row.indexname.endsWith(\"_pkey\");\n const columnsMatch = row.indexdef.match(/\\(([^)]+)\\)/);\n // Capture group 1 is present whenever the pattern matched, but that is a\n // property of the regex literal rather than of this expression. `?? \"\"`\n // yields the empty column list the `: []` branch already gives.\n const columns = (columnsMatch?.[1] ?? \"\")\n .split(\",\")\n .map((c) => c.trim().replace(/\"/g, \"\"))\n .filter(Boolean);\n\n let type = \"btree\";\n if (row.indexdef.includes(\"USING GIN\")) type = \"gin\";\n else if (row.indexdef.includes(\"USING GIST\")) type = \"gist\";\n else if (row.indexdef.includes(\"USING HASH\")) type = \"hash\";\n else if (row.indexdef.includes(\"USING ivfflat\")) type = \"ivfflat\";\n\n return {\n name: row.indexname,\n columns,\n type,\n unique: isUnique || isPrimary,\n partial: row.indexdef.includes(\"WHERE\"),\n options: { primary: isPrimary, definition: row.indexdef },\n };\n });\n }\n\n // ============================================================================\n // EXTENSIONS\n // ============================================================================\n\n /**\n * Check if a PostgreSQL extension is available on the database server.\n *\n * @param extension - Extension name (e.g., \"vector\")\n */\n public async isExtensionAvailable(extension: string): Promise<boolean> {\n try {\n const result = await this.driver.query<{ name: string }>(\n `SELECT name FROM pg_available_extensions WHERE name = $1`,\n [extension],\n );\n\n return result?.rows?.length > 0;\n } catch {\n // If we can't check (e.g., permissions issue or pg_available_extensions not accessible),\n // we return true to avoid false positives. Execution will just proceed and fail naturally if it's missing.\n return true;\n }\n }\n\n /**\n * Get the official documentation or installation URL for a PostgreSQL extension.\n *\n * @param extension - Extension name\n */\n public getExtensionDocsUrl(extension: string): string | undefined {\n const guideUrls: Record<string, string> = {\n vector: \"https://github.com/pgvector/pgvector#installation\",\n postgis: \"https://postgis.net/documentation/getting_started/\",\n pg_trgm: \"https://www.postgresql.org/docs/current/pgtrgm.html\",\n uuid_ossp: \"https://www.postgresql.org/docs/current/uuid-ossp.html\",\n };\n\n return guideUrls[extension] ?? `https://www.postgresql.org/docs/current/${extension}.html`;\n }\n\n // ============================================================================\n // CONSTRAINTS\n // ============================================================================\n\n /**\n * Add a foreign key constraint.\n *\n * @param table - Table name\n * @param foreignKey - Foreign key definition\n */\n public async addForeignKey(table: string, foreignKey: ForeignKeyDefinition): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(foreignKey.column);\n const quotedRefTable = this.driver.dialect.quoteIdentifier(foreignKey.referencesTable);\n const quotedRefColumn = this.driver.dialect.quoteIdentifier(foreignKey.referencesColumn);\n\n const constraintName =\n foreignKey.name ?? `fk_${table}_${foreignKey.column}_${foreignKey.referencesTable}`;\n const quotedConstraint = this.driver.dialect.quoteIdentifier(constraintName);\n\n let sql = `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${quotedConstraint} FOREIGN KEY (${quotedColumn}) REFERENCES ${quotedRefTable} (${quotedRefColumn})`;\n\n if (foreignKey.onDelete) {\n sql += ` ON DELETE ${this.mapForeignKeyAction(foreignKey.onDelete)}`;\n }\n\n if (foreignKey.onUpdate) {\n sql += ` ON UPDATE ${this.mapForeignKeyAction(foreignKey.onUpdate)}`;\n }\n\n await this.execute(sql);\n }\n\n /**\n * Drop a foreign key constraint.\n *\n * @param table - Table name\n * @param name - Constraint name\n */\n public async dropForeignKey(table: string, name: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedConstraint = this.driver.dialect.quoteIdentifier(name);\n await this.execute(`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quotedConstraint}`);\n }\n\n /**\n * Add a primary key constraint.\n *\n * @param table - Table name\n * @param columns - Primary key columns\n */\n public async addPrimaryKey(table: string, columns: string[]): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumns = columns.map((c) => this.driver.dialect.quoteIdentifier(c)).join(\", \");\n const constraintName = `pk_${table}`;\n const quotedConstraint = this.driver.dialect.quoteIdentifier(constraintName);\n\n await this.execute(\n `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${quotedConstraint} PRIMARY KEY (${quotedColumns})`,\n );\n }\n\n /**\n * Drop the primary key constraint.\n *\n * @param table - Table name\n */\n public async dropPrimaryKey(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const constraintName = `pk_${table}`;\n const quotedConstraint = this.driver.dialect.quoteIdentifier(constraintName);\n\n await this.execute(`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quotedConstraint}`);\n }\n\n /**\n * Add a CHECK constraint.\n *\n * @param table - Table name\n * @param name - Constraint name\n * @param expression - SQL CHECK expression\n */\n public async addCheck(table: string, name: string, expression: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedName = this.driver.dialect.quoteIdentifier(name);\n const sql = `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${quotedName} CHECK (${expression})`;\n await this.execute(sql);\n }\n\n /**\n * Drop a CHECK constraint.\n *\n * @param table - Table name\n * @param name - Constraint name\n */\n public async dropCheck(table: string, name: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedName = this.driver.dialect.quoteIdentifier(name);\n await this.execute(`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quotedName}`);\n }\n\n // ============================================================================\n // SCHEMA VALIDATION (NOT APPLICABLE FOR PostgreSQL)\n // ============================================================================\n\n /**\n * Set schema validation (no-op for PostgreSQL).\n *\n * PostgreSQL uses column constraints instead.\n */\n public async setSchemaValidation(_table: string, _schema: object): Promise<void> {\n // No-op: PostgreSQL doesn't have MongoDB-style schema validation\n // Use CHECK constraints instead\n }\n\n /**\n * Remove schema validation (no-op for PostgreSQL).\n */\n public async removeSchemaValidation(_table: string): Promise<void> {\n // No-op\n }\n\n // ============================================================================\n // TRANSACTIONS\n // ============================================================================\n\n /**\n * Begin a transaction.\n */\n public async beginTransaction(): Promise<void> {\n await this.execute(\"BEGIN\");\n }\n\n /**\n * Commit the current transaction.\n */\n public async commit(): Promise<void> {\n await this.execute(\"COMMIT\");\n }\n\n /**\n * Rollback the current transaction.\n */\n public async rollback(): Promise<void> {\n await this.execute(\"ROLLBACK\");\n }\n\n /**\n * Whether transactions are supported.\n */\n public supportsTransactions(): boolean {\n return true;\n }\n\n /**\n * Get the default transactional behavior for PostgreSQL.\n *\n * PostgreSQL supports transactional DDL operations, so migrations\n * are wrapped in transactions by default for atomicity and safety.\n *\n * @returns true (PostgreSQL DDL is transactional)\n */\n public getDefaultTransactional(): boolean {\n return true;\n }\n\n // ============================================================================\n // DEFAULTS\n // ============================================================================\n\n /**\n * Get the default UUID generation expression for PostgreSQL.\n *\n * Resolution order:\n * 1. `migrationDefaults.uuidExpression` → raw expression (escape hatch)\n * 2. `migrationDefaults.uuidStrategy` → mapped to PG function\n * 3. Fallback → `gen_random_uuid()` (v4, PG 13+)\n *\n * @param migrationDefaults - Optional overrides from DataSource config\n * @returns PostgreSQL SQL expression for UUID generation\n *\n * @example\n * ```typescript\n * driver.getUuidDefault(); // \"gen_random_uuid()\"\n * driver.getUuidDefault({ uuidStrategy: \"v7\" }); // \"uuidv7()\"\n * driver.getUuidDefault({ uuidExpression: \"uuid_generate_v1mc()\" }); // \"uuid_generate_v1mc()\"\n * ```\n */\n public getUuidDefault(migrationDefaults?: MigrationDefaults): string {\n // Escape hatch: raw expression takes highest precedence\n if (migrationDefaults?.uuidExpression) {\n return migrationDefaults.uuidExpression;\n }\n\n const strategy = migrationDefaults?.uuidStrategy ?? \"v4\";\n\n const strategyMap: Record<UuidStrategy, string> = {\n v4: \"gen_random_uuid()\",\n v7: \"uuidv7()\",\n };\n\n return strategyMap[strategy];\n }\n\n // ============================================================================\n // RAW ACCESS\n // ============================================================================\n\n /**\n * Execute raw operations with direct driver access.\n *\n * @param callback - Callback receiving the driver\n */\n public async raw<T>(callback: (connection: unknown) => Promise<T>): Promise<T> {\n return callback(this.driver);\n }\n\n // ============================================================================\n // HELPERS\n // ============================================================================\n\n /**\n * Execute a SQL statement.\n *\n * @param sql - SQL to execute\n * @param params - Query parameters\n */\n private async execute(sql: string, params: unknown[] = []): Promise<void> {\n await this.driver.query(sql, params);\n\n // DDL changes what the driver's connect-time caches (native-array columns,\n // unique keys) describe, so refresh them.\n if (/^\\s*(CREATE|ALTER|DROP)\\b/i.test(sql)) {\n await this.driver.invalidateSchemaCaches();\n }\n }\n\n /**\n * Map foreign key action to PostgreSQL syntax.\n */\n private mapForeignKeyAction(action: \"cascade\" | \"restrict\" | \"setNull\" | \"noAction\"): string {\n const mapping: Record<string, string> = {\n cascade: \"CASCADE\",\n restrict: \"RESTRICT\",\n setNull: \"SET NULL\",\n noAction: \"NO ACTION\",\n };\n return mapping[action] ?? \"NO ACTION\";\n }\n\n /**\n * Map PostgreSQL data type to ColumnType.\n */\n private mapPostgresTypeToColumnType(pgType: string): ColumnType {\n const typeMap: Record<string, ColumnType> = {\n \"character varying\": \"string\",\n varchar: \"string\",\n character: \"char\",\n char: \"char\",\n text: \"text\",\n integer: \"integer\",\n int: \"integer\",\n smallint: \"smallInteger\",\n bigint: \"bigInteger\",\n real: \"float\",\n \"double precision\": \"double\",\n numeric: \"decimal\",\n decimal: \"decimal\",\n boolean: \"boolean\",\n date: \"date\",\n timestamp: \"dateTime\",\n \"timestamp without time zone\": \"dateTime\",\n \"timestamp with time zone\": \"timestamp\",\n time: \"time\",\n \"time without time zone\": \"time\",\n json: \"json\",\n jsonb: \"json\",\n bytea: \"binary\",\n uuid: \"uuid\",\n inet: \"ipAddress\",\n macaddr: \"macAddress\",\n point: \"point\",\n polygon: \"polygon\",\n line: \"lineString\",\n geometry: \"geometry\",\n // PostgreSQL array types\n \"integer[]\": \"arrayInt\",\n \"int[]\": \"arrayInt\",\n \"bigint[]\": \"arrayBigInt\",\n \"real[]\": \"arrayFloat\",\n \"decimal[]\": \"arrayDecimal\",\n \"numeric[]\": \"arrayDecimal\",\n \"boolean[]\": \"arrayBoolean\",\n \"text[]\": \"arrayText\",\n \"date[]\": \"arrayDate\",\n \"timestamp with time zone[]\": \"arrayTimestamp\",\n \"timestamptz[]\": \"arrayTimestamp\",\n \"uuid[]\": \"arrayUuid\",\n \"jsonb[]\": \"arrayJson\",\n \"json[]\": \"arrayJson\",\n };\n\n return typeMap[pgType.toLowerCase()] ?? \"string\";\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"postgres-migration-driver.mjs","names":[],"sources":["../../../../../../../../cascade/src/drivers/postgres/postgres-migration-driver.ts"],"sourcesContent":["/**\n * PostgreSQL Migration Driver\n *\n * Implements the MigrationDriverContract for PostgreSQL DDL operations.\n * Provides methods for creating/dropping tables, columns, indexes,\n * and constraints.\n *\n * @module cascade/drivers/postgres\n */\n\nimport { databaseTransactionContext } from \"../../context/database-transaction-context\";\nimport type {\n ColumnDefinition,\n ColumnType,\n ForeignKeyDefinition,\n FullTextIndexOptions,\n GeoIndexOptions,\n IndexDefinition,\n MigrationDriverContract,\n TableIndexInformation,\n VectorIndexOptions,\n} from \"../../contracts/migration-driver.contract\";\nimport type { MigrationDefaults, UuidStrategy } from \"../../types\";\nimport type { PostgresDriver } from \"./postgres-driver\";\nimport { postgresTTLPurgeComment, registerPostgresTTLPurgeJob, unregisterPostgresTTLPurgeJob } from \"./postgres-ttl-purge\";\n\n/**\n * PostgreSQL Migration Driver.\n *\n * Handles database schema operations for PostgreSQL including:\n * - Table creation and deletion\n * - Column management\n * - Index creation (B-tree, GIN, GiST, etc.)\n * - Constraint management (foreign keys, unique, etc.)\n *\n * @example\n * ```typescript\n * const migrationDriver = driver.migrationDriver();\n *\n * // Create a table\n * await migrationDriver.createTable('users');\n *\n * // Add columns\n * await migrationDriver.addColumn('users', {\n * name: 'email',\n * type: 'string',\n * length: 255,\n * nullable: false\n * });\n *\n * // Create unique index\n * await migrationDriver.createUniqueIndex('users', ['email']);\n * ```\n */\nexport class PostgresMigrationDriver implements MigrationDriverContract {\n /**\n * Active transaction client (if any).\n */\n private get transactionClient(): unknown {\n return databaseTransactionContext.getSession();\n }\n\n /**\n * Create a new migration driver.\n *\n * @param driver - The PostgreSQL driver instance\n */\n public constructor(public readonly driver: PostgresDriver) {}\n\n // ============================================================================\n // TABLE OPERATIONS\n // ============================================================================\n\n /**\n * Create a new table with a default id column.\n *\n * @param table - Table name\n */\n public async createTable(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n // Create empty table - columns are added via addColumn operations\n await this.execute(`CREATE TABLE ${quotedTable} ()`);\n }\n\n /**\n * Create table if it doesn't exist.\n *\n * @param table - Table name\n */\n public async createTableIfNotExists(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n // Create empty table if not exists - columns are added via addColumn operations\n await this.execute(`CREATE TABLE IF NOT EXISTS ${quotedTable} ()`);\n }\n\n /**\n * Drop an existing table.\n *\n * @param table - Table name\n */\n public async dropTable(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n await this.execute(`DROP TABLE ${quotedTable} CASCADE`);\n }\n\n /**\n * Drop table if it exists.\n *\n * @param table - Table name\n */\n public async dropTableIfExists(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n await this.execute(`DROP TABLE IF EXISTS ${quotedTable} CASCADE`);\n }\n\n /**\n * Rename a table.\n *\n * @param from - Current table name\n * @param to - New table name\n */\n public async renameTable(from: string, to: string): Promise<void> {\n const quotedFrom = this.driver.dialect.quoteIdentifier(from);\n const quotedTo = this.driver.dialect.quoteIdentifier(to);\n await this.execute(`ALTER TABLE ${quotedFrom} RENAME TO ${quotedTo}`);\n }\n\n /**\n * Truncate a table — remove all rows efficiently.\n *\n * @param table - Table name\n */\n public async truncateTable(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n await this.execute(`TRUNCATE TABLE ${quotedTable}`);\n }\n\n /**\n * Check if a table exists.\n *\n * @param table - Table name\n * @returns Whether the table exists\n */\n public async tableExists(table: string): Promise<boolean> {\n const result = await this.driver.query<{ exists: boolean }>(\n `SELECT EXISTS (\n SELECT FROM information_schema.tables\n WHERE table_schema = current_schema()\n AND table_name = $1\n )`,\n [table],\n );\n return result.rows[0]?.exists ?? false;\n }\n\n /**\n * List all columns in a table.\n *\n * @param table - Table name\n * @returns Array of column definitions\n */\n public async listColumns(table: string): Promise<ColumnDefinition[]> {\n const result = await this.driver.query<{\n column_name: string;\n data_type: string;\n character_maximum_length: number | null;\n numeric_precision: number | null;\n numeric_scale: number | null;\n is_nullable: string;\n column_default: string | null;\n }>(\n `SELECT\n column_name,\n data_type,\n character_maximum_length,\n numeric_precision,\n numeric_scale,\n is_nullable,\n column_default\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name = $1\n ORDER BY ordinal_position`,\n [table],\n );\n\n return result.rows.map((row) => ({\n name: row.column_name,\n type: this.mapPostgresTypeToColumnType(row.data_type),\n length: row.character_maximum_length ?? undefined,\n precision: row.numeric_precision ?? undefined,\n scale: row.numeric_scale ?? undefined,\n nullable: row.is_nullable === \"YES\",\n defaultValue: row.column_default ?? undefined,\n }));\n }\n\n /**\n * List all tables in the current database.\n *\n * @returns Array of table names\n */\n public async listTables(): Promise<string[]> {\n const result = await this.driver.query<{ table_name: string }>(\n `SELECT table_name\n FROM information_schema.tables\n WHERE table_schema = current_schema()\n ORDER BY table_name`,\n );\n\n return result.rows.map((row) => row.table_name);\n }\n\n /**\n * Ensure the migrations tracking table exists.\n *\n * Creates the table with proper schema if it doesn't exist.\n *\n * @param tableName - Name of the migrations table\n */\n public async ensureMigrationsTable(tableName: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(tableName);\n\n await this.execute(`\n CREATE TABLE IF NOT EXISTS ${quotedTable} (\n \"id\" SERIAL PRIMARY KEY,\n \"name\" VARCHAR(255) NOT NULL UNIQUE,\n \"batch\" INTEGER NOT NULL,\n \"executedAt\" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),\n \"createdAt\" TIMESTAMP WITH TIME ZONE\n )\n `);\n }\n\n // ============================================================================\n // COLUMN OPERATIONS\n // ============================================================================\n\n /**\n * Add a column to an existing table.\n *\n * @param table - Table name\n * @param column - Column definition\n */\n public async addColumn(table: string, column: ColumnDefinition): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column.name);\n\n // For auto-increment integers, use SERIAL/BIGSERIAL instead of INTEGER/BIGINT\n let sqlType: string;\n if (column.autoIncrement) {\n if (column.type === \"bigInteger\") {\n sqlType = \"BIGSERIAL\";\n } else {\n sqlType = \"SERIAL\";\n }\n } else {\n sqlType = this.driver.dialect.getSqlType(column.type, {\n length: column.length,\n precision: column.precision,\n scale: column.scale,\n dimensions: column.dimensions,\n });\n }\n\n let sql = `ALTER TABLE ${quotedTable} ADD COLUMN ${quotedColumn} ${sqlType}`;\n\n // Handle generated columns\n if (column.generated) {\n sql += ` GENERATED ALWAYS AS (${column.generated.expression})`;\n if (column.generated.stored) {\n sql += \" STORED\";\n }\n // PostgreSQL only supports STORED, not VIRTUAL\n // If virtual is requested, it's simply ignored (PostgreSQL doesn't support it)\n } else {\n // SERIAL/BIGSERIAL are always NOT NULL, so skip for those\n if (!column.autoIncrement && column.nullable === false) {\n sql += \" NOT NULL\";\n }\n\n if (column.defaultValue !== undefined) {\n // Check for special CURRENT_TIMESTAMP marker\n if (\n typeof column.defaultValue === \"object\" &&\n column.defaultValue !== null &&\n (column.defaultValue as any).__type === \"CURRENT_TIMESTAMP\"\n ) {\n sql += \" DEFAULT NOW()\";\n } else if (column.isRawDefault === false) {\n // Explicit string literal - escape it properly\n const escaped = String(column.defaultValue).replace(/'/g, \"''\");\n sql += ` DEFAULT '${escaped}'`;\n } else if (typeof column.defaultValue === \"boolean\") {\n // Boolean values\n sql += ` DEFAULT ${column.defaultValue ? \"TRUE\" : \"FALSE\"}`;\n } else if (typeof column.defaultValue === \"number\") {\n // Numeric values\n sql += ` DEFAULT ${column.defaultValue}`;\n } else {\n // Raw SQL expression (default behavior when isRawDefault is true or undefined)\n sql += ` DEFAULT ${column.defaultValue}`;\n }\n }\n\n // Handle primary key\n if (column.primary) {\n sql += \" PRIMARY KEY\";\n }\n\n // Handle unique constraint\n if (column.unique) {\n sql += \" UNIQUE\";\n }\n }\n\n await this.execute(sql);\n }\n\n /**\n * Drop a column from a table.\n *\n * @param table - Table name\n * @param column - Column name\n */\n public async dropColumn(table: string, column: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column);\n await this.execute(`ALTER TABLE ${quotedTable} DROP COLUMN ${quotedColumn}`);\n }\n\n /**\n * Drop multiple columns from a table.\n *\n * @param table - Table name\n * @param columns - Column names\n */\n public async dropColumns(table: string, columns: string[]): Promise<void> {\n for (const column of columns) {\n await this.dropColumn(table, column);\n }\n }\n\n /**\n * Rename a column.\n *\n * @param table - Table name\n * @param from - Current column name\n * @param to - New column name\n */\n public async renameColumn(table: string, from: string, to: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedFrom = this.driver.dialect.quoteIdentifier(from);\n const quotedTo = this.driver.dialect.quoteIdentifier(to);\n await this.execute(`ALTER TABLE ${quotedTable} RENAME COLUMN ${quotedFrom} TO ${quotedTo}`);\n }\n\n /**\n * Modify an existing column.\n *\n * @param table - Table name\n * @param column - New column definition\n */\n public async modifyColumn(table: string, column: ColumnDefinition): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column.name);\n const sqlType = this.driver.dialect.getSqlType(column.type, {\n length: column.length,\n precision: column.precision,\n scale: column.scale,\n dimensions: column.dimensions,\n });\n\n // PostgreSQL requires separate ALTER statements for type and nullability\n await this.execute(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} TYPE ${sqlType}`);\n\n if (column.nullable === false) {\n await this.execute(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} SET NOT NULL`);\n } else if (column.nullable === true) {\n await this.execute(`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} DROP NOT NULL`);\n }\n\n if (column.defaultValue !== undefined) {\n let defaultVal: string;\n\n // Check for special CURRENT_TIMESTAMP marker\n if (\n typeof column.defaultValue === \"object\" &&\n column.defaultValue !== null &&\n (column.defaultValue as any).__type === \"CURRENT_TIMESTAMP\"\n ) {\n defaultVal = \"NOW()\";\n } else if (typeof column.defaultValue === \"string\") {\n defaultVal = `'${column.defaultValue}'`;\n } else {\n defaultVal = String(column.defaultValue);\n }\n\n await this.execute(\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} SET DEFAULT ${defaultVal}`,\n );\n }\n }\n\n /**\n * Create standard timestamp columns (created_at, updated_at).\n *\n * PostgreSQL implementation creates TIMESTAMPTZ columns with NOW() defaults.\n *\n * @param table - Table name\n */\n public async createTimestampColumns(table: string): Promise<void> {\n await this.addColumn(table, {\n name: \"created_at\",\n type: \"timestamp\",\n nullable: false,\n defaultValue: \"NOW()\",\n isRawDefault: true,\n });\n\n await this.addColumn(table, {\n name: \"updated_at\",\n type: \"timestamp\",\n nullable: false,\n defaultValue: \"NOW()\",\n isRawDefault: true,\n });\n }\n\n // ============================================================================\n // INDEX OPERATIONS\n // ============================================================================\n\n /**\n * Create an index on one or more columns.\n *\n * Supports:\n * - Regular column indexes\n * - Expression-based indexes (e.g., `lower(email)`)\n * - Covering indexes (INCLUDE clause)\n * - Concurrent index creation (CONCURRENTLY keyword)\n *\n * @param table - Table name\n * @param index - Index definition\n */\n public async createIndex(table: string, index: IndexDefinition): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const indexName = index.name ?? `idx_${table}_${index.columns.join(\"_\")}`;\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n const uniqueKeyword = index.unique ? \"UNIQUE \" : \"\";\n const concurrentlyKeyword = index.concurrently ? \"CONCURRENTLY \" : \"\";\n\n let columnsPart: string;\n\n // Handle expression-based indexes\n if (index.expressions && index.expressions.length > 0) {\n // Wrap each expression in parentheses\n columnsPart = index.expressions.map((expr) => `(${expr})`).join(\", \");\n } else {\n // Regular column indexes\n const columns = index.columns.map((col, i) => {\n const quotedCol = this.driver.dialect.quoteIdentifier(col);\n const direction = index.directions?.[i]?.toUpperCase() ?? \"\";\n return direction ? `${quotedCol} ${direction}` : quotedCol;\n });\n columnsPart = columns.join(\", \");\n }\n\n let sql = `CREATE ${uniqueKeyword}INDEX ${concurrentlyKeyword}${quotedIndexName} ON ${quotedTable} (${columnsPart})`;\n\n // Add INCLUDE clause for covering indexes\n if (index.include && index.include.length > 0) {\n const includeCols = index.include\n .map((col) => this.driver.dialect.quoteIdentifier(col))\n .join(\", \");\n sql += ` INCLUDE (${includeCols})`;\n }\n\n // Add partial index condition\n if (index.where && Object.keys(index.where).length > 0) {\n const conditions = Object.entries(index.where)\n .map(([key, value]) => {\n const quotedKey = this.driver.dialect.quoteIdentifier(key);\n return typeof value === \"string\"\n ? `${quotedKey} = '${value}'`\n : `${quotedKey} = ${value}`;\n })\n .join(\" AND \");\n sql += ` WHERE ${conditions}`;\n }\n\n await this.execute(sql);\n }\n\n /**\n * Drop an index.\n *\n * @param table - Table name\n * @param indexNameOrColumns - Index name or columns\n */\n public async dropIndex(table: string, indexNameOrColumns: string | string[]): Promise<void> {\n let indexName: string;\n\n if (typeof indexNameOrColumns === \"string\") {\n indexName = indexNameOrColumns;\n } else {\n indexName = `idx_${table}_${indexNameOrColumns.join(\"_\")}`;\n }\n\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n await this.execute(`DROP INDEX IF EXISTS ${quotedIndexName}`);\n }\n\n /**\n * Create a unique index.\n *\n * @param table - Table name\n * @param columns - Columns to include\n * @param name - Optional index name\n */\n public async createUniqueIndex(table: string, columns: string[], name?: string): Promise<void> {\n await this.createIndex(table, { columns, unique: true, name });\n }\n\n /**\n * Drop a unique index.\n *\n * @param table - Table name\n * @param columns - Columns in the index\n */\n public async dropUniqueIndex(table: string, columns: string[]): Promise<void> {\n await this.dropIndex(table, columns);\n }\n\n // ============================================================================\n // SPECIALIZED INDEXES\n // ============================================================================\n\n /**\n * Create a full-text search index using GIN.\n *\n * @param table - Table name\n * @param columns - Columns to index\n * @param options - Full-text options\n */\n public async createFullTextIndex(\n table: string,\n columns: string[],\n options?: FullTextIndexOptions,\n ): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const indexName = options?.name ?? `idx_${table}_fulltext_${columns.join(\"_\")}`;\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n const language = options?.language ?? \"english\";\n\n const tsvectors = columns.map((col) => {\n const weight = options?.weights?.[col] ?? \"A\";\n return `setweight(to_tsvector('${language}', COALESCE(${this.driver.dialect.quoteIdentifier(col)}, '')), '${weight}')`;\n });\n\n await this.execute(\n `CREATE INDEX ${quotedIndexName} ON ${quotedTable} USING GIN ((${tsvectors.join(\" || \")}))`,\n );\n }\n\n /**\n * Drop a full-text search index.\n *\n * @param table - Table name\n * @param name - Index name\n */\n public async dropFullTextIndex(table: string, name: string): Promise<void> {\n await this.dropIndex(table, name);\n }\n\n /**\n * Create a geo-spatial index using GiST.\n *\n * @param table - Table name\n * @param column - Geo column\n * @param options - Geo index options\n */\n public async createGeoIndex(\n table: string,\n column: string,\n options?: GeoIndexOptions,\n ): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column);\n const indexName = options?.name ?? `idx_${table}_geo_${column}`;\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n\n await this.execute(\n `CREATE INDEX ${quotedIndexName} ON ${quotedTable} USING GIST (${quotedColumn})`,\n );\n }\n\n /**\n * Drop a geo-spatial index.\n *\n * @param table - Table name\n * @param column - Geo column\n */\n public async dropGeoIndex(table: string, column: string): Promise<void> {\n await this.dropIndex(table, `idx_${table}_geo_${column}`);\n }\n\n /**\n * Create a vector search index (requires pgvector extension).\n *\n * @param table - Table name\n * @param column - Vector column\n * @param options - Vector index options\n */\n public async createVectorIndex(\n table: string,\n column: string,\n options: VectorIndexOptions,\n ): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column);\n const indexName = options.name ?? `idx_${table}_vector_${column}`;\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n\n // Map similarity to pgvector operator class\n const opClass =\n options.similarity === \"euclidean\"\n ? \"vector_l2_ops\"\n : options.similarity === \"dotProduct\"\n ? \"vector_ip_ops\"\n : \"vector_cosine_ops\";\n\n const lists = options.lists ?? 100;\n\n await this.execute(\n `CREATE INDEX ${quotedIndexName} ON ${quotedTable} USING ivfflat (${quotedColumn} ${opClass}) WITH (lists = ${lists})`,\n );\n }\n\n /**\n * Drop a vector search index.\n *\n * @param table - Table name\n * @param column - Vector column\n */\n public async dropVectorIndex(table: string, column: string): Promise<void> {\n await this.dropIndex(table, `idx_${table}_vector_${column}`);\n }\n\n /**\n * Create a TTL index (not natively supported in PostgreSQL).\n *\n * Note: PostgreSQL doesn't have native TTL indexes like MongoDB.\n * This creates a plain index (a `NOW()` predicate is not IMMUTABLE, so a\n * partial index cannot be built) and registers a scheduler purge job.\n *\n * @param table - Table name\n * @param column - Date column\n * @param expireAfterSeconds - Expiration time in seconds\n */\n public async createTTLIndex(\n table: string,\n column: string,\n expireAfterSeconds: number,\n ): Promise<void> {\n // Plain index that keeps the purge `DELETE ... WHERE column < ...` efficient\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(column);\n const indexName = `idx_${table}_ttl_${column}`;\n const quotedIndexName = this.driver.dialect.quoteIdentifier(indexName);\n\n await this.execute(\n `CREATE INDEX ${quotedIndexName} ON ${quotedTable} (${quotedColumn})`,\n );\n\n const comment = postgresTTLPurgeComment(table, column, expireAfterSeconds).replaceAll(\"'\", \"''\");\n await this.execute(`COMMENT ON INDEX ${quotedIndexName} IS '${comment}'`);\n\n registerPostgresTTLPurgeJob(this.driver, table, column, expireAfterSeconds);\n }\n\n /**\n * Drop a TTL index.\n *\n * @param table - Table name\n * @param column - Column with TTL index\n */\n public async dropTTLIndex(table: string, column: string): Promise<void> {\n await this.dropIndex(table, `idx_${table}_ttl_${column}`);\n unregisterPostgresTTLPurgeJob(this.driver, table, column);\n }\n\n /**\n * List all indexes on a table.\n *\n * @param table - Table name\n * @returns Array of index metadata\n */\n public async listIndexes(table: string): Promise<TableIndexInformation[]> {\n const result = await this.driver.query<{ indexname: string; indexdef: string }>(\n `SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = current_schema() AND tablename = $1`,\n [table],\n );\n\n return result.rows.map((row) => {\n const isUnique = row.indexdef.includes(\"UNIQUE\");\n const isPrimary = row.indexname.endsWith(\"_pkey\");\n const columnsMatch = row.indexdef.match(/\\(([^)]+)\\)/);\n // Capture group 1 is present whenever the pattern matched, but that is a\n // property of the regex literal rather than of this expression. `?? \"\"`\n // yields the empty column list the `: []` branch already gives.\n const columns = (columnsMatch?.[1] ?? \"\")\n .split(\",\")\n .map((c) => c.trim().replace(/\"/g, \"\"))\n .filter(Boolean);\n\n let type = \"btree\";\n if (row.indexdef.includes(\"USING GIN\")) type = \"gin\";\n else if (row.indexdef.includes(\"USING GIST\")) type = \"gist\";\n else if (row.indexdef.includes(\"USING HASH\")) type = \"hash\";\n else if (row.indexdef.includes(\"USING ivfflat\")) type = \"ivfflat\";\n\n return {\n name: row.indexname,\n columns,\n type,\n unique: isUnique || isPrimary,\n partial: row.indexdef.includes(\"WHERE\"),\n options: { primary: isPrimary, definition: row.indexdef },\n };\n });\n }\n\n // ============================================================================\n // EXTENSIONS\n // ============================================================================\n\n /**\n * Check if a PostgreSQL extension is available on the database server.\n *\n * @param extension - Extension name (e.g., \"vector\")\n */\n public async isExtensionAvailable(extension: string): Promise<boolean> {\n try {\n const result = await this.driver.query<{ name: string }>(\n `SELECT name FROM pg_available_extensions WHERE name = $1`,\n [extension],\n );\n\n return result?.rows?.length > 0;\n } catch {\n // If we can't check (e.g., permissions issue or pg_available_extensions not accessible),\n // we return true to avoid false positives. Execution will just proceed and fail naturally if it's missing.\n return true;\n }\n }\n\n /**\n * Get the official documentation or installation URL for a PostgreSQL extension.\n *\n * @param extension - Extension name\n */\n public getExtensionDocsUrl(extension: string): string | undefined {\n const guideUrls: Record<string, string> = {\n vector: \"https://github.com/pgvector/pgvector#installation\",\n postgis: \"https://postgis.net/documentation/getting_started/\",\n pg_trgm: \"https://www.postgresql.org/docs/current/pgtrgm.html\",\n uuid_ossp: \"https://www.postgresql.org/docs/current/uuid-ossp.html\",\n };\n\n return guideUrls[extension] ?? `https://www.postgresql.org/docs/current/${extension}.html`;\n }\n\n // ============================================================================\n // CONSTRAINTS\n // ============================================================================\n\n /**\n * Add a foreign key constraint.\n *\n * @param table - Table name\n * @param foreignKey - Foreign key definition\n */\n public async addForeignKey(table: string, foreignKey: ForeignKeyDefinition): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumn = this.driver.dialect.quoteIdentifier(foreignKey.column);\n const quotedRefTable = this.driver.dialect.quoteIdentifier(foreignKey.referencesTable);\n const quotedRefColumn = this.driver.dialect.quoteIdentifier(foreignKey.referencesColumn);\n\n const constraintName =\n foreignKey.name ?? `fk_${table}_${foreignKey.column}_${foreignKey.referencesTable}`;\n const quotedConstraint = this.driver.dialect.quoteIdentifier(constraintName);\n\n let sql = `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${quotedConstraint} FOREIGN KEY (${quotedColumn}) REFERENCES ${quotedRefTable} (${quotedRefColumn})`;\n\n if (foreignKey.onDelete) {\n sql += ` ON DELETE ${this.mapForeignKeyAction(foreignKey.onDelete)}`;\n }\n\n if (foreignKey.onUpdate) {\n sql += ` ON UPDATE ${this.mapForeignKeyAction(foreignKey.onUpdate)}`;\n }\n\n await this.execute(sql);\n }\n\n /**\n * Drop a foreign key constraint.\n *\n * @param table - Table name\n * @param name - Constraint name\n */\n public async dropForeignKey(table: string, name: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedConstraint = this.driver.dialect.quoteIdentifier(name);\n await this.execute(`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quotedConstraint}`);\n }\n\n /**\n * Add a primary key constraint.\n *\n * @param table - Table name\n * @param columns - Primary key columns\n */\n public async addPrimaryKey(table: string, columns: string[]): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedColumns = columns.map((c) => this.driver.dialect.quoteIdentifier(c)).join(\", \");\n const constraintName = `pk_${table}`;\n const quotedConstraint = this.driver.dialect.quoteIdentifier(constraintName);\n\n await this.execute(\n `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${quotedConstraint} PRIMARY KEY (${quotedColumns})`,\n );\n }\n\n /**\n * Drop the primary key constraint.\n *\n * @param table - Table name\n */\n public async dropPrimaryKey(table: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const constraintName = `pk_${table}`;\n const quotedConstraint = this.driver.dialect.quoteIdentifier(constraintName);\n\n await this.execute(`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quotedConstraint}`);\n }\n\n /**\n * Add a CHECK constraint.\n *\n * @param table - Table name\n * @param name - Constraint name\n * @param expression - SQL CHECK expression\n */\n public async addCheck(table: string, name: string, expression: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedName = this.driver.dialect.quoteIdentifier(name);\n const sql = `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${quotedName} CHECK (${expression})`;\n await this.execute(sql);\n }\n\n /**\n * Drop a CHECK constraint.\n *\n * @param table - Table name\n * @param name - Constraint name\n */\n public async dropCheck(table: string, name: string): Promise<void> {\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\n const quotedName = this.driver.dialect.quoteIdentifier(name);\n await this.execute(`ALTER TABLE ${quotedTable} DROP CONSTRAINT ${quotedName}`);\n }\n\n // ============================================================================\n // SCHEMA VALIDATION (NOT APPLICABLE FOR PostgreSQL)\n // ============================================================================\n\n /**\n * Set schema validation (no-op for PostgreSQL).\n *\n * PostgreSQL uses column constraints instead.\n */\n public async setSchemaValidation(_table: string, _schema: object): Promise<void> {\n // No-op: PostgreSQL doesn't have MongoDB-style schema validation\n // Use CHECK constraints instead\n }\n\n /**\n * Remove schema validation (no-op for PostgreSQL).\n */\n public async removeSchemaValidation(_table: string): Promise<void> {\n // No-op\n }\n\n // ============================================================================\n // TRANSACTIONS\n // ============================================================================\n\n /**\n * Begin a transaction.\n */\n public async beginTransaction(): Promise<void> {\n await this.execute(\"BEGIN\");\n }\n\n /**\n * Commit the current transaction.\n */\n public async commit(): Promise<void> {\n await this.execute(\"COMMIT\");\n }\n\n /**\n * Rollback the current transaction.\n */\n public async rollback(): Promise<void> {\n await this.execute(\"ROLLBACK\");\n }\n\n /**\n * Whether transactions are supported.\n */\n public supportsTransactions(): boolean {\n return true;\n }\n\n /**\n * Get the default transactional behavior for PostgreSQL.\n *\n * PostgreSQL supports transactional DDL operations, so migrations\n * are wrapped in transactions by default for atomicity and safety.\n *\n * @returns true (PostgreSQL DDL is transactional)\n */\n public getDefaultTransactional(): boolean {\n return true;\n }\n\n // ============================================================================\n // DEFAULTS\n // ============================================================================\n\n /**\n * Get the default UUID generation expression for PostgreSQL.\n *\n * Resolution order:\n * 1. `migrationDefaults.uuidExpression` → raw expression (escape hatch)\n * 2. `migrationDefaults.uuidStrategy` → mapped to PG function\n * 3. Fallback → `gen_random_uuid()` (v4, PG 13+)\n *\n * @param migrationDefaults - Optional overrides from DataSource config\n * @returns PostgreSQL SQL expression for UUID generation\n *\n * @example\n * ```typescript\n * driver.getUuidDefault(); // \"gen_random_uuid()\"\n * driver.getUuidDefault({ uuidStrategy: \"v7\" }); // \"uuidv7()\"\n * driver.getUuidDefault({ uuidExpression: \"uuid_generate_v1mc()\" }); // \"uuid_generate_v1mc()\"\n * ```\n */\n public getUuidDefault(migrationDefaults?: MigrationDefaults): string {\n // Escape hatch: raw expression takes highest precedence\n if (migrationDefaults?.uuidExpression) {\n return migrationDefaults.uuidExpression;\n }\n\n const strategy = migrationDefaults?.uuidStrategy ?? \"v4\";\n\n const strategyMap: Record<UuidStrategy, string> = {\n v4: \"gen_random_uuid()\",\n v7: \"uuidv7()\",\n };\n\n return strategyMap[strategy];\n }\n\n // ============================================================================\n // RAW ACCESS\n // ============================================================================\n\n /**\n * Execute raw operations with direct driver access.\n *\n * @param callback - Callback receiving the driver\n */\n public async raw<T>(callback: (connection: unknown) => Promise<T>): Promise<T> {\n return callback(this.driver);\n }\n\n // ============================================================================\n // HELPERS\n // ============================================================================\n\n /**\n * Execute a SQL statement.\n *\n * @param sql - SQL to execute\n * @param params - Query parameters\n */\n private async execute(sql: string, params: unknown[] = []): Promise<void> {\n await this.driver.query(sql, params);\n\n // DDL changes what the driver's connect-time caches (native-array columns,\n // unique keys) describe, so refresh them.\n if (/^\\s*(CREATE|ALTER|DROP)\\b/i.test(sql)) {\n await this.driver.invalidateSchemaCaches();\n }\n }\n\n /**\n * Map foreign key action to PostgreSQL syntax.\n */\n private mapForeignKeyAction(action: \"cascade\" | \"restrict\" | \"setNull\" | \"noAction\"): string {\n const mapping: Record<string, string> = {\n cascade: \"CASCADE\",\n restrict: \"RESTRICT\",\n setNull: \"SET NULL\",\n noAction: \"NO ACTION\",\n };\n return mapping[action] ?? \"NO ACTION\";\n }\n\n /**\n * Map PostgreSQL data type to ColumnType.\n */\n private mapPostgresTypeToColumnType(pgType: string): ColumnType {\n const typeMap: Record<string, ColumnType> = {\n \"character varying\": \"string\",\n varchar: \"string\",\n character: \"char\",\n char: \"char\",\n text: \"text\",\n integer: \"integer\",\n int: \"integer\",\n smallint: \"smallInteger\",\n bigint: \"bigInteger\",\n real: \"float\",\n \"double precision\": \"double\",\n numeric: \"decimal\",\n decimal: \"decimal\",\n boolean: \"boolean\",\n date: \"date\",\n timestamp: \"dateTime\",\n \"timestamp without time zone\": \"dateTime\",\n \"timestamp with time zone\": \"timestamp\",\n time: \"time\",\n \"time without time zone\": \"time\",\n json: \"json\",\n jsonb: \"json\",\n bytea: \"binary\",\n uuid: \"uuid\",\n inet: \"ipAddress\",\n macaddr: \"macAddress\",\n point: \"point\",\n polygon: \"polygon\",\n line: \"lineString\",\n geometry: \"geometry\",\n // PostgreSQL array types\n \"integer[]\": \"arrayInt\",\n \"int[]\": \"arrayInt\",\n \"bigint[]\": \"arrayBigInt\",\n \"real[]\": \"arrayFloat\",\n \"decimal[]\": \"arrayDecimal\",\n \"numeric[]\": \"arrayDecimal\",\n \"boolean[]\": \"arrayBoolean\",\n \"text[]\": \"arrayText\",\n \"date[]\": \"arrayDate\",\n \"timestamp with time zone[]\": \"arrayTimestamp\",\n \"timestamptz[]\": \"arrayTimestamp\",\n \"uuid[]\": \"arrayUuid\",\n \"jsonb[]\": \"arrayJson\",\n \"json[]\": \"arrayJson\",\n };\n\n return typeMap[pgType.toLowerCase()] ?? \"string\";\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ import { job, scheduler } from "@warlock.js/scheduler";
2
+
3
+ //#region ../cascade/src/drivers/postgres/postgres-ttl-purge.ts
4
+ const TTL_PURGE_INTERVAL_MINUTES = 1;
5
+ const TTL_COMMENT_PREFIX = "warlock_ttl:";
6
+ let nextDriverId = 0;
7
+ const driverIds = /* @__PURE__ */ new WeakMap();
8
+ const driverJobs = /* @__PURE__ */ new WeakMap();
9
+ function getDriverId(driver) {
10
+ let id = driverIds.get(driver);
11
+ if (id === void 0) {
12
+ id = nextDriverId++;
13
+ driverIds.set(driver, id);
14
+ }
15
+ return id;
16
+ }
17
+ /** Persist the retention rule with the index so a restarted process can restore its job. */
18
+ function postgresTTLPurgeComment(table, column, expireAfterSeconds) {
19
+ return TTL_COMMENT_PREFIX + JSON.stringify({
20
+ table,
21
+ column,
22
+ expireAfterSeconds
23
+ });
24
+ }
25
+ /**
26
+ * Return the scheduler job name for one PostgreSQL TTL index.
27
+ *
28
+ * The driver id prevents two data sources with identically named tables from
29
+ * replacing each other's purge jobs on the shared scheduler singleton.
30
+ */
31
+ function postgresTTLPurgeJobName(driver, table, column) {
32
+ return `cascade:postgres:${getDriverId(driver)}:ttl:${table}:${column}`;
33
+ }
34
+ /**
35
+ * Delete rows which have outlived a PostgreSQL TTL index's retention period.
36
+ *
37
+ * Identifiers are produced by the dialect, while the retention interval stays
38
+ * parameterized so a migration value can never become executable SQL.
39
+ */
40
+ async function purgeExpiredPostgresRows(driver, table, column, expireAfterSeconds) {
41
+ const quotedTable = driver.dialect.quoteIdentifier(table);
42
+ const quotedColumn = driver.dialect.quoteIdentifier(column);
43
+ await driver.query(`DELETE FROM ${quotedTable} WHERE ${quotedColumn} < NOW() - ($1 * INTERVAL '1 second')`, [expireAfterSeconds]);
44
+ }
45
+ /**
46
+ * Register the in-process scheduler purge job for a PostgreSQL TTL index.
47
+ *
48
+ * The application owns the scheduler lifecycle: callers must start the shared
49
+ * scheduler during bootstrap. Re-registering replaces a previous job for the
50
+ * same driver/index so repeated migration setup cannot leak callbacks.
51
+ */
52
+ function registerPostgresTTLPurgeJob(driver, table, column, expireAfterSeconds) {
53
+ const name = postgresTTLPurgeJobName(driver, table, column);
54
+ scheduler.removeJob(name);
55
+ scheduler.addJob(job(name, () => purgeExpiredPostgresRows(driver, table, column, expireAfterSeconds)).everyMinutes(TTL_PURGE_INTERVAL_MINUTES));
56
+ let names = driverJobs.get(driver);
57
+ if (!names) {
58
+ names = /* @__PURE__ */ new Set();
59
+ driverJobs.set(driver, names);
60
+ }
61
+ names.add(name);
62
+ }
63
+ /** Remove the scheduler callback associated with a dropped PostgreSQL TTL index. */
64
+ function unregisterPostgresTTLPurgeJob(driver, table, column) {
65
+ const name = postgresTTLPurgeJobName(driver, table, column);
66
+ scheduler.removeJob(name);
67
+ driverJobs.get(driver)?.delete(name);
68
+ }
69
+ /** Remove a driver's callbacks before its pool is closed. */
70
+ function unregisterAllPostgresTTLPurgeJobs(driver) {
71
+ for (const name of driverJobs.get(driver) ?? []) scheduler.removeJob(name);
72
+ driverJobs.delete(driver);
73
+ }
74
+ /** Restore registered retention rules from index comments on every connection. */
75
+ async function loadPostgresTTLPurgeJobs(driver) {
76
+ const result = await driver.query(`SELECT d.description FROM pg_catalog.pg_description d
77
+ JOIN pg_catalog.pg_class c ON c.oid = d.objoid
78
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
79
+ WHERE c.relkind = 'i' AND n.nspname = current_schema()
80
+ AND d.description LIKE 'warlock_ttl:%'`);
81
+ for (const row of result.rows ?? []) {
82
+ if (typeof row.description !== "string" || !row.description.startsWith(TTL_COMMENT_PREFIX)) continue;
83
+ let rule;
84
+ try {
85
+ rule = JSON.parse(row.description.slice(12));
86
+ } catch {
87
+ continue;
88
+ }
89
+ if (!rule || typeof rule !== "object") continue;
90
+ const { table, column, expireAfterSeconds } = rule;
91
+ if (typeof table !== "string" || table.length === 0 || typeof column !== "string" || column.length === 0 || !Number.isSafeInteger(expireAfterSeconds) || expireAfterSeconds < 0) continue;
92
+ registerPostgresTTLPurgeJob(driver, table, column, expireAfterSeconds);
93
+ }
94
+ }
95
+
96
+ //#endregion
97
+ export { loadPostgresTTLPurgeJobs, postgresTTLPurgeComment, registerPostgresTTLPurgeJob, unregisterAllPostgresTTLPurgeJobs, unregisterPostgresTTLPurgeJob };
98
+ //# sourceMappingURL=postgres-ttl-purge.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-ttl-purge.mjs","names":[],"sources":["../../../../../../../../cascade/src/drivers/postgres/postgres-ttl-purge.ts"],"sourcesContent":["import { job, scheduler } from \"@warlock.js/scheduler\";\nimport type { PostgresDriver } from \"./postgres-driver\";\n\nconst TTL_PURGE_INTERVAL_MINUTES = 1;\nconst TTL_COMMENT_PREFIX = \"warlock_ttl:\";\n\nlet nextDriverId = 0;\nconst driverIds = new WeakMap<object, number>();\nconst driverJobs = new WeakMap<object, Set<string>>();\n\ntype PostgresTTLPurgeDriver = Pick<PostgresDriver, \"dialect\"> & {\n query(sql: string, params?: unknown[]): Promise<unknown>;\n};\n\nfunction getDriverId(driver: object): number {\n let id = driverIds.get(driver);\n\n if (id === undefined) {\n id = nextDriverId++;\n driverIds.set(driver, id);\n }\n\n return id;\n}\n\n/** Persist the retention rule with the index so a restarted process can restore its job. */\nexport function postgresTTLPurgeComment(table: string, column: string, expireAfterSeconds: number): string {\n return TTL_COMMENT_PREFIX + JSON.stringify({ table, column, expireAfterSeconds });\n}\n\n/**\n * Return the scheduler job name for one PostgreSQL TTL index.\n *\n * The driver id prevents two data sources with identically named tables from\n * replacing each other's purge jobs on the shared scheduler singleton.\n */\nexport function postgresTTLPurgeJobName(driver: object, table: string, column: string): string {\n return `cascade:postgres:${getDriverId(driver)}:ttl:${table}:${column}`;\n}\n\n/**\n * Delete rows which have outlived a PostgreSQL TTL index's retention period.\n *\n * Identifiers are produced by the dialect, while the retention interval stays\n * parameterized so a migration value can never become executable SQL.\n */\nexport async function purgeExpiredPostgresRows(\n driver: PostgresTTLPurgeDriver,\n table: string,\n column: string,\n expireAfterSeconds: number,\n): Promise<void> {\n const quotedTable = driver.dialect.quoteIdentifier(table);\n const quotedColumn = driver.dialect.quoteIdentifier(column);\n\n await driver.query(\n `DELETE FROM ${quotedTable} WHERE ${quotedColumn} < NOW() - ($1 * INTERVAL '1 second')`,\n [expireAfterSeconds],\n );\n}\n\n/**\n * Register the in-process scheduler purge job for a PostgreSQL TTL index.\n *\n * The application owns the scheduler lifecycle: callers must start the shared\n * scheduler during bootstrap. Re-registering replaces a previous job for the\n * same driver/index so repeated migration setup cannot leak callbacks.\n */\nexport function registerPostgresTTLPurgeJob(\n driver: PostgresTTLPurgeDriver,\n table: string,\n column: string,\n expireAfterSeconds: number,\n): void {\n const name = postgresTTLPurgeJobName(driver, table, column);\n\n scheduler.removeJob(name);\n scheduler.addJob(\n job(name, () => purgeExpiredPostgresRows(driver, table, column, expireAfterSeconds)).everyMinutes(\n TTL_PURGE_INTERVAL_MINUTES,\n ),\n );\n let names = driverJobs.get(driver);\n if (!names) {\n names = new Set();\n driverJobs.set(driver, names);\n }\n names.add(name);\n}\n\n/** Remove the scheduler callback associated with a dropped PostgreSQL TTL index. */\nexport function unregisterPostgresTTLPurgeJob(driver: object, table: string, column: string): void {\n const name = postgresTTLPurgeJobName(driver, table, column);\n scheduler.removeJob(name);\n driverJobs.get(driver)?.delete(name);\n}\n\n/** Remove a driver's callbacks before its pool is closed. */\nexport function unregisterAllPostgresTTLPurgeJobs(driver: object): void {\n for (const name of driverJobs.get(driver) ?? []) scheduler.removeJob(name);\n driverJobs.delete(driver);\n}\n\n/** Restore registered retention rules from index comments on every connection. */\nexport async function loadPostgresTTLPurgeJobs(driver: PostgresTTLPurgeDriver): Promise<void> {\n const result = await driver.query(\n `SELECT d.description FROM pg_catalog.pg_description d\n JOIN pg_catalog.pg_class c ON c.oid = d.objoid\n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relkind = 'i' AND n.nspname = current_schema()\n AND d.description LIKE 'warlock_ttl:%'`,\n ) as { rows?: Array<{ description?: unknown }> };\n\n for (const row of result.rows ?? []) {\n if (typeof row.description !== \"string\" || !row.description.startsWith(TTL_COMMENT_PREFIX)) continue;\n let rule: unknown;\n try {\n rule = JSON.parse(row.description.slice(TTL_COMMENT_PREFIX.length));\n } catch {\n continue;\n }\n if (!rule || typeof rule !== \"object\") continue;\n const { table, column, expireAfterSeconds } = rule as Record<string, unknown>;\n if (\n typeof table !== \"string\" || table.length === 0 ||\n typeof column !== \"string\" || column.length === 0 ||\n !Number.isSafeInteger(expireAfterSeconds) || (expireAfterSeconds as number) < 0\n ) continue;\n registerPostgresTTLPurgeJob(driver, table, column, expireAfterSeconds as number);\n }\n}\n"],"mappings":";;;AAGA,MAAM,6BAA6B;AACnC,MAAM,qBAAqB;AAE3B,IAAI,eAAe;AACnB,MAAM,4BAAY,IAAI,QAAwB;AAC9C,MAAM,6BAAa,IAAI,QAA6B;AAMpD,SAAS,YAAY,QAAwB;CAC3C,IAAI,KAAK,UAAU,IAAI,MAAM;CAE7B,IAAI,OAAO,QAAW;EACpB,KAAK;EACL,UAAU,IAAI,QAAQ,EAAE;CAC1B;CAEA,OAAO;AACT;;AAGA,SAAgB,wBAAwB,OAAe,QAAgB,oBAAoC;CACzG,OAAO,qBAAqB,KAAK,UAAU;EAAE;EAAO;EAAQ;CAAmB,CAAC;AAClF;;;;;;;AAQA,SAAgB,wBAAwB,QAAgB,OAAe,QAAwB;CAC7F,OAAO,oBAAoB,YAAY,MAAM,EAAE,OAAO,MAAM,GAAG;AACjE;;;;;;;AAQA,eAAsB,yBACpB,QACA,OACA,QACA,oBACe;CACf,MAAM,cAAc,OAAO,QAAQ,gBAAgB,KAAK;CACxD,MAAM,eAAe,OAAO,QAAQ,gBAAgB,MAAM;CAE1D,MAAM,OAAO,MACX,eAAe,YAAY,SAAS,aAAa,wCACjD,CAAC,kBAAkB,CACrB;AACF;;;;;;;;AASA,SAAgB,4BACd,QACA,OACA,QACA,oBACM;CACN,MAAM,OAAO,wBAAwB,QAAQ,OAAO,MAAM;CAE1D,UAAU,UAAU,IAAI;CACxB,UAAU,OACR,IAAI,YAAY,yBAAyB,QAAQ,OAAO,QAAQ,kBAAkB,CAAC,CAAC,CAAC,aACnF,0BACF,CACF;CACA,IAAI,QAAQ,WAAW,IAAI,MAAM;CACjC,IAAI,CAAC,OAAO;EACV,wBAAQ,IAAI,IAAI;EAChB,WAAW,IAAI,QAAQ,KAAK;CAC9B;CACA,MAAM,IAAI,IAAI;AAChB;;AAGA,SAAgB,8BAA8B,QAAgB,OAAe,QAAsB;CACjG,MAAM,OAAO,wBAAwB,QAAQ,OAAO,MAAM;CAC1D,UAAU,UAAU,IAAI;CACxB,WAAW,IAAI,MAAM,CAAC,EAAE,OAAO,IAAI;AACrC;;AAGA,SAAgB,kCAAkC,QAAsB;CACtE,KAAK,MAAM,QAAQ,WAAW,IAAI,MAAM,KAAK,CAAC,GAAG,UAAU,UAAU,IAAI;CACzE,WAAW,OAAO,MAAM;AAC1B;;AAGA,eAAsB,yBAAyB,QAA+C;CAC5F,MAAM,SAAS,MAAM,OAAO,MAC1B;;;;+CAKF;CAEA,KAAK,MAAM,OAAO,OAAO,QAAQ,CAAC,GAAG;EACnC,IAAI,OAAO,IAAI,gBAAgB,YAAY,CAAC,IAAI,YAAY,WAAW,kBAAkB,GAAG;EAC5F,IAAI;EACJ,IAAI;GACF,OAAO,KAAK,MAAM,IAAI,YAAY,MAAM,EAAyB,CAAC;EACpE,QAAQ;GACN;EACF;EACA,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;EACvC,MAAM,EAAE,OAAO,QAAQ,uBAAuB;EAC9C,IACE,OAAO,UAAU,YAAY,MAAM,WAAW,KAC9C,OAAO,WAAW,YAAY,OAAO,WAAW,KAChD,CAAC,OAAO,cAAc,kBAAkB,KAAM,qBAAgC,GAC9E;EACF,4BAA4B,QAAQ,OAAO,QAAQ,kBAA4B;CACjF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"database-remover.d.mts","names":[],"sources":["../../../../../../../cascade/src/remover/database-remover.ts"],"mappings":";;;;;;AAoCA;;;;;;;;;;;;;;;;;;cAAa,eAAA,YAA2B,eAAA;EA+BnB;EAAA,iBA7BF,KAAA;EA6Ca;EAAA,iBA1Cb,IAAA;EA0CmC;EAAA,iBAvCnC,UAAA;EAoNT;EAAA,iBAjNS,MAAA;EA8PH;EAAA,iBA3PG,KAAA;EA2PQ;EAAA,iBAxPR,UAAA;;;;;;;;;;;;;cAcE,KAAA,EAAO,KAAA;;;;;;;;EAgBb,OAAA,CAAQ,OAAA,GAAS,cAAA,GAAsB,OAAA,CAAQ,aAAA;;;;;;;;;;;;;;;;;;;;;UA6KpD,kBAAA;;;;;;;;;;;;UAyBA,iBAAA;;;;;;;;;UAoBM,WAAA;AAAA"}
1
+ {"version":3,"file":"database-remover.d.mts","names":[],"sources":["../../../../../../../cascade/src/remover/database-remover.ts"],"mappings":";;;;;;AAoCA;;;;;;;;;;;;;;;;;;cAAa,eAAA,YAA2B,eAAA;EA+BnB;EAAA,iBA7BF,KAAA;EA6Ca;EAAA,iBA1Cb,IAAA;EA0CmC;EAAA,iBAvCnC,UAAA;EAmNT;EAAA,iBAhNS,MAAA;EA6PT;EAAA,iBA1PS,KAAA;EA0PE;EAAA,iBAvPF,UAAA;;;;;;;;;;;;;cAcE,KAAA,EAAO,KAAA;;;;;;;;EAgBb,OAAA,CAAQ,OAAA,GAAS,cAAA,GAAsB,OAAA,CAAQ,aAAA;;;;;;;;;;;;;;;;;;;;;UA4KpD,kBAAA;;;;;;;;;;;;UAyBA,iBAAA;;;;;;;;;UAoBA,WAAA;AAAA"}
@@ -1,6 +1,6 @@
1
1
  import { afterCommit } from "../transactions/after-commit.mjs";
2
2
  import { triggerModelEvent } from "../sync/model-events.mjs";
3
- import { log } from "@warlock.js/logger";
3
+ import { enqueueSyncFanout } from "../sync/sync-fanout.mjs";
4
4
 
5
5
  //#region ../cascade/src/remover/database-remover.ts
6
6
  /**
@@ -176,12 +176,12 @@ var DatabaseRemover = class {
176
176
  *
177
177
  * @private
178
178
  */
179
- async triggerSync() {
180
- try {
181
- await triggerModelEvent(this.ctor, "deleted", this.model);
182
- } catch (error) {
183
- log.error("database", "sync.failed", `[cascade] sync failed for ${this.ctor.name}: ${error}`);
184
- }
179
+ triggerSync() {
180
+ enqueueSyncFanout({
181
+ sourceModel: this.ctor.name,
182
+ operation: "delete",
183
+ execute: () => triggerModelEvent(this.ctor, "deleted", this.model)
184
+ });
185
185
  }
186
186
  };
187
187
 
@@ -1 +1 @@
1
- {"version":3,"file":"database-remover.mjs","names":[],"sources":["../../../../../../../cascade/src/remover/database-remover.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport type {\n DriverContract,\n UpdateOperations,\n} from \"../contracts/database-driver.contract\";\nimport type {\n RemoverContract,\n RemoverOptions,\n RemoverResult,\n} from \"../contracts/database-remover.contract\";\nimport type { OnDeletedEventContext } from \"../events/model-events\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { triggerModelEvent } from \"../sync/model-events\";\nimport { afterCommit } from \"../transactions/after-commit\";\nimport type { DataSource } from \"./../data-source/data-source\";\n\n/**\n * Database remover service that orchestrates model deletion.\n *\n * Handles the complete deletion pipeline:\n * 1. Strategy resolution (options → model static → data source default)\n * 2. Validation (check if model is new, has primary key)\n * 3. Event emission (deleting, deleted)\n * 4. Driver execution (based on strategy: trash, permanent, or soft)\n * 5. Post-deletion cleanup (mark as new, reset state)\n *\n * @example\n * ```typescript\n * const user = await User.find(1);\n * const remover = new DatabaseRemover(user);\n * const result = await remover.destroy();\n *\n * console.log(result.success); // true\n * console.log(result.strategy); // \"trash\" | \"permanent\" | \"soft\"\n * ```\n */\nexport class DatabaseRemover implements RemoverContract {\n /** The model instance being deleted */\n private readonly model: Model;\n\n /** Model constructor reference */\n private readonly ctor: ChildModel<Model>;\n\n /** Data source containing driver */\n private readonly dataSource: DataSource;\n\n /** Database driver for executing queries */\n private readonly driver: DriverContract;\n\n /** Table/collection name */\n private readonly table: string;\n\n /** Primary key field name */\n private readonly primaryKey: string;\n\n /**\n * Create a new remover instance for a model.\n *\n * @param model - The model instance to delete\n *\n * @example\n * ```typescript\n * const user = await User.find(1);\n * const remover = new DatabaseRemover(user);\n * await remover.destroy();\n * ```\n */\n public constructor(model: Model) {\n this.model = model;\n this.ctor = model.constructor as ChildModel<Model>;\n this.dataSource = this.ctor.getDataSource();\n this.driver = this.dataSource.driver;\n this.table = this.ctor.table;\n this.primaryKey = this.ctor.primaryKey;\n }\n\n /**\n * Destroy (delete) the model instance from the database.\n *\n * @param options - Remover options\n * @returns Result containing success status, strategy used, and metadata\n * @throws {Error} If model is new (not saved) or if deletion fails\n */\n public async destroy(options: RemoverOptions = {}): Promise<RemoverResult> {\n // 1. Resolve strategy (options → model static → data source default → permanent)\n const strategy =\n options.strategy ??\n this.ctor.deleteStrategy ??\n this.dataSource.defaultDeleteStrategy ??\n \"permanent\";\n\n // 2. Validate model is not new and has primary key\n if (this.model.isNew) {\n throw new Error(\n `Cannot destroy ${this.ctor.name} instance that hasn't been saved to the database.`,\n );\n }\n\n // The value captured when the model became persisted, never the current\n // (mass-assignable) one — a delete must not be retargetable by a payload\n // carrying `{ id: \"<victim-id>\" }`. Same rule as the writer's update filter.\n const primaryKeyValue = this.model.trustedPrimaryKey;\n if (!primaryKeyValue) {\n throw new Error(\n `Cannot destroy ${this.ctor.name} instance: primary key (${this.primaryKey}) is missing.`,\n );\n }\n\n // 3. Emit deleting event (unless skipEvents)\n if (!options.skipEvents) {\n await this.model.emitEvent(\"deleting\", {\n strategy,\n primaryKeyValue,\n primaryKey: this.primaryKey,\n });\n }\n\n // 4. Execute deletion based on strategy\n let deletedCount = 0;\n let trashRecord: Record<string, unknown> | undefined;\n\n const filter = { [this.primaryKey]: primaryKeyValue };\n\n const context: Partial<OnDeletedEventContext> = {\n strategy,\n primaryKeyValue,\n primaryKey: this.primaryKey,\n };\n\n switch (strategy) {\n case \"trash\": {\n // Move to trash table, then delete\n const trashTable = this.resolveTrashTable();\n const documentData = { ...this.model.data };\n\n // Prepare trash record with metadata and handle ID conflicts\n const trashData = this.prepareTrashRecord(documentData);\n\n // Insert into trash table\n const insertResult = await this.driver.insert(trashTable, trashData);\n trashRecord = insertResult.document as Record<string, unknown>;\n\n context.trashRecord = trashRecord;\n\n // Delete original\n const result = await this.driver.delete(this.table, filter);\n deletedCount = result > 0 ? 1 : 0;\n break;\n }\n\n case \"permanent\": {\n // Direct deletion\n const result = await this.driver.delete(this.table, filter);\n deletedCount = result > 0 ? 1 : 0;\n break;\n }\n\n case \"soft\": {\n // Set deletedAt timestamp (using resolved column name)\n const deletedAtColumn = this.ctor.deletedAtColumn;\n\n // Only proceed if deletedAtColumn is configured (not false or undefined)\n if (deletedAtColumn === false || deletedAtColumn === undefined) {\n throw new Error(\n `Cannot perform soft delete on ${this.ctor.name}: deletedAtColumn is not configured. ` +\n `Set a column name or use a different delete strategy.`,\n );\n }\n\n const deletedAt = new Date();\n const updateOperations: UpdateOperations = {\n $set: { [deletedAtColumn]: deletedAt },\n };\n // Guard: an already soft-deleted row must keep its original timestamp\n const softFilter = { ...filter, [deletedAtColumn]: null };\n const updateResult = await this.driver.update(\n this.table,\n softFilter,\n updateOperations,\n );\n deletedCount = updateResult.modifiedCount > 0 ? 1 : 0;\n\n // The row stays (unlike trash/permanent), so reflect the persisted\n // timestamp on the in-memory model — otherwise the instance is stale\n // and `model.get(deletedAtColumn)` stays undefined after destroy().\n if (deletedCount > 0) {\n this.model.set(deletedAtColumn, deletedAt);\n\n // The timestamp is already persisted, so it must not stay dirty.\n // Re-baseline only when it is the sole pending change, so unsaved\n // edits to other columns keep their dirty state.\n const dirty = this.model.dirtyTracker.getDirtyColumns();\n if (dirty.length === 1 && dirty[0] === deletedAtColumn) {\n this.model.dirtyTracker.reset(\n this.model.data as Record<string, unknown>,\n );\n }\n }\n break;\n }\n }\n\n if (deletedCount === 0) {\n throw new Error(\n `Failed to destroy ${this.ctor.name} instance: record not found.`,\n );\n }\n\n context.deletedCount = deletedCount;\n\n // 5. Post-deletion cleanup\n // Only mark as new for permanent and trash (soft delete keeps the record)\n if (strategy !== \"soft\") {\n this.model.isNew = true;\n }\n\n // 6. Emit deleted event (unless skipEvents)\n if (!options.skipEvents) {\n await this.model.emitEvent(\"deleted\", context);\n }\n\n // 7. Trigger sync operations after COMMIT; failures are logged\n // A soft delete keeps the row, so embedded copies must stay: only permanent/trash fire it.\n // TODO(5.22, K2:B10): move this fan-out to a retry/queue-backed path.\n if (!options.skipSync && strategy !== \"soft\") {\n afterCommit(() => this.triggerSync());\n }\n\n return {\n success: true,\n deletedCount,\n strategy,\n trashRecord,\n };\n }\n\n /**\n * Prepare the trash record by preserving all original fields and adding deletion metadata.\n *\n * Keeps all original fields intact for easy restoration and adds:\n * - `deletedAt`: Timestamp when the record was deleted\n * - `originalTable`: The table/collection the record came from (for filtering in restoreAll)\n *\n * **ID Handling:**\n * - MongoDB with `_id`: Keeps `_id` as-is (unique across database)\n * - MongoDB with auto-increment `id`: Keeps `id` as a regular field (not primary key)\n * - SQL: Keeps original `id` as a regular field (trash table uses its own auto-increment primary key)\n *\n * The trash table should use its own primary key structure:\n * - MongoDB: Uses `_id` (ObjectId) as primary key, original `id` is just a field\n * - SQL: Uses auto-increment `trashId` as primary key, original `id` is just a field\n *\n * @param documentData - The original document data\n * @returns Prepared trash record data with all original fields + deletedAt + originalTable\n * @private\n */\n private prepareTrashRecord(\n documentData: Record<string, unknown>,\n ): Record<string, unknown> {\n // Preserve all original fields and add deletion metadata\n const deletedAtColumn = this.ctor.deletedAtColumn;\n\n return {\n ...documentData,\n [typeof deletedAtColumn === \"string\" ? deletedAtColumn : \"deletedAt\"]:\n new Date(),\n originalTable: this.table,\n };\n }\n\n /**\n * Resolve the trash table/collection name.\n *\n * Priority:\n * 1. Model.trashTable (if set)\n * 2. Data source defaultTrashTable (e.g., \"RecycleBin\" for MongoDB)\n * 3. Default pattern: `{table}Trash`\n *\n * @returns The trash table/collection name\n * @private\n */\n private resolveTrashTable(): string {\n if (this.ctor.trashTable) {\n return this.ctor.trashTable;\n }\n\n if (this.dataSource.defaultTrashTable) {\n return this.dataSource.defaultTrashTable;\n }\n\n return `${this.table}Trash`;\n }\n\n /**\n * Trigger sync operations after successful deletion.\n *\n * Emits a model.deleted event that ModelSyncOperation listens to.\n * The sync is handled by registered sync operations, not directly here.\n *\n * @private\n */\n private async triggerSync(): Promise<void> {\n // Emit model.deleted event - ModelSyncOperation listens to these\n try {\n await triggerModelEvent(this.ctor, \"deleted\", this.model);\n } catch (error) {\n log.error(\"database\", \"sync.failed\", `[cascade] sync failed for ${this.ctor.name}: ${error}`);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"database-remover.mjs","names":[],"sources":["../../../../../../../cascade/src/remover/database-remover.ts"],"sourcesContent":["import type {\n DriverContract,\n UpdateOperations,\n} from \"../contracts/database-driver.contract\";\nimport type {\n RemoverContract,\n RemoverOptions,\n RemoverResult,\n} from \"../contracts/database-remover.contract\";\nimport type { OnDeletedEventContext } from \"../events/model-events\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { enqueueSyncFanout } from \"../sync/sync-fanout\";\nimport { triggerModelEvent } from \"../sync/model-events\";\nimport { afterCommit } from \"../transactions/after-commit\";\nimport type { DataSource } from \"./../data-source/data-source\";\n\n/**\n * Database remover service that orchestrates model deletion.\n *\n * Handles the complete deletion pipeline:\n * 1. Strategy resolution (options → model static → data source default)\n * 2. Validation (check if model is new, has primary key)\n * 3. Event emission (deleting, deleted)\n * 4. Driver execution (based on strategy: trash, permanent, or soft)\n * 5. Post-deletion cleanup (mark as new, reset state)\n *\n * @example\n * ```typescript\n * const user = await User.find(1);\n * const remover = new DatabaseRemover(user);\n * const result = await remover.destroy();\n *\n * console.log(result.success); // true\n * console.log(result.strategy); // \"trash\" | \"permanent\" | \"soft\"\n * ```\n */\nexport class DatabaseRemover implements RemoverContract {\n /** The model instance being deleted */\n private readonly model: Model;\n\n /** Model constructor reference */\n private readonly ctor: ChildModel<Model>;\n\n /** Data source containing driver */\n private readonly dataSource: DataSource;\n\n /** Database driver for executing queries */\n private readonly driver: DriverContract;\n\n /** Table/collection name */\n private readonly table: string;\n\n /** Primary key field name */\n private readonly primaryKey: string;\n\n /**\n * Create a new remover instance for a model.\n *\n * @param model - The model instance to delete\n *\n * @example\n * ```typescript\n * const user = await User.find(1);\n * const remover = new DatabaseRemover(user);\n * await remover.destroy();\n * ```\n */\n public constructor(model: Model) {\n this.model = model;\n this.ctor = model.constructor as ChildModel<Model>;\n this.dataSource = this.ctor.getDataSource();\n this.driver = this.dataSource.driver;\n this.table = this.ctor.table;\n this.primaryKey = this.ctor.primaryKey;\n }\n\n /**\n * Destroy (delete) the model instance from the database.\n *\n * @param options - Remover options\n * @returns Result containing success status, strategy used, and metadata\n * @throws {Error} If model is new (not saved) or if deletion fails\n */\n public async destroy(options: RemoverOptions = {}): Promise<RemoverResult> {\n // 1. Resolve strategy (options → model static → data source default → permanent)\n const strategy =\n options.strategy ??\n this.ctor.deleteStrategy ??\n this.dataSource.defaultDeleteStrategy ??\n \"permanent\";\n\n // 2. Validate model is not new and has primary key\n if (this.model.isNew) {\n throw new Error(\n `Cannot destroy ${this.ctor.name} instance that hasn't been saved to the database.`,\n );\n }\n\n // The value captured when the model became persisted, never the current\n // (mass-assignable) one — a delete must not be retargetable by a payload\n // carrying `{ id: \"<victim-id>\" }`. Same rule as the writer's update filter.\n const primaryKeyValue = this.model.trustedPrimaryKey;\n if (!primaryKeyValue) {\n throw new Error(\n `Cannot destroy ${this.ctor.name} instance: primary key (${this.primaryKey}) is missing.`,\n );\n }\n\n // 3. Emit deleting event (unless skipEvents)\n if (!options.skipEvents) {\n await this.model.emitEvent(\"deleting\", {\n strategy,\n primaryKeyValue,\n primaryKey: this.primaryKey,\n });\n }\n\n // 4. Execute deletion based on strategy\n let deletedCount = 0;\n let trashRecord: Record<string, unknown> | undefined;\n\n const filter = { [this.primaryKey]: primaryKeyValue };\n\n const context: Partial<OnDeletedEventContext> = {\n strategy,\n primaryKeyValue,\n primaryKey: this.primaryKey,\n };\n\n switch (strategy) {\n case \"trash\": {\n // Move to trash table, then delete\n const trashTable = this.resolveTrashTable();\n const documentData = { ...this.model.data };\n\n // Prepare trash record with metadata and handle ID conflicts\n const trashData = this.prepareTrashRecord(documentData);\n\n // Insert into trash table\n const insertResult = await this.driver.insert(trashTable, trashData);\n trashRecord = insertResult.document as Record<string, unknown>;\n\n context.trashRecord = trashRecord;\n\n // Delete original\n const result = await this.driver.delete(this.table, filter);\n deletedCount = result > 0 ? 1 : 0;\n break;\n }\n\n case \"permanent\": {\n // Direct deletion\n const result = await this.driver.delete(this.table, filter);\n deletedCount = result > 0 ? 1 : 0;\n break;\n }\n\n case \"soft\": {\n // Set deletedAt timestamp (using resolved column name)\n const deletedAtColumn = this.ctor.deletedAtColumn;\n\n // Only proceed if deletedAtColumn is configured (not false or undefined)\n if (deletedAtColumn === false || deletedAtColumn === undefined) {\n throw new Error(\n `Cannot perform soft delete on ${this.ctor.name}: deletedAtColumn is not configured. ` +\n `Set a column name or use a different delete strategy.`,\n );\n }\n\n const deletedAt = new Date();\n const updateOperations: UpdateOperations = {\n $set: { [deletedAtColumn]: deletedAt },\n };\n // Guard: an already soft-deleted row must keep its original timestamp\n const softFilter = { ...filter, [deletedAtColumn]: null };\n const updateResult = await this.driver.update(\n this.table,\n softFilter,\n updateOperations,\n );\n deletedCount = updateResult.modifiedCount > 0 ? 1 : 0;\n\n // The row stays (unlike trash/permanent), so reflect the persisted\n // timestamp on the in-memory model — otherwise the instance is stale\n // and `model.get(deletedAtColumn)` stays undefined after destroy().\n if (deletedCount > 0) {\n this.model.set(deletedAtColumn, deletedAt);\n\n // The timestamp is already persisted, so it must not stay dirty.\n // Re-baseline only when it is the sole pending change, so unsaved\n // edits to other columns keep their dirty state.\n const dirty = this.model.dirtyTracker.getDirtyColumns();\n if (dirty.length === 1 && dirty[0] === deletedAtColumn) {\n this.model.dirtyTracker.reset(\n this.model.data as Record<string, unknown>,\n );\n }\n }\n break;\n }\n }\n\n if (deletedCount === 0) {\n throw new Error(\n `Failed to destroy ${this.ctor.name} instance: record not found.`,\n );\n }\n\n context.deletedCount = deletedCount;\n\n // 5. Post-deletion cleanup\n // Only mark as new for permanent and trash (soft delete keeps the record)\n if (strategy !== \"soft\") {\n this.model.isNew = true;\n }\n\n // 6. Emit deleted event (unless skipEvents)\n if (!options.skipEvents) {\n await this.model.emitEvent(\"deleted\", context);\n }\n\n // 7. Queue sync operations after COMMIT. A soft delete keeps the row, so\n // embedded copies must stay: only permanent/trash fire a delete sync.\n if (!options.skipSync && strategy !== \"soft\") {\n afterCommit(() => this.triggerSync());\n }\n\n return {\n success: true,\n deletedCount,\n strategy,\n trashRecord,\n };\n }\n\n /**\n * Prepare the trash record by preserving all original fields and adding deletion metadata.\n *\n * Keeps all original fields intact for easy restoration and adds:\n * - `deletedAt`: Timestamp when the record was deleted\n * - `originalTable`: The table/collection the record came from (for filtering in restoreAll)\n *\n * **ID Handling:**\n * - MongoDB with `_id`: Keeps `_id` as-is (unique across database)\n * - MongoDB with auto-increment `id`: Keeps `id` as a regular field (not primary key)\n * - SQL: Keeps original `id` as a regular field (trash table uses its own auto-increment primary key)\n *\n * The trash table should use its own primary key structure:\n * - MongoDB: Uses `_id` (ObjectId) as primary key, original `id` is just a field\n * - SQL: Uses auto-increment `trashId` as primary key, original `id` is just a field\n *\n * @param documentData - The original document data\n * @returns Prepared trash record data with all original fields + deletedAt + originalTable\n * @private\n */\n private prepareTrashRecord(\n documentData: Record<string, unknown>,\n ): Record<string, unknown> {\n // Preserve all original fields and add deletion metadata\n const deletedAtColumn = this.ctor.deletedAtColumn;\n\n return {\n ...documentData,\n [typeof deletedAtColumn === \"string\" ? deletedAtColumn : \"deletedAt\"]:\n new Date(),\n originalTable: this.table,\n };\n }\n\n /**\n * Resolve the trash table/collection name.\n *\n * Priority:\n * 1. Model.trashTable (if set)\n * 2. Data source defaultTrashTable (e.g., \"RecycleBin\" for MongoDB)\n * 3. Default pattern: `{table}Trash`\n *\n * @returns The trash table/collection name\n * @private\n */\n private resolveTrashTable(): string {\n if (this.ctor.trashTable) {\n return this.ctor.trashTable;\n }\n\n if (this.dataSource.defaultTrashTable) {\n return this.dataSource.defaultTrashTable;\n }\n\n return `${this.table}Trash`;\n }\n\n /**\n * Trigger sync operations after successful deletion.\n *\n * Emits a model.deleted event that ModelSyncOperation listens to.\n * The sync is handled by registered sync operations, not directly here.\n *\n * @private\n */\n private triggerSync(): void {\n enqueueSyncFanout({\n sourceModel: this.ctor.name,\n operation: \"delete\",\n execute: () => triggerModelEvent(this.ctor, \"deleted\", this.model),\n });\n }\n}\n"],"mappings":"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"}
@@ -132,9 +132,8 @@ declare class DatabaseRestorer implements RestorerContract {
132
132
  */
133
133
  private resolveTrashTable;
134
134
  /**
135
- * Re-run sync for a restored record so embedded copies come back up to date.
136
- * Failures are swallowed: the restore itself already succeeded.
137
- * TODO(5.22, K2:B10): move this fan-out to a retry/queue-backed path.
135
+ * Queue sync for a restored record so embedded copies come back up to date.
136
+ * The restore has already succeeded, so queued failures are retried separately.
138
137
  *
139
138
  * @private
140
139
  */
@@ -1 +1 @@
1
- {"version":3,"file":"database-restorer.d.mts","names":[],"sources":["../../../../../../../cascade/src/restorer/database-restorer.ts"],"mappings":";;;;;;AA+BA;;;;;;;;;;;;;;;;;cAAa,gBAAA,YAA4B,gBAAA;EActB;EAAA,iBAZA,IAAA;EAyBqB;EAAA,iBAtBrB,UAAA;EAuCJ;EAAA,iBApCI,MAAA;EAsCN;EAAA,iBAnCM,KAAA;EAoCd;EAAA,iBAjCc,UAAA;EA4HJ;;;;;;;;;;;cA/GM,UAAA,SAAmB,KAAA;EAqexB;;AAAW;;;;;;;EApdZ,OAAA,CACX,EAAA,mBACA,OAAA,GAAS,eAAA,GACR,OAAA,CAAQ,cAAA;;;;;;;EA2FE,UAAA,CAAW,OAAA,GAAS,eAAA,GAAuB,OAAA,CAAQ,cAAA;;;;;;;;;;UAsKxD,eAAA;;;;;;;;;UAiBM,qBAAA;;;;;;;;UA2CA,yBAAA;;;;;;;;;;UA6CA,gBAAA;;;;;;;;UA4BA,aAAA;;;;;;;;;;;;;UAsBA,WAAA;;;;;;;;;;;;UAkCN,iBAAA;;;;;;;;UAmBM,WAAA;AAAA"}
1
+ {"version":3,"file":"database-restorer.d.mts","names":[],"sources":["../../../../../../../cascade/src/restorer/database-restorer.ts"],"mappings":";;;;;;AAgCA;;;;;;;;;;;;;;;;;cAAa,gBAAA,YAA4B,gBAAA;EActB;EAAA,iBAZA,IAAA;EAyBqB;EAAA,iBAtBrB,UAAA;EAuCJ;EAAA,iBApCI,MAAA;EAsCN;EAAA,iBAnCM,KAAA;EAoCd;EAAA,iBAjCc,UAAA;EA4HJ;;;;;;;;;;;cA/GM,UAAA,SAAmB,KAAA;EAoe9B;;AAAW;;;;;;;EAndN,OAAA,CACX,EAAA,mBACA,OAAA,GAAS,eAAA,GACR,OAAA,CAAQ,cAAA;;;;;;;EA2FE,UAAA,CAAW,OAAA,GAAS,eAAA,GAAuB,OAAA,CAAQ,cAAA;;;;;;;;;;UAsKxD,eAAA;;;;;;;;;UAiBM,qBAAA;;;;;;;;UA2CA,yBAAA;;;;;;;;;;UA6CA,gBAAA;;;;;;;;UA4BA,aAAA;;;;;;;;;;;;;UAsBA,WAAA;;;;;;;;;;;;UAkCN,iBAAA;;;;;;;UAkBA,WAAA;AAAA"}
@@ -1,4 +1,5 @@
1
1
  import { triggerModelEvent } from "../sync/model-events.mjs";
2
+ import { enqueueSyncFanout } from "../sync/sync-fanout.mjs";
2
3
  import { DatabaseWriter } from "../writer/database-writer.mjs";
3
4
 
4
5
  //#region ../cascade/src/restorer/database-restorer.ts
@@ -328,16 +329,17 @@ var DatabaseRestorer = class {
328
329
  return `${this.table}Trash`;
329
330
  }
330
331
  /**
331
- * Re-run sync for a restored record so embedded copies come back up to date.
332
- * Failures are swallowed: the restore itself already succeeded.
333
- * TODO(5.22, K2:B10): move this fan-out to a retry/queue-backed path.
332
+ * Queue sync for a restored record so embedded copies come back up to date.
333
+ * The restore has already succeeded, so queued failures are retried separately.
334
334
  *
335
335
  * @private
336
336
  */
337
- async triggerSync(model) {
338
- try {
339
- await triggerModelEvent(this.ctor, "updated", model, Object.keys(model.data));
340
- } catch {}
337
+ triggerSync(model) {
338
+ enqueueSyncFanout({
339
+ sourceModel: this.ctor.name,
340
+ operation: "restore",
341
+ execute: () => triggerModelEvent(this.ctor, "updated", model, Object.keys(model.data))
342
+ });
341
343
  }
342
344
  };
343
345