import { Kysely, Migrator as Migrator$1, FileMigrationProvider, sql } from 'kysely';
import { PostgresJSDialect } from 'kysely-postgres-js';
import { mkdirSync, writeFileSync, existsSync, renameSync, rmSync } from 'node:fs';
import { readdir } from 'node:fs/promises';
import { join, extname, basename, dirname } from 'node:path';
import { parse } from 'pg-connection-string';
import postgres from 'postgres';
import { createHash } from 'node:crypto';
import { jsonArrayFrom } from 'kysely/helpers/postgres';

var DatabaseSslMode = /*#__PURE__*/ function(DatabaseSslMode) {
    DatabaseSslMode["Disable"] = "disable";
    DatabaseSslMode["Allow"] = "allow";
    DatabaseSslMode["Prefer"] = "prefer";
    DatabaseSslMode["Require"] = "require";
    DatabaseSslMode["VerifyFull"] = "verify-full";
    return DatabaseSslMode;
}({});
var ConstraintType = /*#__PURE__*/ function(ConstraintType) {
    ConstraintType["PRIMARY_KEY"] = "primary-key";
    ConstraintType["FOREIGN_KEY"] = "foreign-key";
    ConstraintType["UNIQUE"] = "unique";
    ConstraintType["CHECK"] = "check";
    return ConstraintType;
}({});
var ActionType = /*#__PURE__*/ function(ActionType) {
    ActionType["NO_ACTION"] = "NO ACTION";
    ActionType["RESTRICT"] = "RESTRICT";
    ActionType["CASCADE"] = "CASCADE";
    ActionType["SET_NULL"] = "SET NULL";
    ActionType["SET_DEFAULT"] = "SET DEFAULT";
    return ActionType;
}({});
var Reason = /*#__PURE__*/ function(Reason) {
    Reason["MissingInSource"] = "missing in source";
    Reason["MissingInTarget"] = "missing in target";
    Reason["Rename"] = "name has changed";
    return Reason;
}({});

const isPostgresSsl = (ssl)=>typeof ssl !== 'string' || ssl === 'require' || ssl === 'allow' || ssl === 'prefer' || ssl === 'verify-full';
const asPostgresConfig = (params)=>{
    if (params.connectionType === 'parts') {
        return {
            host: params.host,
            port: params.port,
            username: params.username,
            password: params.password,
            database: params.database,
            ssl: params.ssl === DatabaseSslMode.Disable ? false : params.ssl
        };
    }
    const { host, port, user, password, database, ssl } = parse(params.url);
    if (ssl !== undefined && !isPostgresSsl(ssl)) {
        throw new Error(`Invalid ssl option: ${ssl}`);
    }
    return {
        host: host ?? undefined,
        port: port ? Number(port) : undefined,
        username: user,
        password,
        database: database ?? undefined,
        ssl
    };
};
const createPostgres = (options = {})=>{
    const { connection = {
        connectionType: 'url',
        url: 'postgres://postgres:postgres@localhost:5432/postgres'
    }, maxConnections = 10, timeZone = 'UTC', convertToJsDate = true, convertBigIntToNumber = true, onNotice, onClose } = options;
    const config = asPostgresConfig(connection);
    const types = {};
    if (convertToJsDate) {
        types.date = {
            to: 1184,
            from: [
                1082,
                1114,
                1184
            ],
            serialize: (x)=>x instanceof Date ? x.toISOString() : x,
            parse: (x)=>new Date(x)
        };
    }
    if (convertBigIntToNumber) {
        types.bigint = {
            to: 20,
            from: [
                20,
                1700
            ],
            parse: (value)=>Number.parseInt(value),
            serialize: (value)=>value.toString()
        };
    }
    return postgres({
        host: config.host,
        port: config.port,
        username: config.username,
        password: config.password,
        database: config.database,
        ssl: config.ssl,
        max: maxConnections,
        connection: {
            TimeZone: timeZone
        },
        types,
        onnotice: onNotice,
        onclose: onClose
    });
};

const items = [];
const register = (item)=>void items.push(item);
const getRegisteredItems = ()=>items;
const resetRegisteredItems = ()=>{
    items.length = 0;
};

const asOptions = (options)=>{
    if (typeof options === 'string') {
        return {
            name: options
        };
    }
    return options;
};
const sha1 = (value)=>createHash('sha1').update(value).digest('hex');
const fromColumnValue = (columnValue)=>{
    if (columnValue === undefined) {
        return;
    }
    if (typeof columnValue === 'function') {
        return columnValue();
    }
    const value = columnValue;
    if (value === null) {
        return value;
    }
    if (typeof value === 'number') {
        return String(value);
    }
    if (typeof value === 'boolean') {
        return value ? 'true' : 'false';
    }
    if (value instanceof Date) {
        return `'${value.toISOString()}'`;
    }
    if (Array.isArray(value)) {
        return "'{}'";
    }
    return `'${String(value)}'`;
};
const setIsEqual = (source, target)=>source.size === target.size && [
        ...source
    ].every((x)=>target.has(x));
const haveEqualColumns = (sourceColumns, targetColumns)=>{
    return setIsEqual(new Set(sourceColumns), new Set(targetColumns));
};
const haveEqualOverrides = (source, target)=>{
    if (!source.override || !target.override) {
        return false;
    }
    const sourceValue = source.override.value;
    const targetValue = target.override.value;
    return sourceValue.name === targetValue.name && sourceValue.sql === targetValue.sql;
};
const compare = (sources, targets, options, comparer)=>{
    options = options || {};
    const sourceMap = Object.fromEntries(sources.map((table)=>[
            table.name,
            table
        ]));
    const targetMap = Object.fromEntries(targets.map((table)=>[
            table.name,
            table
        ]));
    const items = [];
    const keys = new Set([
        ...Object.keys(sourceMap),
        ...Object.keys(targetMap)
    ]);
    const missingKeys = new Set();
    const extraKeys = new Set();
    // common keys
    for (const key of keys){
        const source = sourceMap[key];
        const target = targetMap[key];
        if (isIgnored(source, target, options ?? true)) {
            continue;
        }
        if (isSynchronizeDisabled(source, target)) {
            continue;
        }
        if (source && !target) {
            missingKeys.add(key);
            continue;
        }
        if (!source && target) {
            extraKeys.add(key);
            continue;
        }
        if (haveEqualOverrides(source, target)) {
            continue;
        }
        items.push(...comparer.onCompare(source, target));
    }
    // renames
    if (comparer.getRenameKey && comparer.onRename) {
        const renameMap = {};
        for (const sourceKey of missingKeys){
            const source = sourceMap[sourceKey];
            const renameKey = comparer.getRenameKey(source);
            renameMap[renameKey] = sourceKey;
        }
        for (const targetKey of extraKeys){
            const target = targetMap[targetKey];
            const renameKey = comparer.getRenameKey(target);
            const sourceKey = renameMap[renameKey];
            if (!sourceKey) {
                continue;
            }
            const source = sourceMap[sourceKey];
            items.push(...comparer.onRename(source, target));
            missingKeys.delete(sourceKey);
            extraKeys.delete(targetKey);
        }
    }
    // missing
    for (const key of missingKeys){
        items.push(...comparer.onMissing(sourceMap[key]));
    }
    // extra
    for (const key of extraKeys){
        items.push(...comparer.onExtra(targetMap[key]));
    }
    return items;
};
const isIgnored = (source, target, options)=>{
    if (typeof options === 'boolean') {
        return !options;
    }
    return options.ignoreExtra && !source || options.ignoreMissing && !target;
};
const isSynchronizeDisabled = (source, target)=>{
    return source?.synchronize === false || target?.synchronize === false;
};
const isDefaultEqual = (source, target)=>{
    if (source.default === target.default) {
        return true;
    }
    if (source.default === undefined || target.default === undefined) {
        return false;
    }
    if (withTypeCast(source.default, getColumnType(source)) === target.default || withTypeCast(target.default, getColumnType(target)) === source.default) {
        return true;
    }
    return false;
};
const getColumnType = (column)=>{
    let type = column.enumName || column.type;
    if (column.isArray) {
        type += `[${column.length ?? ''}]`;
    } else if (column.length !== undefined) {
        type += `(${column.length})`;
    }
    return type;
};
const withTypeCast = (value, type)=>{
    if (!value.startsWith(`'`)) {
        value = `'${value}'`;
    }
    return `${value}::${type}`;
};
const getColumnModifiers = (column)=>{
    const modifiers = [];
    if (!column.nullable) {
        modifiers.push('NOT NULL');
    }
    if (column.default) {
        modifiers.push(`DEFAULT ${column.default}`);
    }
    if (column.identity) {
        modifiers.push(`GENERATED ALWAYS AS IDENTITY`);
    }
    return modifiers.length === 0 ? '' : ' ' + modifiers.join(' ');
};
const asColumnComment = (tableName, columnName, comment)=>{
    return `COMMENT ON COLUMN "${tableName}"."${columnName}" IS '${comment}';`;
};
const asColumnList = (columns)=>columns.map((column)=>`"${column}"`).join(', ');
const asJsonString = (value)=>{
    return `'${escape(JSON.stringify(value))}'::jsonb`;
};
const escape = (value)=>{
    return value.replaceAll("'", "''").replaceAll(/[\\]/g, '\\\\').replaceAll(/[\b]/g, String.raw`\b`).replaceAll(/[\f]/g, String.raw`\f`).replaceAll(/[\n]/g, String.raw`\n`).replaceAll(/[\r]/g, String.raw`\r`).replaceAll(/[\t]/g, String.raw`\t`);
};
const asRenameKey = (values)=>values.map((value)=>value ?? '').join('|');

