import { DatabaseClient } from '../database/database-client.interface'; import { LogLevel } from '../entity/db-context'; import { TableSchema, IndexMethod } from '../schema/table-builder'; import { ColumnConfig } from '../schema/column-builder'; import { CollationDefinition } from '../types/collation-builder'; import { SequenceConfig } from '../schema/sequence-builder'; /** * Database column information from pg */ interface DbColumnInfo { column_name: string; data_type: string; udt_name: string; character_maximum_length: number | null; numeric_precision: number | null; numeric_scale: number | null; is_nullable: 'YES' | 'NO'; column_default: string | null; collation_name: string | null; } /** * Migration operation types - describes schema changes to be applied */ export type MigrationOperation = { type: 'create_schema'; schemaName: string; } | { type: 'create_collation'; collation: CollationDefinition; } | { type: 'create_enum'; enumName: string; values: readonly string[]; } | { type: 'add_enum_value'; enumName: string; values: string[]; } | { type: 'create_sequence'; config: SequenceConfig; } | { type: 'create_table'; tableName: string; schema: TableSchema; } | { type: 'drop_table'; tableName: string; } | { type: 'add_column'; tableName: string; schema?: string; columnName: string; config: ColumnConfig; } | { type: 'drop_column'; tableName: string; schema?: string; columnName: string; } | { type: 'alter_column'; tableName: string; schema?: string; columnName: string; from: DbColumnInfo; to: ColumnConfig; } | { type: 'create_index'; tableName: string; schema?: string; indexName: string; columns: string[]; isUnique?: boolean; using?: IndexMethod; operatorClass?: string; concurrent?: boolean; expressions?: string[]; where?: string; nullsNotDistinct?: boolean; } | { type: 'recreate_index'; tableName: string; schema?: string; indexName: string; columns: string[]; isUnique?: boolean; using?: IndexMethod; operatorClass?: string; concurrent?: boolean; expressions?: string[]; where?: string; nullsNotDistinct?: boolean; reason?: string; previousDef?: string; } | { type: 'drop_index'; tableName: string; schema?: string; indexName: string; } | { type: 'create_statistics'; tableName: string; schema?: string; statisticsName: string; expressions: string[]; kinds?: Array<'ndistinct' | 'dependencies' | 'mcv'>; } | { type: 'create_foreign_key'; tableName: string; schema?: string; constraint: any; } | { type: 'drop_foreign_key'; tableName: string; schema?: string; constraintName: string; }; /** * Database schema manager - handles schema creation, deletion, and automatic migrations */ export declare class DbSchemaManager { private client; private schemaRegistry; private logQueries; private logger; private preMigrationHook?; private postMigrationHook?; private sequenceRegistry; private concurrentIndexes; private recreateChangedIndexes; private searchNormalizeRequired; /** Monotonic counter for unique temp object names during index confirmation. */ private indexCheckSeq; private rl; constructor(client: DatabaseClient, schemaRegistry: Map, options?: { logQueries?: boolean; logger?: (message: string, level?: LogLevel) => void; preMigrationHook?: (client: DatabaseClient) => Promise; postMigrationHook?: (client: DatabaseClient) => Promise; sequenceRegistry?: Map; /** * When true, the `search_normalize` support objects (the `unaccent` * extension and `public.search_normalize(text)` function) are created * even if no `ixNormalized` index is present. Set via * `model.useSearchNormalize()` for query-only usage. */ searchNormalizeRequired?: boolean; /** * When true, every index created by this schema manager uses * `CREATE INDEX CONCURRENTLY`, regardless of per-index `.concurrent()`. * Must not be used inside a transaction — PostgreSQL disallows it. */ concurrentIndexes?: boolean; /** * When true (the default), automatic migration compares each model index * against the matching index already in the database and, when their * definitions differ (operator class, expressions, method, uniqueness, * columns, partial predicate) while the name is unchanged, drops and * recreates it. Set to false to keep the legacy name-only behavior, where * a same-named index is never touched. * * The recreate is non-blocking (`DROP/CREATE INDEX CONCURRENTLY`) when the * index is marked `.concurrent()` or `concurrentIndexes: true` is set. */ recreateChangedIndexes?: boolean; }); /** * Get or create readline interface for interactive prompts */ private getReadlineInterface; /** * Close readline interface if it exists */ private closeReadlineInterface; /** * Get qualified table name with schema prefix if specified */ private getQualifiedTableName; /** * Create all schemas used by tables */ private createSchemas; /** * Create all ENUM types used in the schema */ private createEnumTypes; /** * Create all COLLATION types used in the schema */ private createCollations; /** * Create all sequences registered in the schema */ private createSequences; /** * Create a single table * @param tableName - The table name * @param tableSchema - The table schema * @param options - Options for table creation * @param options.skipForeignKeys - If true, foreign keys will not be added (useful for deferred FK creation) */ private createTable; /** * Add foreign key constraints to a table (used after all tables are created) */ private addForeignKeysToTable; /** * Sort tables in dependency order (topological sort) * Tables with no foreign key dependencies come first */ private sortTablesByDependency; /** * Execute the pre-migration hook, if one is configured. * * Runs BEFORE any schema analysis or changes. Exposed publicly so the * MigrationRunner can fire it ahead of file-based migrations on an * existing database (where `migrate()` is not invoked). Safe to call when * no hook is configured — it is a no-op in that case. */ runPreMigrationHook(): Promise; /** * SQL body for the `public.search_normalize(text)` function. Uses the 2-arg * form of `unaccent` (explicit dictionary) so the function is IMMUTABLE and * therefore usable in expression indexes. * * The dictionary is schema-qualified (`'public.unaccent'`) on purpose: * PostgreSQL evaluates functional-index expressions with a restricted * `search_path`, so an unqualified `'unaccent'::regdictionary` would fail with * "text search dictionary unaccent does not exist" when the index is built. */ private static readonly SEARCH_NORMALIZE_FUNCTION_SQL; /** * Whether any registered index uses an `ixNormalized` expression. */ private hasNormalizedIndex; /** * Whether any registered `ixNormalized` index is a trigram GIN index, which * additionally needs the `pg_trgm` extension. */ private needsPgTrgmForNormalized; /** * Create the `search_normalize` support objects (the `unaccent` extension and * the `public.search_normalize(text)` function — plus `pg_trgm` when a * trigram GIN normalized index is present) when the model needs them. * * Runs before tables/indexes so that `ixNormalized` expression indexes can be * built. Idempotent — safe to run on every migration. */ ensureSearchNormalizeSupport(): Promise; /** * Create all tables in the database */ ensureCreated(): Promise; /** * Create indexes for a table */ private createIndexes; /** * Create extended-statistics objects for a table */ private createStatistics; /** * Drop all tables */ ensureDeleted(): Promise; /** * Analyze differences between current DB and model schema */ analyze(): Promise; /** * Perform automatic migration - analyze and apply changes * * Tables are created first without foreign keys, then foreign keys are added * in a second pass. This ensures all referenced tables exist before FK constraints * are created. */ migrate(): Promise; /** * Execute a single migration operation */ private executeOperation; /** * Execute create schema */ private executeCreateSchema; /** * Execute create enum */ private executeCreateCollation; private executeCreateEnum; private executeAddEnumValues; /** * Execute create sequence */ private executeCreateSequence; /** * Execute drop table */ private executeDropTable; /** * Execute add column */ private executeAddColumn; /** * Execute drop column */ private executeDropColumn; /** * Execute alter column */ private executeAlterColumn; /** * Execute create index */ private static readonly VALID_INDEX_METHODS; private static readonly VALID_OPERATOR_CLASS; private executeCreateIndex; /** * Recreate an index whose definition changed while its name stayed the same: * drop the existing index, then create it from the model definition. * * The drop and create both use `CONCURRENTLY` when the index opted into it * (`.concurrent()` or the schema manager's `concurrentIndexes` option), making * the change non-blocking; otherwise it is a plain (briefly locking) recreate. * Either way this must run outside a transaction when concurrent. */ private executeRecreateIndex; /** * Execute drop index */ private executeDropIndex; /** * Create an extended-statistics object and immediately `ANALYZE` its table * so the statistics take effect without waiting for autovacuum. Idempotent * (`IF NOT EXISTS`), mirroring `executeCreateIndex`. */ private executeCreateStatistics; /** * Names of the extended-statistics objects defined on a table, resolved via * the table's namespace so the lookup is independent of search_path. */ private getExistingStatistics; /** * Execute create foreign key */ private executeCreateForeignKey; /** * Execute drop foreign key */ private executeDropForeignKey; /** * Get all existing tables in the database across all schemas used by the model */ private getExistingTables; /** * Get all columns for a table */ private getExistingColumns; /** * Get all indexes for a table */ private getExistingIndexes; /** * Authoritatively confirm which candidate indexes have genuinely changed by * asking PostgreSQL to canonicalize the model's intended definition the exact * same way it canonicalized the stored one — then comparing the two canonical * forms. * * Each candidate's index is rebuilt on a throwaway **empty** mirror table * (`CREATE TEMP TABLE ... (LIKE realTable)`), which makes the build instant * regardless of how much data the real table holds. PostgreSQL's * `pg_get_indexdef()` of that rebuild is the canonical form of the *model*; the * stored index already gives the canonical form of the *database*. If they are * equal, the index is unchanged — no matter how differently the model's SQL was * spelled (timestamp-literal expansion, re-parenthesization, `IN`→`ANY`, casts, * …). This is what makes a needless recreate impossible. * * Defensive by construction: if the mirror table or any rebuild can't be made * (e.g. a read-only session, missing `search_normalize` support, lacking TEMP * privilege), that candidate is treated as **unchanged** and left alone, so the * worst case is a missed change — never a churning rebuild. */ private confirmIndexChanges; /** * Get all foreign key constraints for a table */ private getExistingForeignKeys; /** * Check if a column needs to be altered */ private needsAlter; /** * Normalize default values for comparison */ private normalizeDefault; /** * Resolve the effective type from database column info. * Handles ARRAY and USER-DEFINED types by using udt_name. */ private resolveDbType; /** * Map PostgreSQL internal udt names to SQL type names */ private udtToSqlType; /** * Normalize PostgreSQL type names for comparison */ private normalizeType; /** * Build type definition for ALTER COLUMN TYPE */ private buildTypeDefinition; /** * Describe a database column */ private describeDbColumn; /** * Describe a model column */ private describeModelColumn; /** * Describe a migration operation */ private describeOperation; /** * Ask user for confirmation (CLI prompt) */ private confirm; /** * Format default value for SQL * Accepts RawSql to pass through raw SQL without formatting */ private formatDefaultValue; /** * Close the schema manager and any open resources */ close(): void; } export {}; //# sourceMappingURL=db-schema-manager.d.ts.map