const defaultUuidFactory = ({ db, major })=>{
    switch(db){
        case 'PostgreSQL':
            {
                switch(major){
                    case '18':
                        {
                            return (version)=>version === 4 ? 'uuidv4()' : 'uuidv7()';
                        }
                    default:
                        {
                            return ()=>'uuid_generate_v4()';
                        }
                }
            }
        default:
            {
                throw new Error(`Unsupported database: ${db}`);
            }
    }
};
class Migrator {
    #db;
    #migrator;
    #connectionParams;
    #migrationsFolder;
    #uuidFactory;
    constructor(options){
        const { connectionParams, allowUnorderedMigrations, migrationFolder, uuidFactory } = options;
        this.#connectionParams = connectionParams;
        this.#migrationsFolder = migrationFolder;
        this.#uuidFactory = uuidFactory ?? defaultUuidFactory;
        this.#db = this.#getDatabaseClient();
        this.#migrator = this.#getMigrator(allowUnorderedMigrations);
    }
    #getDatabaseClient = ()=>{
        return new Kysely({
            dialect: new PostgresJSDialect({
                postgres: createPostgres({
                    connection: this.#connectionParams
                })
            })
        });
    };
    #getMigrator = (allowUnorderedMigrations)=>{
        return new Migrator$1({
            db: this.#db,
            migrationLockTableName: 'kysely_migrations_lock',
            allowUnorderedMigrations,
            migrationTableName: 'kysely_migrations',
            provider: new FileMigrationProvider({
                fs: {
                    readdir
                },
                path: {
                    join
                },
                migrationFolder: join(this.#migrationsFolder)
            })
        });
    };
    getDatabase() {
        return this.#db;
    }
    async runMigrations() {
        const { error, results } = await this.#migrator.migrateToLatest();
        for (const result of results ?? []){
            if (result.status === 'Success') {
                console.log(`Migration "${result.migrationName}" succeeded`);
            }
            if (result.status === 'Error') {
                console.warn(`Migration "${result.migrationName}" failed`);
            }
        }
        if (error) {
            console.error(`Migrations failed: ${error}`);
            throw error;
        }
        console.log('Finished running migrations');
    }
    async #revertLastMigration() {
        console.debug('Reverting last migration');
        const { error, results } = await this.#migrator.migrateDown();
        for (const result of results ?? []){
            if (result.status === 'Success') {
                console.log(`Reverted migration "${result.migrationName}"`);
            }
            if (result.status === 'Error') {
                console.warn(`Failed to revert migration "${result.migrationName}"`);
            }
        }
        if (error) {
            console.error(`Failed to revert migrations: ${error}`);
            throw error;
        }
        const reverted = results?.find((result)=>result.direction === 'Down' && result.status === 'Success');
        if (!reverted) {
            console.debug('No migrations to revert');
            return undefined;
        }
        console.debug('Finished reverting migration');
        return reverted.migrationName;
    }
    revert = async (migrationsDistFolder)=>{
        const migrationName = await this.#revertLastMigration();
        if (!migrationName) {
            console.log('No migrations to revert');
            return;
        }
        this.#markMigrationAsReverted(migrationName, migrationsDistFolder);
    };
    generate = async ({ dist, targetPath, withComments })=>{
        const paths = [];
        for (const filename of (await readdir(dist, {
            recursive: true
        }))){
            if (extname(filename) !== '.js') {
                continue;
            }
            paths.push(join(dist, filename));
        }
        await Promise.all(paths.map((path)=>import(path)));
        const { up, down } = await this.#compare();
        if (up.items.length === 0) {
            console.log('No changes detected');
            return;
        }
        this.create(targetPath, up.asSql({
            comments: withComments
        }), down.asSql({
            comments: withComments
        }));
    };
    create = (path, up, down)=>{
        const timestamp = Date.now();
        const name = basename(path, extname(path));
        const filename = `${timestamp}-${name}.ts`;
        const folder = dirname(path);
        const fullPath = join(folder, filename);
        mkdirSync(folder, {
            recursive: true
        });
        writeFileSync(fullPath, this.#asMigration({
            up,
            down
        }));
        console.log(`Wrote ${fullPath}`);
    };
    #compare = async ()=>{
        const { version } = await this.#db.selectNoFrom(({ fn })=>fn('version').$castTo().as('version')).executeTakeFirstOrThrow();
        const { db, major } = /^(?<db>\w+) (?<major>\d+)\.(?<minor>\d+)(\.(?<patch>\d+))?.*$/.exec(version)?.groups ?? {};
        const source = schemaFromCode({
            overrides: true,
            namingStrategy: 'default',
            uuidFunction: this.#uuidFactory({
                db,
                major
            })
        });
        const target = await schemaFromDatabase({
            connection: this.#connectionParams
        });
        console.log(source.warnings.join('\n'));
        const up = schemaDiff(source, target, {
            tables: {
                ignoreExtra: true
            },
            functions: {
                ignoreExtra: false
            },
            parameters: {
                ignoreExtra: true
            }
        });
        const down = schemaDiff(target, source, {
            tables: {
                ignoreExtra: false,
                ignoreMissing: true
            },
            functions: {
                ignoreExtra: false
            },
            extensions: {
                ignoreMissing: true
            },
            parameters: {
                ignoreMissing: true
            }
        });
        return {
            up,
            down
        };
    };
    #asMigration = ({ up, down })=>{
        const upSql = up.map((sql)=>`  await sql\`${sql}\`.execute(db);`).join('\n');
        const downSql = down.map((sql)=>`  await sql\`${sql}\`.execute(db);`).join('\n');
        return `import { Kysely, sql } from 'kysely';

export async function up(db: Kysely<any>): Promise<void> {
${upSql}
}

export async function down(db: Kysely<any>): Promise<void> {
${downSql}
}
`;
    };
    #markMigrationAsReverted = (migrationName, migrationsDistFolder)=>{
        const sourcePath = join(this.#migrationsFolder, `${migrationName}.ts`);
        const revertedFolder = join(this.#migrationsFolder, 'reverted');
        const revertedPath = join(revertedFolder, `${migrationName}.ts`);
        if (existsSync(revertedPath)) {
            console.log(`Migration ${migrationName} is already marked as reverted`);
        } else if (existsSync(sourcePath)) {
            mkdirSync(revertedFolder, {
                recursive: true
            });
            renameSync(sourcePath, revertedPath);
            console.log(`Moved ${sourcePath} to ${revertedPath}`);
        } else {
            console.warn(`Source migration file not found for ${migrationName}`);
        }
        const distBase = join(migrationsDistFolder, migrationName);
        for (const extension of [
            '.js',
            '.js.map',
            '.d.ts'
        ]){
            const filePath = `${distBase}${extension}`;
            if (existsSync(filePath)) {
                rmSync(filePath, {
                    force: true
                });
                console.log(`Removed ${filePath}`);
            }
        }
    };
    destroy() {
        return this.#db.destroy();
    }
}

const asSnakeCase = (name)=>name.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
class DefaultNamingStrategy {
    getName(item) {
        switch(item.type){
            case 'database':
                {
                    return asSnakeCase(item.name);
                }
            case 'table':
                {
                    return asSnakeCase(item.name);
                }
            case 'column':
                {
                    return item.name;
                }
            case 'primaryKey':
                {
                    return `${item.tableName}_pkey`;
                }
            case 'foreignKey':
                {
                    return `${item.tableName}_${item.columnNames.join('_')}_fkey`;
                }
            case 'check':
                {
                    return `${item.tableName}_${sha1(item.expression).slice(0, 8)}_chk`;
                }
            case 'unique':
                {
                    return `${item.tableName}_${item.columnNames.join('_')}_uq`;
                }
            case 'index':
                {
                    if (item.columnNames) {
                        return `${item.tableName}_${item.columnNames.join('_')}_idx`;
                    }
                    return `${item.tableName}_${sha1(item.expression || item.where || '').slice(0, 8)}_idx`;
                }
            case 'trigger':
                {
                    return `${item.tableName}_${item.functionName}`;
                }
        }
    }
}

const fallback = new DefaultNamingStrategy();
const asKey = (prefix, tableName, values)=>(prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30);
class HashNamingStrategy {
    getName(item) {
        switch(item.type){
            case 'primaryKey':
                {
                    return asKey('PK_', item.tableName, item.columnNames);
                }
            case 'foreignKey':
                {
                    return asKey('FK_', item.tableName, item.columnNames);
                }
            case 'check':
                {
                    return asKey('CHK_', item.tableName, [
                        item.expression
                    ]);
                }
            case 'unique':
                {
                    return asKey('UQ_', item.tableName, item.columnNames);
                }
            case 'index':
                {
                    const items = [];
                    for (const columnName of item.columnNames ?? []){
                        items.push(columnName);
                    }
                    if (item.where) {
                        items.push(item.where);
                    }
                    return asKey('IDX_', item.tableName, items);
                }
            case 'trigger':
                {
                    return asKey('TR_', item.tableName, [
                        ...item.actions,
                        item.scope,
                        item.timing,
                        item.functionName
                    ]);
                }
            default:
                {
                    return fallback.getName(item);
                }
        }
    }
}

const compareEnums = ()=>({
        onMissing: (source)=>[
                {
                    type: 'EnumCreate',
                    enum: source,
                    reason: Reason.MissingInTarget
                }
            ],
        onExtra: (target)=>[
                {
                    type: 'EnumDrop',
                    enumName: target.name,
                    reason: Reason.MissingInSource
                }
            ],
        onCompare: (source, target)=>{
            if (source.values.toString() !== target.values.toString()) {
                // TODO add or remove values if the lists are different or the order has changed
                const reason = `enum values has changed (${source.values} vs ${target.values})`;
                return [
                    {
                        type: 'EnumDrop',
                        enumName: source.name,
                        reason
                    },
                    {
                        type: 'EnumCreate',
                        enum: source,
                        reason
                    }
                ];
            }
            return [];
        }
    });

const compareExtensions = ()=>({
        onMissing: (source)=>[
                {
                    type: 'ExtensionCreate',
                    extension: source,
                    reason: Reason.MissingInTarget
                }
            ],
        onExtra: (target)=>[
                {
                    type: 'ExtensionDrop',
                    extensionName: target.name,
                    reason: Reason.MissingInSource
                }
            ],
        onCompare: ()=>{
            // if the name matches they are the same
            return [];
        }
    });

const compareFunctions = ()=>({
        onMissing: (source)=>[
                {
                    type: 'FunctionCreate',
                    function: source,
                    reason: Reason.MissingInTarget
                }
            ],
        onExtra: (target)=>[
                {
                    type: 'FunctionDrop',
                    functionName: target.name,
                    reason: Reason.MissingInSource
                }
            ],
        onCompare: (source, target)=>{
            if (source.expression !== target.expression) {
                const reason = `function expression has changed (${source.expression} vs ${target.expression})`;
                return [
                    {
                        type: 'FunctionCreate',
                        function: source,
                        reason
                    }
                ];
            }
            return [];
        }
    });

const compareOverrides = ()=>({
        onMissing: (source)=>[
                {
                    type: 'OverrideCreate',
                    override: source,
                    reason: Reason.MissingInTarget
                }
            ],
        onExtra: (target)=>[
                {
                    type: 'OverrideDrop',
                    overrideName: target.name,
                    reason: Reason.MissingInSource
                }
            ],
        onCompare: (source, target)=>{
            if (source.value.name !== target.value.name || source.value.sql !== target.value.sql) {
                const sourceValue = JSON.stringify(source.value);
                const targetValue = JSON.stringify(target.value);
                return [
                    {
                        type: 'OverrideUpdate',
                        override: source,
                        reason: `value is different (${sourceValue} vs ${targetValue})`
                    }
                ];
            }
            return [];
        }
    });

const compareParameters = ()=>({
        onMissing: (source)=>[
                {
                    type: 'ParameterSet',
                    parameter: source,
                    reason: Reason.MissingInTarget
                }
            ],
        onExtra: (target)=>[
                {
                    type: 'ParameterReset',
                    databaseName: target.databaseName,
                    parameterName: target.name,
                    reason: Reason.MissingInSource
                }
            ],
        onCompare: ()=>{
            // TODO
            return [];
        }
    });

const compareColumns = ()=>({
        getRenameKey: (column)=>{
            return asRenameKey([
                column.tableName,
                column.type,
                column.nullable,
                column.default,
                column.storage,
                column.primary,
                column.isArray,
                column.length,
                column.identity,
                column.enumName,
                column.numericPrecision,
                column.numericScale
            ]);
        },
        onRename: (source, target)=>[
                {
                    type: 'ColumnRename',
                    tableName: source.tableName,
                    oldName: target.name,
                    newName: source.name,
                    reason: Reason.Rename
                }
            ],
        onMissing: (source)=>[
                {
                    type: 'ColumnAdd',
                    column: source,
                    reason: Reason.MissingInTarget
                }
            ],
        onExtra: (target)=>[
                {
                    type: 'ColumnDrop',
                    tableName: target.tableName,
                    columnName: target.name,
                    reason: Reason.MissingInSource
                }
            ],
        onCompare: (source, target)=>{
            const sourceType = getColumnType(source);
            const targetType = getColumnType(target);
            const isTypeChanged = sourceType !== targetType;
            if (isTypeChanged) {
                // TODO: convert between types via UPDATE when possible
                return dropAndRecreateColumn(source, target, `column type is different (${sourceType} vs ${targetType})`);
            }
            const items = [];
            if (source.nullable !== target.nullable) {
                items.push({
                    type: 'ColumnAlter',
                    tableName: source.tableName,
                    columnName: source.name,
                    changes: {
                        nullable: source.nullable
                    },
                    reason: `nullable is different (${source.nullable} vs ${target.nullable})`
                });
            }
            if (!isDefaultEqual(source, target)) {
                items.push({
                    type: 'ColumnAlter',
                    tableName: source.tableName,
                    columnName: source.name,
                    changes: {
                        default: String(source.default ?? 'NULL')
                    },
                    reason: `default is different (${source.default ?? 'null'} vs ${target.default})`
                });
            }
            if (source.comment !== target.comment) {
                items.push({
                    type: 'ColumnAlter',
                    tableName: source.tableName,
                    columnName: source.name,
                    changes: {
                        comment: String(source.comment)
                    },
                    reason: `comment is different (${source.comment} vs ${target.comment})`
                });
            }
            return items;
        }
    });
const dropAndRecreateColumn = (source, target, reason)=>{
    return [
        {
            type: 'ColumnDrop',
            tableName: target.tableName,
            columnName: target.name,
            reason
        },
        {
            type: 'ColumnAdd',
            column: source,
            reason
        }
    ];
};

const compareConstraints = ()=>({
        getRenameKey: (constraint)=>{
            switch(constraint.type){
                case ConstraintType.PRIMARY_KEY:
                case ConstraintType.UNIQUE:
                    {
                        return asRenameKey([
                            constraint.type,
                            constraint.tableName,
                            ...constraint.columnNames.toSorted()
                        ]);
                    }
                case ConstraintType.FOREIGN_KEY:
                    {
                        return asRenameKey([
                            constraint.type,
                            constraint.tableName,
                            ...constraint.columnNames.toSorted(),
                            constraint.referenceTableName,
                            ...constraint.referenceColumnNames.toSorted()
                        ]);
                    }
                case ConstraintType.CHECK:
                    {
                        const expression = constraint.expression.replaceAll('(', '').replaceAll(')', '');
                        return asRenameKey([
                            constraint.type,
                            constraint.tableName,
                            expression
                        ]);
                    }
            }
        },
        onRename: (source, target)=>[
                {
                    type: 'ConstraintRename',
                    tableName: target.tableName,
                    oldName: target.name,
                    newName: source.name,
                    reason: Reason.Rename
                }
            ],
        onMissing: (source)=>[
                {
                    type: 'ConstraintAdd',
                    constraint: source,
                    reason: Reason.MissingInTarget
                }
            ],
        onExtra: (target)=>[
                {
                    type: 'ConstraintDrop',
                    tableName: target.tableName,
                    constraintName: target.name,
                    reason: Reason.MissingInSource
                }
            ],
        onCompare: (source, target)=>{
            switch(source.type){
                case ConstraintType.PRIMARY_KEY:
                    {
                        return comparePrimaryKeyConstraint(source, target);
                    }
                case ConstraintType.FOREIGN_KEY:
                    {
                        return compareForeignKeyConstraint(source, target);
                    }
                case ConstraintType.UNIQUE:
                    {
                        return compareUniqueConstraint(source, target);
                    }
                case ConstraintType.CHECK:
                    {
                        return compareCheckConstraint(source, target);
                    }
                default:
                    {
                        return [];
                    }
            }
        }
    });
const comparePrimaryKeyConstraint = (source, target)=>{
    if (!haveEqualColumns(source.columnNames, target.columnNames)) {
        return dropAndRecreateConstraint(source, target, `Primary key columns are different: (${source.columnNames} vs ${target.columnNames})`);
    }
    return [];
};
const compareForeignKeyConstraint = (source, target)=>{
    let reason = '';
    const sourceDeleteAction = source.onDelete ?? 'NO ACTION';
    const targetDeleteAction = target.onDelete ?? 'NO ACTION';
    const sourceUpdateAction = source.onUpdate ?? 'NO ACTION';
    const targetUpdateAction = target.onUpdate ?? 'NO ACTION';
    if (!haveEqualColumns(source.columnNames, target.columnNames)) {
        reason = `columns are different (${source.columnNames} vs ${target.columnNames})`;
    } else if (!haveEqualColumns(source.referenceColumnNames, target.referenceColumnNames)) {
        reason = `reference columns are different (${source.referenceColumnNames} vs ${target.referenceColumnNames})`;
    } else if (source.referenceTableName !== target.referenceTableName) {
        reason = `reference table is different (${source.referenceTableName} vs ${target.referenceTableName})`;
    } else if (sourceDeleteAction !== targetDeleteAction) {
        reason = `ON DELETE action is different (${sourceDeleteAction} vs ${targetDeleteAction})`;
    } else if (sourceUpdateAction !== targetUpdateAction) {
        reason = `ON UPDATE action is different (${sourceUpdateAction} vs ${targetUpdateAction})`;
    }
    if (reason) {
        return dropAndRecreateConstraint(source, target, reason);
    }
    return [];
};
const compareUniqueConstraint = (source, target)=>{
    let reason = '';
    if (!haveEqualColumns(source.columnNames, target.columnNames)) {
        reason = `columns are different (${source.columnNames} vs ${target.columnNames})`;
    }
    if (reason) {
        return dropAndRecreateConstraint(source, target, reason);
    }
    return [];
};
const compareCheckConstraint = (source, target)=>{
    if (source.expression !== target.expression) ;
    return [];
};
const dropAndRecreateConstraint = (source, target, reason)=>{
    return [
        {
            type: 'ConstraintDrop',
            tableName: target.tableName,
            constraintName: target.name,
            reason
        },
        {
            type: 'ConstraintAdd',
            constraint: source,
            reason
        }
    ];
};

const compareIndexes = ()=>({
        getRenameKey: (index)=>{
            if (index.override) {
                return index.override.value.sql.replace(index.name, 'INDEX_NAME');
            }
            return asRenameKey([
                index.tableName,
                ...index.columnNames || [],
                index.unique
            ]);
        },
        onRename: (source, target)=>[
                {
                    type: 'IndexRename',
                    tableName: source.tableName,
                    oldName: target.name,
                    newName: source.name,
                    reason: Reason.Rename
                }
            ],
        onMissing: (source)=>[
                {
                    type: 'IndexCreate',
                    index: source,
                    reason: Reason.MissingInTarget
                }
            ],
        onExtra: (target)=>[
                {
                    type: 'IndexDrop',
                    indexName: target.name,
                    reason: Reason.MissingInSource
                }
            ],
        onCompare: (source, target)=>{
            const sourceUsing = source.using ?? 'btree';
            const targetUsing = target.using ?? 'btree';
            let reason = '';
            if (!haveEqualColumns(source.columnNames, target.columnNames)) {
                reason = `columns are different (${source.columnNames} vs ${target.columnNames})`;
            } else if (source.unique !== target.unique) {
                reason = `uniqueness is different (${source.unique} vs ${target.unique})`;
            } else if (sourceUsing !== targetUsing) {
                reason = `using method is different (${source.using} vs ${target.using})`;
            } else if (source.where !== target.where) {
                reason = `where clause is different (${source.where} vs ${target.where})`;
            } else if (source.expression !== target.expression) {
                reason = `expression is different (${source.expression} vs ${target.expression})`;
            }
            if (reason) {
                return [
                    {
                        type: 'IndexDrop',
                        indexName: target.name,
                        reason
                    },
                    {
                        type: 'IndexCreate',
                        index: source,
                        reason
                    }
                ];
            }
            return [];
        }
    });

const compareTriggers = ()=>({
        onMissing: (source)=>[
                {
                    type: 'TriggerCreate',
                    trigger: source,
                    reason: Reason.MissingInTarget
                }
            ],
        onExtra: (target)=>[
                {
                    type: 'TriggerDrop',
                    tableName: target.tableName,
                    triggerName: target.name,
                    reason: Reason.MissingInSource
                }
            ],
        onCompare: (source, target)=>{
            let reason = '';
            if (source.functionName !== target.functionName) {
                reason = `function is different (${source.functionName} vs ${target.functionName})`;
            } else if (source.actions.join(' OR ') !== target.actions.join(' OR ')) {
                reason = `action is different (${source.actions} vs ${target.actions})`;
            } else if (source.timing !== target.timing) {
                reason = `timing method is different (${source.timing} vs ${target.timing})`;
            } else if (source.scope !== target.scope) {
                reason = `scope is different (${source.scope} vs ${target.scope})`;
            } else if (source.referencingNewTableAs !== target.referencingNewTableAs) {
                reason = `new table reference is different (${source.referencingNewTableAs} vs ${target.referencingNewTableAs})`;
            } else if (source.referencingOldTableAs !== target.referencingOldTableAs) {
                reason = `old table reference is different (${source.referencingOldTableAs} vs ${target.referencingOldTableAs})`;
            }
            if (reason) {
                return [
                    {
                        type: 'TriggerCreate',
                        trigger: source,
                        reason
                    }
                ];
            }
            return [];
        }
    });

const compareTables = (options)=>({
        onMissing: (source)=>[
                {
                    type: 'TableCreate',
                    table: source,
                    reason: Reason.MissingInTarget
                }
            ],
        onExtra: (target)=>[
                {
                    type: 'TableDrop',
                    tableName: target.name,
                    reason: Reason.MissingInSource
                }
            ],
        onCompare: (source, target)=>{
            return [
                ...compare(source.columns, target.columns, options.columns, compareColumns()),
                ...compare(source.indexes, target.indexes, options.indexes, compareIndexes()),
                ...compare(source.constraints, target.constraints, options.constraints, compareConstraints()),
                ...compare(source.triggers, target.triggers, options.triggers, compareTriggers())
            ];
        }
    });

const asOverrideKey = (type, name)=>`${type}:${name}`;
const isNamingInterface = (strategy)=>{
    return typeof strategy === 'object' && typeof strategy.getName === 'function';
};
const asNamingStrategy = (strategy)=>{
    if (isNamingInterface(strategy)) {
        return strategy;
    }
    switch(strategy){
        case 'hash':
            {
                return new HashNamingStrategy();
            }
        default:
            {
                return new DefaultNamingStrategy();
            }
    }
};
const asUuidFunctionStrategy = (uuidFunction)=>{
    return typeof uuidFunction === 'string' ? ()=>uuidFunction : uuidFunction;
};
const defaultUuidFunction = (version)=>{
    return version === 4 ? 'uuidv4()' : 'uuidv7()';
};
class BaseContext {
    databaseName;
    schemaName;
    overrideTableName;
    uuidFunctionFactory;
    tables = [];
    functions = [];
    enums = [];
    extensions = [];
    parameters = [];
    overrides = [];
    warnings = [];
    namingStrategy;
    constructor(options){
        this.databaseName = options.databaseName ?? 'postgres';
        this.schemaName = options.schemaName ?? 'public';
        this.overrideTableName = options.overrideTableName ?? 'migration_overrides';
        this.namingStrategy = asNamingStrategy(options.namingStrategy ?? 'hash');
        this.uuidFunctionFactory = asUuidFunctionStrategy(options.uuidFunction ?? defaultUuidFunction);
    }
    getNameFor(item) {
        return this.namingStrategy.getName(item);
    }
    getTableByName(name) {
        return this.tables.find((table)=>table.name === name);
    }
    warn(context, message) {
        this.warnings.push(`[${context}] ${message}`);
    }
    build() {
        const overrideMap = new Map();
        for (const override of this.overrides){
            const { type, name } = override.value;
            overrideMap.set(asOverrideKey(type, name), override);
        }
        for (const func of this.functions){
            func.override = overrideMap.get(asOverrideKey('function', func.name));
        }
        for (const { indexes, triggers } of this.tables){
            for (const index of indexes){
                index.override = overrideMap.get(asOverrideKey('index', index.name));
            }
            for (const trigger of triggers){
                trigger.override = overrideMap.get(asOverrideKey('trigger', trigger.name));
            }
        }
        return {
            databaseName: this.databaseName,
            schemaName: this.schemaName,
            tables: this.tables,
            functions: this.functions,
            enums: this.enums,
            extensions: this.extensions,
            parameters: this.parameters,
            overrides: this.overrides,
            warnings: this.warnings
        };
    }
}

const transformColumns = (ctx, item)=>{
    switch(item.type){
        case 'ColumnAdd':
            {
                return asColumnAdd(item.column);
            }
        case 'ColumnAlter':
            {
                return asColumnAlter(item.tableName, item.columnName, item.changes);
            }
        case 'ColumnRename':
            {
                return `ALTER TABLE "${item.tableName}" RENAME COLUMN "${item.oldName}" TO "${item.newName}";`;
            }
        case 'ColumnDrop':
            {
                return `ALTER TABLE "${item.tableName}" DROP COLUMN "${item.columnName}";`;
            }
        default:
            {
                return false;
            }
    }
};
const asColumnAdd = (column)=>{
    return `ALTER TABLE "${column.tableName}" ADD "${column.name}" ${getColumnType(column)}` + getColumnModifiers(column) + ';';
};
const asColumnAlter = (tableName, columnName, changes)=>{
    const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`;
    const items = [];
    if (changes.nullable !== undefined) {
        items.push(changes.nullable ? `${base} DROP NOT NULL;` : `${base} SET NOT NULL;`);
    }
    if (changes.default !== undefined) {
        items.push(`${base} SET DEFAULT ${changes.default};`);
    }
    if (changes.storage !== undefined) {
        items.push(`${base} SET STORAGE ${changes.storage.toUpperCase()};`);
    }
    if (changes.comment !== undefined) {
        items.push(asColumnComment(tableName, columnName, changes.comment));
    }
    return items;
};

const transformConstraints = (ctx, item)=>{
    switch(item.type){
        case 'ConstraintAdd':
            {
                return `ALTER TABLE "${item.constraint.tableName}" ADD ${asConstraintBody(item.constraint)};`;
            }
        case 'ConstraintRename':
            {
                return `ALTER TABLE "${item.tableName}" RENAME CONSTRAINT "${item.oldName}" TO "${item.newName}";`;
            }
        case 'ConstraintDrop':
            {
                return `ALTER TABLE "${item.tableName}" DROP CONSTRAINT "${item.constraintName}";`;
            }
        default:
            {
                return false;
            }
    }
};
const withAction = (constraint)=>` ON UPDATE ${constraint.onUpdate ?? ActionType.NO_ACTION} ON DELETE ${constraint.onDelete ?? ActionType.NO_ACTION}`;
const asConstraintBody = (constraint)=>{
    const base = `CONSTRAINT "${constraint.name}"`;
    const type = constraint.type;
    switch(type){
        case ConstraintType.PRIMARY_KEY:
            {
                const columnNames = asColumnList(constraint.columnNames);
                return `${base} PRIMARY KEY (${columnNames})`;
            }
        case ConstraintType.FOREIGN_KEY:
            {
                const columnNames = asColumnList(constraint.columnNames);
                const referenceColumnNames = asColumnList(constraint.referenceColumnNames);
                return `${base} FOREIGN KEY (${columnNames}) REFERENCES "${constraint.referenceTableName}" (${referenceColumnNames})` + withAction(constraint);
            }
        case ConstraintType.UNIQUE:
            {
                const columnNames = asColumnList(constraint.columnNames);
                return `${base} UNIQUE (${columnNames})`;
            }
        case ConstraintType.CHECK:
            {
                return `${base} CHECK (${constraint.expression})`;
            }
        default:
            {
                throw new Error(`Unknown constraint type: ${type}`);
            }
    }
};

const transformEnums = (ctx, item)=>{
    switch(item.type){
        case 'EnumCreate':
            {
                return asEnumCreate(item.enum);
            }
        case 'EnumDrop':
            {
                return asEnumDrop(item.enumName);
            }
        default:
            {
                return false;
            }
    }
};
const asEnumCreate = ({ name, values })=>{
    return `CREATE TYPE "${name}" AS ENUM (${values.map((value)=>`'${value}'`)});`;
};
const asEnumDrop = (enumName)=>{
    return `DROP TYPE "${enumName}";`;
};

const transformExtensions = (ctx, item)=>{
    switch(item.type){
        case 'ExtensionCreate':
            {
                return asExtensionCreate(item.extension);
            }
        case 'ExtensionDrop':
            {
                return asExtensionDrop(item.extensionName);
            }
        default:
            {
                return false;
            }
    }
};
const asExtensionCreate = (extension)=>{
    return `CREATE EXTENSION IF NOT EXISTS "${extension.name}";`;
};
const asExtensionDrop = (extensionName)=>{
    return `DROP EXTENSION "${extensionName}";`;
};

const transformFunctions = (ctx, item)=>{
    switch(item.type){
        case 'FunctionCreate':
            {
                return asFunctionCreate(item.function);
            }
        case 'FunctionDrop':
            {
                return asFunctionDrop(item.functionName);
            }
        default:
            {
                return false;
            }
    }
};
const asFunctionCreate = (func)=>{
    return func.expression;
};
const asFunctionDrop = (functionName)=>{
    return `DROP FUNCTION ${functionName};`;
};

const transformIndexes = (ctx, item)=>{
    switch(item.type){
        case 'IndexCreate':
            {
                return asIndexCreate(item.index);
            }
        case 'IndexRename':
            {
                return `ALTER INDEX "${item.oldName}" RENAME TO "${item.newName}";`;
            }
        case 'IndexDrop':
            {
                return `DROP INDEX "${item.indexName}";`;
            }
        default:
            {
                return false;
            }
    }
};
const asIndexCreate = (index)=>{
    let sql = `CREATE`;
    if (index.unique) {
        sql += ' UNIQUE';
    }
    sql += ` INDEX "${index.name}" ON "${index.tableName}"`;
    if (index.columnNames) {
        const columnNames = asColumnList(index.columnNames);
        sql += ` (${columnNames})`;
    }
    if (index.using && index.using !== 'btree') {
        sql += ` USING ${index.using}`;
    }
    if (index.expression) {
        sql += ` (${index.expression})`;
    }
    if (index.with) {
        sql += ` WITH (${index.with})`;
    }
    if (index.where) {
        sql += ` WHERE ${index.where}`;
    }
    return sql + ';';
};

const transformOverrides = (ctx, item)=>{
    const tableName = ctx.overrideTableName;
    switch(item.type){
        case 'OverrideCreate':
            {
                return asOverrideCreate(tableName, item.override);
            }
        case 'OverrideUpdate':
            {
                return asOverrideUpdate(tableName, item.override);
            }
        case 'OverrideDrop':
            {
                return asOverrideDrop(tableName, item.overrideName);
            }
        default:
            {
                return false;
            }
    }
};
const asOverrideCreate = (tableName, override)=>{
    return `INSERT INTO "${tableName}" ("name", "value") VALUES ('${override.name}', ${asJsonString(override.value)});`;
};
const asOverrideUpdate = (tableName, override)=>{
    return `UPDATE "${tableName}" SET "value" = ${asJsonString(override.value)} WHERE "name" = '${override.name}';`;
};
const asOverrideDrop = (tableName, overrideName)=>{
    return `DELETE FROM "${tableName}" WHERE "name" = '${overrideName}';`;
};

const transformParameters = (ctx, item)=>{
    switch(item.type){
        case 'ParameterSet':
            {
                return asParameterSet(item.parameter);
            }
        case 'ParameterReset':
            {
                return asParameterReset(item.databaseName, item.parameterName);
            }
        default:
            {
                return false;
            }
    }
};
const asParameterSet = (parameter)=>{
    let sql = '';
    if (parameter.scope === 'database') {
        sql += `ALTER DATABASE "${parameter.databaseName}" `;
    }
    sql += `SET ${parameter.name} TO ${parameter.value}`;
    return sql;
};
const asParameterReset = (databaseName, parameterName)=>{
    return `ALTER DATABASE "${databaseName}" RESET "${parameterName}"`;
};

const transformTriggers = (ctx, item)=>{
    switch(item.type){
        case 'TriggerCreate':
            {
                return asTriggerCreate(item.trigger);
            }
        case 'TriggerDrop':
            {
                return asTriggerDrop(item.tableName, item.triggerName);
            }
        default:
            {
                return false;
            }
    }
};
const asTriggerCreate = (trigger)=>{
    const sql = [
        `CREATE OR REPLACE TRIGGER "${trigger.name}"`,
        `${trigger.timing.toUpperCase()} ${trigger.actions.map((action)=>action.toUpperCase()).join(' OR ')} ON "${trigger.tableName}"`
    ];
    if (trigger.referencingOldTableAs || trigger.referencingNewTableAs) {
        let statement = `REFERENCING`;
        if (trigger.referencingOldTableAs) {
            statement += ` OLD TABLE AS "${trigger.referencingOldTableAs}"`;
        }
        if (trigger.referencingNewTableAs) {
            statement += ` NEW TABLE AS "${trigger.referencingNewTableAs}"`;
        }
        sql.push(statement);
    }
    if (trigger.scope) {
        sql.push(`FOR EACH ${trigger.scope.toUpperCase()}`);
    }
    if (trigger.when) {
        sql.push(`WHEN (${trigger.when})`);
    }
    sql.push(`EXECUTE FUNCTION ${trigger.functionName}();`);
    return sql.join('\n  ');
};
const asTriggerDrop = (tableName, triggerName)=>{
    return `DROP TRIGGER "${triggerName}" ON "${tableName}";`;
};

const transformTables = (ctx, item)=>{
    switch(item.type){
        case 'TableCreate':
            {
                return asTableCreate(item.table);
            }
        case 'TableDrop':
            {
                return asTableDrop(item.tableName);
            }
        default:
            {
                return false;
            }
    }
};
const asTableCreate = (table)=>{
    const tableName = table.name;
    const items = [];
    for (const column of table.columns){
        items.push(`"${column.name}" ${getColumnType(column)}${getColumnModifiers(column)}`);
    }
    for (const constraint of table.constraints){
        items.push(asConstraintBody(constraint));
    }
    const sql = [
        `CREATE TABLE "${tableName}" (\n  ${items.join(',\n  ')}\n);`
    ];
    for (const column of table.columns){
        if (column.comment) {
            sql.push(asColumnComment(tableName, column.name, column.comment));
        }
        if (column.storage) {
            sql.push(...asColumnAlter(tableName, column.name, {
                storage: column.storage
            }));
        }
    }
    for (const index of table.indexes){
        sql.push(asIndexCreate(index));
    }
    for (const trigger of table.triggers){
        sql.push(asTriggerCreate(trigger));
    }
    return sql;
};
const asTableDrop = (tableName)=>{
    return `DROP TABLE "${tableName}";`;
};

const transformers = [
    transformColumns,
    transformConstraints,
    transformEnums,
    transformExtensions,
    transformFunctions,
    transformIndexes,
    transformParameters,
    transformTables,
    transformTriggers,
    transformOverrides
];

/**
 * Compute the difference between two database schemas
 */ const schemaDiff = (source, target, options = {})=>{
    const items = [
        ...compare(source.parameters, target.parameters, options.parameters, compareParameters()),
        ...compare(source.extensions, target.extensions, options.extensions, compareExtensions()),
        ...compare(source.functions, target.functions, options.functions, compareFunctions()),
        ...compare(source.enums, target.enums, options.enums, compareEnums()),
        ...compare(source.tables, target.tables, options.tables, compareTables(options)),
        ...compare(source.overrides, target.overrides, options.overrides, compareOverrides())
    ];
    const itemMap = {
        ColumnRename: [],
        ConstraintRename: [],
        IndexRename: [],
        ExtensionDrop: [],
        ExtensionCreate: [],
        ParameterSet: [],
        ParameterReset: [],
        FunctionDrop: [],
        FunctionCreate: [],
        EnumDrop: [],
        EnumCreate: [],
        TriggerDrop: [],
        ConstraintDrop: [],
        TableDrop: [],
        ColumnDrop: [],
        ColumnAdd: [],
        ColumnAlter: [],
        TableCreate: [],
        ConstraintAdd: [],
        TriggerCreate: [],
        IndexCreate: [],
        IndexDrop: [],
        OverrideCreate: [],
        OverrideUpdate: [],
        OverrideDrop: []
    };
    for (const item of items){
        itemMap[item.type].push(item);
    }
    const constraintAdds = itemMap.ConstraintAdd.filter((item)=>item.type === 'ConstraintAdd');
    const orderedItems = [
        ...itemMap.ExtensionCreate,
        ...itemMap.FunctionCreate,
        ...itemMap.ParameterSet,
        ...itemMap.ParameterReset,
        ...itemMap.EnumCreate,
        ...itemMap.TriggerDrop,
        ...itemMap.IndexDrop,
        ...itemMap.ConstraintDrop,
        ...itemMap.TableCreate,
        ...itemMap.ColumnAlter,
        ...itemMap.ColumnAdd,
        ...itemMap.ColumnRename,
        ...constraintAdds.filter(({ constraint })=>constraint.type === ConstraintType.PRIMARY_KEY),
        ...constraintAdds.filter(({ constraint })=>constraint.type === ConstraintType.FOREIGN_KEY),
        ...constraintAdds.filter(({ constraint })=>constraint.type === ConstraintType.UNIQUE),
        ...constraintAdds.filter(({ constraint })=>constraint.type === ConstraintType.CHECK),
        ...itemMap.ConstraintRename,
        ...itemMap.IndexCreate,
        ...itemMap.IndexRename,
        ...itemMap.TriggerCreate,
        ...itemMap.ColumnDrop,
        ...itemMap.TableDrop,
        ...itemMap.EnumDrop,
        ...itemMap.FunctionDrop,
        ...itemMap.OverrideCreate,
        ...itemMap.OverrideUpdate,
        ...itemMap.OverrideDrop
    ];
    return {
        items: orderedItems,
        asSql: (options)=>schemaDiffToSql(orderedItems, options),
        asHuman: ()=>schemaDiffToHuman(orderedItems)
    };
};
/**
 * Convert schema diffs into SQL statements
 */ const schemaDiffToSql = (items, options = {})=>{
    return items.flatMap((item)=>asSql(item, options));
};
/**
 * Convert schema diff into human readable statements
 */ const schemaDiffToHuman = (items)=>{
    return items.flatMap((item)=>asHuman(item));
};
const asSql = (item, options)=>{
    const ctx = new BaseContext(options);
    for (const transform of transformers){
        const result = transform(ctx, item);
        if (!result) {
            continue;
        }
        return asArray(result).map((result)=>result + withComments(options.comments, item));
    }
    throw new Error(`Unhandled schema diff type: ${item.type}`);
};
const asHuman = (item)=>{
    switch(item.type){
        case 'ExtensionCreate':
            {
                return `The extension "${item.extension.name}" is missing and needs to be created`;
            }
        case 'ExtensionDrop':
            {
                return `The extension "${item.extensionName}" exists but is no longer needed`;
            }
        case 'FunctionCreate':
            {
                return `The function "${item.function.name}" is missing and needs to be created`;
            }
        case 'FunctionDrop':
            {
                return `The function "${item.functionName}" exists but should be removed`;
            }
        case 'TableCreate':
            {
                return `The table "${item.table.name}" is missing and needs to be created`;
            }
        case 'TableDrop':
            {
                return `The table "${item.tableName}" exists but should be removed`;
            }
        case 'ColumnAdd':
            {
                return `The column "${item.column.tableName}"."${item.column.name}" is missing and needs to be created`;
            }
        case 'ColumnRename':
            {
                return `The column "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`;
            }
        case 'ColumnAlter':
            {
                return `The column "${item.tableName}"."${item.columnName}" has changes that need to be applied ${JSON.stringify(item.changes)}`;
            }
        case 'ColumnDrop':
            {
                return `The column "${item.tableName}"."${item.columnName}" exists but should be removed`;
            }
        case 'ConstraintAdd':
            {
                return `The constraint "${item.constraint.tableName}"."${item.constraint.name}" (${item.constraint.type}) is missing and needs to be created`;
            }
        case 'ConstraintRename':
            {
                return `The constraint "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`;
            }
        case 'ConstraintDrop':
            {
                return `The constraint "${item.tableName}"."${item.constraintName}" exists but should be removed`;
            }
        case 'IndexCreate':
            {
                return `The index "${item.index.tableName}"."${item.index.name}" is missing and needs to be created`;
            }
        case 'IndexRename':
            {
                return `The index "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`;
            }
        case 'IndexDrop':
            {
                return `The index "${item.indexName}" exists but is no longer needed`;
            }
        case 'TriggerCreate':
            {
                return `The trigger "${item.trigger.tableName}"."${item.trigger.name}" is missing and needs to be created`;
            }
        case 'TriggerDrop':
            {
                return `The trigger "${item.tableName}"."${item.triggerName}" exists but is no longer needed`;
            }
        case 'ParameterSet':
            {
                return `The configuration parameter "${item.parameter.name}" has a different value and needs to be updated to "${item.parameter.value}"`;
            }
        case 'ParameterReset':
            {
                return `The configuration parameter "${item.parameterName}" is set, but should be reset to the default value`;
            }
        case 'EnumCreate':
            {
                return `The enum "${item.enum.name}" is missing and needs to be created`;
            }
        case 'EnumDrop':
            {
                return `The enum "${item.enumName}" exists but is no longer needed`;
            }
        case 'OverrideCreate':
            {
                return `The override "${item.override.name}" is missing and needs to be created`;
            }
        case 'OverrideUpdate':
            {
                return `The override "${item.override.name}" needs to be updated`;
            }
        case 'OverrideDrop':
            {
                return `The override "${item.overrideName}" exists but is no longer needed`;
            }
    }
};
const withComments = (comments, item)=>{
    if (!comments) {
        return '';
    }
    return ` -- ${item.reason}`;
};
const asArray = (items)=>{
    if (Array.isArray(items)) {
        return items;
    }
    return [
        items
    ];
};

class ProcessorContext extends BaseContext {
    options;
    constructor(options){
        options.createForeignKeyIndexes = options.createForeignKeyIndexes ?? true;
        options.overrides = options.overrides ?? false;
        super(options), this.options = options;
    }
    classToTable = new WeakMap();
    tableToMetadata = new WeakMap();
    getTableByObject(object) {
        return this.classToTable.get(object);
    }
    getTableMetadata(table) {
        const metadata = this.tableToMetadata.get(table);
        if (!metadata) {
            throw new Error(`Table metadata not found for table: ${table.name}`);
        }
        return metadata;
    }
    addTable(table, options, object) {
        this.tables.push(table);
        this.classToTable.set(object, table);
        this.tableToMetadata.set(table, {
            options,
            object,
            methodToColumn: new Map()
        });
    }
    getColumnByObjectAndPropertyName(object, propertyName) {
        const table = this.getTableByObject(object.constructor);
        if (!table) {
            return {};
        }
        const tableMetadata = this.tableToMetadata.get(table);
        if (!tableMetadata) {
            return {};
        }
        const column = tableMetadata.methodToColumn.get(propertyName);
        return {
            table,
            column
        };
    }
    addColumn(table, column, options, propertyName) {
        table.columns.push(column);
        const tableMetadata = this.getTableMetadata(table);
        tableMetadata.methodToColumn.set(propertyName, column);
    }
    warnMissingTable(context, object, propertyName) {
        const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
        this.warn(context, `Unable to find table (${label})`);
    }
    warnMissingColumn(context, object, propertyName) {
        const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
        this.warn(context, `Unable to find column (${label})`);
    }
}

const processCheckConstraints = (ctx, items)=>{
    for (const { item: { object, options } } of items.filter((item)=>item.type === 'checkConstraint')){
        const table = ctx.getTableByObject(object);
        if (!table) {
            ctx.warnMissingTable('@Check', object);
            continue;
        }
        const tableName = table.name;
        table.constraints.push({
            type: ConstraintType.CHECK,
            name: options.name || ctx.getNameFor({
                type: 'check',
                tableName,
                expression: options.expression
            }),
            tableName,
            expression: options.expression,
            synchronize: options.synchronize ?? true
        });
    }
};

const processColumns = (ctx, items)=>{
    for (const { type, item: { object, propertyName, options } } of items.filter((item)=>item.type === 'column' || item.type === 'foreignKeyColumn')){
        const table = ctx.getTableByObject(object.constructor);
        if (!table) {
            ctx.warnMissingTable(type === 'column' ? '@Column' : '@ForeignKeyColumn', object, propertyName);
            continue;
        }
        const columnName = options.name ?? ctx.getNameFor({
            type: 'column',
            name: String(propertyName)
        });
        const existingColumn = table.columns.find((column)=>column.name === columnName);
        if (existingColumn) {
            continue;
        }
        if ('strategy' in options) {
            switch(options.strategy){
                // auto uuid based on database type
                case 'uuid':
                    {
                        options.type = 'uuid';
                        options.default = ()=>ctx.uuidFunctionFactory();
                        break;
                    }
                case 'uuid-v4':
                    {
                        options.type = 'uuid';
                        options.default = ()=>ctx.uuidFunctionFactory(4);
                        break;
                    }
                case 'uuid-v7':
                    {
                        options.type = 'uuid';
                        options.default = ()=>ctx.uuidFunctionFactory(7);
                        break;
                    }
                case 'identity':
                    {
                        options.identity = true;
                        options.type = 'integer';
                        break;
                    }
                default:
                    {
                        throw new Error(`Unsupported generated column strategy ${options.strategy}`);
                    }
            }
        }
        let defaultValue = fromColumnValue(options.default);
        let nullable = options.nullable ?? false;
        // map `{ default: null }` to `{ nullable: true }`
        if (defaultValue === null) {
            nullable = true;
            defaultValue = undefined;
        }
        const isEnum = !!options.enum;
        ctx.addColumn(table, {
            name: columnName,
            tableName: table.name,
            primary: options.primary ?? false,
            default: defaultValue,
            nullable,
            isArray: options.array ?? false,
            length: options.length,
            type: isEnum ? 'enum' : options.type || 'character varying',
            enumName: isEnum ? options.enum.name : undefined,
            comment: options.comment,
            storage: options.storage,
            identity: options.identity,
            synchronize: options.synchronize ?? true
        }, options, propertyName);
    }
};

const processConfigurationParameters = (ctx, items)=>{
    for (const { item: { options } } of items.filter((item)=>item.type === 'configurationParameter')){
        ctx.parameters.push({
            databaseName: ctx.databaseName,
            name: options.name,
            value: fromColumnValue(options.value),
            scope: options.scope,
            synchronize: options.synchronize ?? true
        });
    }
};

const processDatabases = (ctx, items)=>{
    for (const { item: { object, options } } of items.filter((item)=>item.type === 'database')){
        ctx.databaseName = options.name || ctx.getNameFor({
            type: 'database',
            name: object.name
        });
    }
};

const processEnums = (ctx, items)=>{
    for (const { item } of items.filter((item)=>item.type === 'enum')){
        // TODO log warnings if enum name is not unique
        ctx.enums.push(item);
    }
};

const processExtensions = (ctx, items)=>{
    if (ctx.options.extensions === false) {
        return;
    }
    for (const { item: { options } } of items.filter((item)=>item.type === 'extension')){
        ctx.extensions.push({
            name: options.name,
            synchronize: options.synchronize ?? true
        });
    }
};

const processForeignKeyColumns = (ctx, items)=>{
    for (const { item: { object, propertyName, options, target } } of items.filter((item)=>item.type === 'foreignKeyColumn')){
        const { table, column } = ctx.getColumnByObjectAndPropertyName(object, propertyName);
        if (!table) {
            ctx.warnMissingTable('@ForeignKeyColumn', object);
            continue;
        }
        if (!column) {
            // should be impossible since they are pre-created in `column.processor.ts`
            ctx.warnMissingColumn('@ForeignKeyColumn', object, propertyName);
            continue;
        }
        const referenceTable = ctx.getTableByObject(target());
        if (!referenceTable) {
            ctx.warnMissingTable('@ForeignKeyColumn', object, propertyName);
            continue;
        }
        const columnNames = [
            column.name
        ];
        const referenceColumns = referenceTable.columns.filter((column)=>column.primary);
        // infer FK column type from reference table
        if (referenceColumns.length === 1) {
            column.type = referenceColumns[0].type;
        }
        const referenceTableName = referenceTable.name;
        const referenceColumnNames = referenceColumns.map((column)=>column.name);
        const name = options.constraintName || ctx.getNameFor({
            type: 'foreignKey',
            tableName: table.name,
            columnNames,
            referenceTableName,
            referenceColumnNames
        });
        table.constraints.push({
            name,
            tableName: table.name,
            columnNames,
            type: ConstraintType.FOREIGN_KEY,
            referenceTableName,
            referenceColumnNames,
            onUpdate: options.onUpdate,
            onDelete: options.onDelete,
            synchronize: options.synchronize ?? true
        });
        if (options.unique || options.uniqueConstraintName) {
            table.constraints.push({
                name: options.uniqueConstraintName || ctx.getNameFor({
                    type: 'unique',
                    tableName: table.name,
                    columnNames
                }),
                tableName: table.name,
                columnNames,
                type: ConstraintType.UNIQUE,
                synchronize: options.synchronize ?? true
            });
        }
    }
};

const processForeignKeyConstraints = (ctx, items)=>{
    for (const { item: { object, options } } of items.filter((item)=>item.type === 'foreignKeyConstraint')){
        const table = ctx.getTableByObject(object);
        if (!table) {
            ctx.warnMissingTable('@ForeignKeyConstraint', {
                name: 'referenceTable'
            });
            continue;
        }
        const referenceTable = ctx.getTableByObject(options.referenceTable());
        if (!referenceTable) {
            const referenceTableName = options.referenceTable()?.name;
            ctx.warn('@ForeignKeyConstraint.referenceTable', `Unable to find table` + (referenceTableName ? ` (${referenceTableName})` : ''));
            continue;
        }
        let missingColumn = false;
        for (const columnName of options.columns){
            if (!table.columns.some(({ name })=>name === columnName)) {
                const metadata = ctx.getTableMetadata(table);
                ctx.warn('@ForeignKeyConstraint.columns', `Unable to find column (${metadata.object.name}.${columnName})`);
                missingColumn = true;
            }
        }
        for (const columnName of options.referenceColumns || []){
            if (!referenceTable.columns.some(({ name })=>name === columnName)) {
                const metadata = ctx.getTableMetadata(referenceTable);
                ctx.warn('@ForeignKeyConstraint.referenceColumns', `Unable to find column (${metadata.object.name}.${columnName})`);
                missingColumn = true;
            }
        }
        if (missingColumn) {
            continue;
        }
        const referenceTableName = referenceTable.name;
        const referenceColumnNames = options.referenceColumns || referenceTable.columns.filter(({ primary })=>primary).map(({ name })=>name);
        const name = options.name || ctx.getNameFor({
            type: 'foreignKey',
            tableName: table.name,
            columnNames: options.columns,
            referenceTableName,
            referenceColumnNames
        });
        table.constraints.push({
            type: ConstraintType.FOREIGN_KEY,
            name,
            tableName: table.name,
            columnNames: options.columns,
            referenceTableName,
            referenceColumnNames,
            onUpdate: options.onUpdate,
            onDelete: options.onDelete,
            synchronize: options.synchronize ?? true
        });
        if (options.index === false) {
            continue;
        }
        if (options.index || options.indexName || ctx.options.createForeignKeyIndexes) {
            const indexName = options.indexName || ctx.getNameFor({
                type: 'index',
                tableName: table.name,
                columnNames: options.columns
            });
            table.indexes.push({
                name: indexName,
                tableName: table.name,
                columnNames: options.columns,
                unique: false,
                synchronize: options.synchronize ?? true
            });
        }
    }
};

const processFunctions = (ctx, items)=>{
    if (ctx.options.functions === false) {
        return;
    }
    for (const { item } of items.filter((item)=>item.type === 'function')){
        // TODO log warnings if function name is not unique
        ctx.functions.push(item);
    }
};

const processIndexes = (ctx, items)=>{
    for (const { item: { object, options } } of items.filter((item)=>item.type === 'index')){
        const table = ctx.getTableByObject(object);
        if (!table) {
            ctx.warnMissingTable('@Check', object);
            continue;
        }
        const indexName = options.name || ctx.getNameFor({
            type: 'index',
            tableName: table.name,
            columnNames: options.columns,
            where: options.where
        });
        table.indexes.push({
            name: indexName,
            tableName: table.name,
            unique: options.unique ?? false,
            expression: options.expression,
            using: options.using,
            with: options.with,
            where: options.where,
            columnNames: options.columns,
            synchronize: options.synchronize ?? true
        });
    }
    // column indexes
    for (const { type, item: { object, propertyName, options } } of items.filter((item)=>item.type === 'column' || item.type === 'foreignKeyColumn')){
        const { table, column } = ctx.getColumnByObjectAndPropertyName(object, propertyName);
        if (!table) {
            ctx.warnMissingTable('@Column', object);
            continue;
        }
        if (!column) {
            // should be impossible since they are created in `column.processor.ts`
            ctx.warnMissingColumn('@Column', object, propertyName);
            continue;
        }
        if (options.index === false) {
            continue;
        }
        const isIndexRequested = options.indexName || options.index || type === 'foreignKeyColumn' && ctx.options.createForeignKeyIndexes;
        if (!isIndexRequested) {
            continue;
        }
        const indexName = options.indexName || ctx.getNameFor({
            type: 'index',
            tableName: table.name,
            columnNames: [
                column.name
            ]
        });
        const isIndexPresent = table.indexes.some((index)=>index.name === indexName);
        if (isIndexPresent) {
            continue;
        }
        const isOnlyPrimaryColumn = options.primary && table.columns.filter(({ primary })=>primary === true).length === 1;
        if (isOnlyPrimaryColumn) {
            continue;
        }
        table.indexes.push({
            name: indexName,
            tableName: table.name,
            unique: false,
            columnNames: [
                column.name
            ],
            synchronize: options.synchronize ?? true
        });
    }
};

const processOverrides = (ctx)=>{
    if (ctx.options.overrides === false) {
        return;
    }
    for (const func of ctx.functions){
        if (!func.synchronize) {
            continue;
        }
        ctx.overrides.push({
            name: `function_${func.name}`,
            value: {
                type: 'function',
                name: func.name,
                sql: asFunctionCreate(func)
            },
            synchronize: true
        });
    }
    for (const { triggers, indexes } of ctx.tables){
        for (const trigger of triggers){
            if (!trigger.synchronize) {
                continue;
            }
            ctx.overrides.push({
                name: `trigger_${trigger.name}`,
                value: {
                    type: 'trigger',
                    name: trigger.name,
                    sql: asTriggerCreate(trigger)
                },
                synchronize: true
            });
        }
        for (const index of indexes){
            if (!index.synchronize) {
                continue;
            }
            if (index.expression || index.using || index.with || index.where) {
                ctx.overrides.push({
                    name: `index_${index.name}`,
                    value: {
                        type: 'index',
                        name: index.name,
                        sql: asIndexCreate(index)
                    },
                    synchronize: true
                });
            }
        }
    }
};

const processPrimaryKeyConstraints = (ctx)=>{
    for (const table of ctx.tables){
        const columnNames = [];
        for (const column of table.columns){
            if (column.primary) {
                columnNames.push(column.name);
            }
        }
        if (columnNames.length > 0) {
            const tableMetadata = ctx.getTableMetadata(table);
            table.constraints.push({
                type: ConstraintType.PRIMARY_KEY,
                name: tableMetadata.options.primaryConstraintName || ctx.getNameFor({
                    type: 'primaryKey',
                    tableName: table.name,
                    columnNames
                }),
                tableName: table.name,
                columnNames,
                synchronize: tableMetadata.options.synchronize ?? true
            });
        }
    }
};

const processTables = (ctx, items)=>{
    for (const { item: { options, object } } of items.filter((item)=>item.type === 'table')){
        const test = ctx.getTableByObject(object);
        if (test) {
            throw new Error(`Table ${test.name} has already been registered. Does ${object.name} have two @Table() decorators?`);
        }
        ctx.addTable({
            name: options.name || ctx.getNameFor({
                type: 'table',
                name: object.name
            }),
            columns: [],
            constraints: [],
            indexes: [],
            triggers: [],
            synchronize: options.synchronize ?? true
        }, options, object);
    }
};

const processTriggers = (ctx, items)=>{
    for (const { item: { object, options } } of items.filter((item)=>item.type === 'trigger')){
        const table = ctx.getTableByObject(object);
        if (!table) {
            ctx.warnMissingTable('@Trigger', object);
            continue;
        }
        const triggerName = options.name || ctx.getNameFor({
            type: 'trigger',
            tableName: table.name,
            actions: options.actions,
            scope: options.scope,
            timing: options.timing,
            functionName: options.functionName
        });
        table.triggers.push({
            name: triggerName,
            tableName: table.name,
            timing: options.timing,
            actions: options.actions,
            when: options.when,
            scope: options.scope,
            referencingNewTableAs: options.referencingNewTableAs,
            referencingOldTableAs: options.referencingOldTableAs,
            functionName: options.functionName,
            synchronize: options.synchronize ?? true
        });
    }
};

const processUniqueConstraints = (ctx, items)=>{
    for (const { item: { object, options } } of items.filter((item)=>item.type === 'uniqueConstraint')){
        const table = ctx.getTableByObject(object);
        if (!table) {
            ctx.warnMissingTable('@Unique', object);
            continue;
        }
        const tableName = table.name;
        const columnNames = options.columns;
        table.constraints.push({
            type: ConstraintType.UNIQUE,
            name: options.name || ctx.getNameFor({
                type: 'unique',
                tableName,
                columnNames
            }),
            tableName,
            columnNames,
            synchronize: options.synchronize ?? true
        });
    }
    // column level constraints
    for (const { type, item: { object, propertyName, options } } of items.filter((item)=>item.type === 'column' || item.type === 'foreignKeyColumn')){
        const { table, column } = ctx.getColumnByObjectAndPropertyName(object, propertyName);
        if (!table) {
            ctx.warnMissingTable('@Column', object);
            continue;
        }
        if (!column) {
            // should be impossible since they are created in `column.processor.ts`
            ctx.warnMissingColumn('@Column', object, propertyName);
            continue;
        }
        if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) {
            const uniqueConstraintName = options.uniqueConstraintName || ctx.getNameFor({
                type: 'unique',
                tableName: table.name,
                columnNames: [
                    column.name
                ]
            });
            table.constraints.push({
                type: ConstraintType.UNIQUE,
                name: uniqueConstraintName,
                tableName: table.name,
                columnNames: [
                    column.name
                ],
                synchronize: options.synchronize ?? true
            });
        }
    }
};

const processors = [
    processDatabases,
    processConfigurationParameters,
    processEnums,
    processExtensions,
    processFunctions,
    processTables,
    processColumns,
    processForeignKeyColumns,
    processForeignKeyConstraints,
    processUniqueConstraints,
    processCheckConstraints,
    processPrimaryKeyConstraints,
    processIndexes,
    processTriggers,
    processOverrides
];

/**
 * Load schema from code (decorators, etc)
 */ const schemaFromCode = (options = {})=>{
    try {
        const ctx = new ProcessorContext(options);
        const items = getRegisteredItems();
        for (const processor of processors){
            processor(ctx, items);
        }
        if (ctx.options.overrides) {
            ctx.tables.push({
                name: ctx.overrideTableName,
                columns: [
                    {
                        name: 'name',
                        tableName: ctx.overrideTableName,
                        primary: true,
                        type: 'character varying',
                        nullable: false,
                        isArray: false,
                        synchronize: true
                    },
                    {
                        name: 'value',
                        tableName: ctx.overrideTableName,
                        primary: false,
                        type: 'jsonb',
                        nullable: false,
                        isArray: false,
                        synchronize: true
                    }
                ],
                indexes: [],
                triggers: [],
                constraints: [
                    {
                        type: ConstraintType.PRIMARY_KEY,
                        name: `${ctx.overrideTableName}_pkey`,
                        tableName: ctx.overrideTableName,
                        columnNames: [
                            'name'
                        ],
                        synchronize: true
                    }
                ],
                synchronize: true
            });
        }
        return ctx.build();
    } finally{
        if (options.reset) {
            resetRegisteredItems();
        }
    }
};

class ReaderContext extends BaseContext {
    options;
    constructor(options){
        super(options), this.options = options;
    }
}

const readColumns = async (ctx, db)=>{
    const columns = await db.selectFrom('information_schema.columns as c').leftJoin('information_schema.element_types as o', (join)=>join.onRef('c.table_catalog', '=', 'o.object_catalog').onRef('c.table_schema', '=', 'o.object_schema').onRef('c.table_name', '=', 'o.object_name').on('o.object_type', '=', sql.lit('TABLE')).onRef('c.dtd_identifier', '=', 'o.collection_type_identifier')).leftJoin('pg_type as t', (join)=>join.onRef('t.typname', '=', 'c.udt_name').on('c.data_type', '=', sql.lit('USER-DEFINED'))).leftJoin('pg_enum as e', (join)=>join.onRef('e.enumtypid', '=', 't.oid')).select([
        'c.table_name',
        'c.column_name',
        // is ARRAY, USER-DEFINED, or data type
        'c.data_type',
        'c.column_default',
        'c.is_nullable',
        'c.character_maximum_length',
        // number types
        'c.numeric_precision',
        'c.numeric_scale',
        // date types
        'c.datetime_precision',
        // user defined type
        'c.udt_catalog',
        'c.udt_schema',
        'c.udt_name',
        // data type for ARRAYs
        'o.data_type as array_type'
    ]).where('table_schema', '=', ctx.schemaName).execute();
    const enumRaw = await db.selectFrom('pg_type').innerJoin('pg_namespace', (join)=>join.onRef('pg_namespace.oid', '=', 'pg_type.typnamespace').on('pg_namespace.nspname', '=', ctx.schemaName)).where('typtype', '=', sql.lit('e')).select((eb)=>[
            'pg_type.typname as name',
            jsonArrayFrom(eb.selectFrom('pg_enum as e').select([
                'e.enumlabel as value'
            ]).whereRef('e.enumtypid', '=', 'pg_type.oid')).as('values')
        ]).execute();
    const enums = enumRaw.map((item)=>({
            name: item.name,
            values: item.values.map(({ value })=>value)
        }));
    for (const { name, values } of enums){
        ctx.enums.push({
            name,
            values,
            synchronize: true
        });
    }
    const enumMap = Object.fromEntries(enums.map((e)=>[
            e.name,
            e.values
        ]));
    // add columns to tables
    for (const column of columns){
        const table = ctx.getTableByName(column.table_name);
        if (!table) {
            continue;
        }
        const columnName = column.column_name;
        const item = {
            type: column.data_type,
            // TODO infer this from PK constraints
            primary: false,
            name: columnName,
            tableName: column.table_name,
            nullable: column.is_nullable === 'YES',
            isArray: column.array_type !== null,
            numericPrecision: column.numeric_precision ?? undefined,
            numericScale: column.numeric_scale ?? undefined,
            length: column.character_maximum_length ?? undefined,
            default: column.column_default ?? undefined,
            synchronize: true
        };
        const columnLabel = `${table.name}.${columnName}`;
        switch(column.data_type){
            // array types
            case 'ARRAY':
                {
                    if (!column.array_type) {
                        ctx.warnings.push(`Unable to find type for ${columnLabel} (ARRAY)`);
                        continue;
                    }
                    item.type = column.array_type;
                    break;
                }
            // enum types
            case 'USER-DEFINED':
                {
                    if (!enumMap[column.udt_name]) {
                        ctx.warnings.push(`Unable to find type for ${columnLabel} (ENUM)`);
                        continue;
                    }
                    item.type = 'enum';
                    item.enumName = column.udt_name;
                    break;
                }
        }
        table.columns.push(item);
    }
};

const readComments = async (ctx, db)=>{
    const comments = await db.selectFrom('pg_description as d').innerJoin('pg_class as c', 'd.objoid', 'c.oid').leftJoin('pg_attribute as a', (join)=>join.onRef('a.attrelid', '=', 'c.oid').onRef('a.attnum', '=', 'd.objsubid')).select([
        'c.relname as object_name',
        'c.relkind as object_type',
        'd.description as value',
        'a.attname as column_name'
    ]).where('d.description', 'is not', null).orderBy('object_type').orderBy('object_name').execute();
    for (const comment of comments){
        if (comment.object_type === 'r') {
            const table = ctx.getTableByName(comment.object_name);
            if (!table) {
                continue;
            }
            if (comment.column_name) {
                const column = table.columns.find(({ name })=>name === comment.column_name);
                if (column) {
                    column.comment = comment.value;
                }
            }
        }
    }
};

const readConstraints = async (ctx, db)=>{
    const constraints = await db.selectFrom('pg_constraint').innerJoin('pg_namespace', 'pg_namespace.oid', 'pg_constraint.connamespace') // namespace
    .innerJoin('pg_class as source_table', (join)=>join.onRef('source_table.oid', '=', 'pg_constraint.conrelid').on('source_table.relkind', 'in', [
            // ordinary table
            sql.lit('r'),
            // partitioned table
            sql.lit('p'),
            // foreign table
            sql.lit('f')
        ])) // table
    .leftJoin('pg_class as reference_table', 'reference_table.oid', 'pg_constraint.confrelid') // reference table
    .select((eb)=>[
            'pg_constraint.contype as constraint_type',
            'pg_constraint.conname as constraint_name',
            'source_table.relname as table_name',
            'reference_table.relname as reference_table_name',
            'pg_constraint.confupdtype as update_action',
            'pg_constraint.confdeltype as delete_action',
            // 'pg_constraint.oid as constraint_id',
            eb.selectFrom('pg_attribute')// matching table for PK, FK, and UQ
            .whereRef('pg_attribute.attrelid', '=', 'pg_constraint.conrelid').whereRef('pg_attribute.attnum', '=', sql`any("pg_constraint"."conkey")`).select((eb)=>eb.fn('json_agg', [
                    'pg_attribute.attname'
                ]).as('column_name')).as('column_names'),
            eb.selectFrom('pg_attribute')// matching foreign table for FK
            .whereRef('pg_attribute.attrelid', '=', 'pg_constraint.confrelid').whereRef('pg_attribute.attnum', '=', sql`any("pg_constraint"."confkey")`).select((eb)=>eb.fn('json_agg', [
                    'pg_attribute.attname'
                ]).as('column_name')).as('reference_column_names'),
            eb.fn('pg_get_constraintdef', [
                'pg_constraint.oid'
            ]).as('expression')
        ]).where('pg_namespace.nspname', '=', ctx.schemaName).execute();
    for (const constraint of constraints){
        const table = ctx.getTableByName(constraint.table_name);
        if (!table) {
            continue;
        }
        const constraintName = constraint.constraint_name;
        switch(constraint.constraint_type){
            // primary key constraint
            case 'p':
                {
                    if (!constraint.column_names) {
                        ctx.warnings.push(`Skipping CONSTRAINT "${constraintName}", no columns found`);
                        continue;
                    }
                    table.constraints.push({
                        type: ConstraintType.PRIMARY_KEY,
                        name: constraintName,
                        tableName: constraint.table_name,
                        columnNames: constraint.column_names,
                        synchronize: true
                    });
                    break;
                }
            // foreign key constraint
            case 'f':
                {
                    if (!constraint.column_names || !constraint.reference_table_name || !constraint.reference_column_names) {
                        ctx.warnings.push(`Skipping CONSTRAINT "${constraintName}", missing either columns, referenced table, or referenced columns,`);
                        continue;
                    }
                    table.constraints.push({
                        type: ConstraintType.FOREIGN_KEY,
                        name: constraintName,
                        tableName: constraint.table_name,
                        columnNames: constraint.column_names,
                        referenceTableName: constraint.reference_table_name,
                        referenceColumnNames: constraint.reference_column_names,
                        onUpdate: asDatabaseAction(constraint.update_action),
                        onDelete: asDatabaseAction(constraint.delete_action),
                        synchronize: true
                    });
                    break;
                }
            // unique constraint
            case 'u':
                {
                    table.constraints.push({
                        type: ConstraintType.UNIQUE,
                        name: constraintName,
                        tableName: constraint.table_name,
                        columnNames: constraint.column_names,
                        synchronize: true
                    });
                    break;
                }
            //  check constraint
            case 'c':
                {
                    table.constraints.push({
                        type: ConstraintType.CHECK,
                        name: constraint.constraint_name,
                        tableName: constraint.table_name,
                        expression: constraint.expression.replace('CHECK ', ''),
                        synchronize: true
                    });
                    break;
                }
        }
    }
};
const asDatabaseAction = (action)=>{
    switch(action){
        case 'a':
            {
                return ActionType.NO_ACTION;
            }
        case 'c':
            {
                return ActionType.CASCADE;
            }
        case 'r':
            {
                return ActionType.RESTRICT;
            }
        case 'n':
            {
                return ActionType.SET_NULL;
            }
        case 'd':
            {
                return ActionType.SET_DEFAULT;
            }
        default:
            {
                return ActionType.NO_ACTION;
            }
    }
};

const readExtensions = async (ctx, db)=>{
    const extensions = await db.selectFrom('pg_catalog.pg_extension')// .innerJoin('pg_namespace', 'pg_namespace.oid', 'pg_catalog.pg_extension.extnamespace')
    // .where('pg_namespace.nspname', '=', schemaName)
    .select([
        'extname as name',
        'extversion as version'
    ]).execute();
    for (const { name } of extensions){
        ctx.extensions.push({
            name,
            synchronize: true
        });
    }
};

const readFunctions = async (ctx, db)=>{
    const routines = await db.selectFrom('pg_proc as p').innerJoin('pg_namespace', 'pg_namespace.oid', 'p.pronamespace').leftJoin('pg_depend as d', (join)=>join.onRef('d.objid', '=', 'p.oid').on('d.deptype', '=', sql.lit('e'))).where('d.objid', 'is', sql.lit(null)).where('p.prokind', '=', sql.lit('f')).where('pg_namespace.nspname', '=', ctx.schemaName).select((eb)=>[
            'p.proname as name',
            eb.fn('pg_get_function_identity_arguments', [
                'p.oid'
            ]).as('arguments'),
            eb.fn('pg_get_functiondef', [
                'p.oid'
            ]).as('expression')
        ]).execute();
    for (const { name, expression } of routines){
        ctx.functions.push({
            name,
            // TODO read expression from the overrides table
            expression,
            synchronize: true
        });
    }
};

const readIndexes = async (ctx, db)=>{
    const indexes = await db.selectFrom('pg_index as ix')// matching index, which has column information
    .innerJoin('pg_class as i', 'ix.indexrelid', 'i.oid').innerJoin('pg_am as a', 'i.relam', 'a.oid')// matching table
    .innerJoin('pg_class as t', 'ix.indrelid', 't.oid')// namespace
    .innerJoin('pg_namespace', 'pg_namespace.oid', 'i.relnamespace')// PK and UQ constraints automatically have indexes, so we can ignore those
    .leftJoin('pg_constraint', (join)=>join.onRef('pg_constraint.conindid', '=', 'i.oid').on('pg_constraint.contype', 'in', [
            sql.lit('p'),
            sql.lit('u')
        ])).where('pg_constraint.oid', 'is', null).select((eb)=>[
            'i.relname as index_name',
            't.relname as table_name',
            'ix.indisunique as unique',
            'a.amname as using',
            eb.fn('pg_get_expr', [
                'ix.indexprs',
                'ix.indrelid'
            ]).as('expression'),
            eb.fn('pg_get_expr', [
                'ix.indpred',
                'ix.indrelid'
            ]).as('where'),
            eb.selectFrom('pg_attribute as a').where('t.relkind', '=', sql.lit('r')).whereRef('a.attrelid', '=', 't.oid')// list of columns numbers in the index
            .whereRef('a.attnum', '=', sql`any("ix"."indkey")`).select((eb)=>eb.fn('json_agg', [
                    'a.attname'
                ]).as('column_name')).as('column_names')
        ]).where('pg_namespace.nspname', '=', ctx.schemaName).where('ix.indisprimary', '=', sql.lit(false)).execute();
    for (const index of indexes){
        const table = ctx.getTableByName(index.table_name);
        if (!table) {
            continue;
        }
        table.indexes.push({
            name: index.index_name,
            tableName: index.table_name,
            columnNames: index.column_names ?? undefined,
            expression: index.expression ?? undefined,
            using: index.using,
            where: index.where ?? undefined,
            unique: index.unique,
            synchronize: true
        });
    }
};

const readName = async (ctx, db)=>{
    const result = await sql`SELECT current_database() as name`.execute(db);
    ctx.databaseName = result.rows[0].name;
};

const readOverrides = async (ctx, db)=>{
    try {
        const result = await sql.raw(`SELECT name, value FROM "${ctx.overrideTableName}"`).execute(db);
        for (const { name, value } of result.rows){
            ctx.overrides.push({
                name,
                value,
                synchronize: true
            });
        }
    } catch (error) {
        ctx.warn('Overrides', `Error reading override table: ${error}`);
    }
};

const readParameters = async (ctx, db)=>{
    const parameters = await db.selectFrom('pg_settings').where('source', 'in', [
        sql.lit('database'),
        sql.lit('user')
    ]).select([
        'name',
        'setting as value',
        'source as scope'
    ]).execute();
    for (const parameter of parameters){
        ctx.parameters.push({
            name: parameter.name,
            value: parameter.value,
            databaseName: ctx.databaseName,
            scope: parameter.scope,
            synchronize: true
        });
    }
};

const readTables = async (ctx, db)=>{
    const tables = await db.selectFrom('information_schema.tables').where('table_schema', '=', ctx.schemaName).where('table_type', '=', sql.lit('BASE TABLE')).selectAll().execute();
    for (const table of tables){
        ctx.tables.push({
            name: table.table_name,
            columns: [],
            indexes: [],
            triggers: [],
            constraints: [],
            synchronize: true
        });
    }
};

const readTriggers = async (ctx, db)=>{
    const triggers = await db.selectFrom('pg_trigger as t').innerJoin('pg_proc as p', 't.tgfoid', 'p.oid').innerJoin('pg_namespace as n', 'p.pronamespace', 'n.oid').innerJoin('pg_class as c', 't.tgrelid', 'c.oid').select((eb)=>[
            't.tgname as name',
            't.tgenabled as enabled',
            't.tgtype as type',
            't.tgconstraint as _constraint',
            't.tgdeferrable as is_deferrable',
            't.tginitdeferred as is_initially_deferred',
            't.tgargs as arguments',
            't.tgoldtable as referencing_old_table_as',
            't.tgnewtable as referencing_new_table_as',
            eb.fn('pg_get_expr', [
                't.tgqual',
                't.tgrelid'
            ]).as('when_expression'),
            'p.proname as function_name',
            'c.relname as table_name'
        ]).where('t.tgisinternal', '=', false) // Exclude internal system triggers
    .where('n.nspname', '=', ctx.schemaName).execute();
    // add triggers to tables
    for (const trigger of triggers){
        const table = ctx.getTableByName(trigger.table_name);
        if (!table) {
            continue;
        }
        table.triggers.push({
            name: trigger.name,
            tableName: trigger.table_name,
            functionName: trigger.function_name,
            referencingNewTableAs: trigger.referencing_new_table_as ?? undefined,
            referencingOldTableAs: trigger.referencing_old_table_as ?? undefined,
            when: trigger.when_expression,
            synchronize: true,
            ...parseTriggerType(trigger.type)
        });
    }
};
const hasMask = (input, mask)=>(input & mask) === mask;
const parseTriggerType = (type)=>{
    // eslint-disable-next-line unicorn/prefer-math-trunc
    const scope = hasMask(type, 1 << 0) ? 'row' : 'statement';
    let timing = 'after';
    const timingMasks = [
        {
            mask: 1 << 1,
            value: 'before'
        },
        {
            mask: 1 << 6,
            value: 'instead of'
        }
    ];
    for (const { mask, value } of timingMasks){
        if (hasMask(type, mask)) {
            timing = value;
            break;
        }
    }
    const actions = [];
    const actionMasks = [
        {
            mask: 1 << 2,
            value: 'insert'
        },
        {
            mask: 1 << 3,
            value: 'delete'
        },
        {
            mask: 1 << 4,
            value: 'update'
        },
        {
            mask: 1 << 5,
            value: 'truncate'
        }
    ];
    for (const { mask, value } of actionMasks){
        if (hasMask(type, mask)) {
            actions.push(value);
            break;
        }
    }
    if (actions.length === 0) {
        throw new Error(`Unable to parse trigger type ${type}`);
    }
    return {
        actions,
        timing,
        scope
    };
};

const readers = [
    readName,
    readParameters,
    readExtensions,
    readFunctions,
    readTables,
    readColumns,
    readIndexes,
    readConstraints,
    readTriggers,
    readComments,
    readOverrides
];

/**
 * Load schema from a database url
 */ const schemaFromDatabase = async (options = {})=>{
    const ctx = new ReaderContext(options);
    const db = new Kysely({
        dialect: new PostgresJSDialect({
            postgres: createPostgres(options)
        })
    });
    try {
        for (const reader of readers){
            await reader(ctx, db);
        }
        return ctx.build();
    } finally{
        await db.destroy();
    }
};

export { ActionType as A, ConstraintType as C, DatabaseSslMode as D, HashNamingStrategy as H, Migrator as M, Reason as R, asOptions as a, DefaultNamingStrategy as b, asHuman as c, asPostgresConfig as d, asSql as e, createPostgres as f, schemaDiffToHuman as g, schemaDiffToSql as h, isPostgresSsl as i, schemaFromCode as j, schemaFromDatabase as k, register as r, schemaDiff as s };
