"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Migrator = exports.NO_MIGRATIONS = exports.MIGRATION_LOCK_ID = exports.DEFAULT_ALLOW_UNORDERED_MIGRATIONS = exports.DEFAULT_MIGRATION_LOCK_TABLE = exports.DEFAULT_MIGRATION_TABLE = void 0;
const noop_plugin_js_1 = require("../plugin/noop-plugin.js");
const with_schema_plugin_js_1 = require("../plugin/with-schema/with-schema-plugin.js");
const object_utils_js_1 = require("../util/object-utils.js");
exports.DEFAULT_MIGRATION_TABLE = 'kysely_migration';
exports.DEFAULT_MIGRATION_LOCK_TABLE = 'kysely_migration_lock';
exports.DEFAULT_ALLOW_UNORDERED_MIGRATIONS = false;
exports.MIGRATION_LOCK_ID = 'migration_lock';
exports.NO_MIGRATIONS = (0, object_utils_js_1.freeze)({ __noMigrations__: true });
/**
 * A class for running migrations.
 *
 * ### Example
 *
 * This example uses the {@link FileMigrationProvider} that reads migrations
 * files from a single folder. You can easily implement your own
 * {@link MigrationProvider} if you want to provide migrations some
 * other way.
 *
 * ```ts
 * import { promises as fs } from 'node:fs'
 * import path from 'node:path'
 * import * as Sqlite from 'better-sqlite3'
 * import {
 *   FileMigrationProvider,
 *   Kysely,
 *   Migrator,
 *   SqliteDialect
 * } from 'kysely'
 *
 * const db = new Kysely<any>({
 *   dialect: new SqliteDialect({
 *     database: Sqlite(':memory:')
 *   })
 * })
 *
 * const migrator = new Migrator({
 *   db,
 *   provider: new FileMigrationProvider({
 *     fs,
 *     // Path to the folder that contains all your migrations.
 *     migrationFolder: 'some/path/to/migrations',
 *     path,
 *   })
 * })
 * ```
 */
class Migrator {
    #props;
    constructor(props) {
        this.#props = (0, object_utils_js_1.freeze)(props);
    }
    /**
     * Returns a {@link MigrationInfo} object for each migration.
     *
     * The returned array is sorted by migration name.
     */
    async getMigrations() {
        const tableExists = await this.#doesTableExist(this.#migrationTable);
        const executedMigrations = tableExists
            ? await this.#props.db
                .withPlugin(this.#schemaPlugin)
                .selectFrom(this.#migrationTable)
                .select(['name', 'timestamp'])
                .$narrowType()
                .execute()
            : [];
        const migrations = await this.#resolveMigrations();
        return migrations.map(({ name, ...migration }) => {
            const executed = executedMigrations.find((it) => it.name === name);
            return {
                name,
                migration,
                executedAt: executed ? new Date(executed.timestamp) : undefined,
            };
        });
    }
    /**
     * Runs all migrations that have not yet been run.
     *
     * This method returns a {@link MigrationResultSet} instance and _never_ throws.
     * {@link MigrationResultSet.error} holds the error if something went wrong.
     * {@link MigrationResultSet.results} contains information about which migrations
     * were executed and which failed. See the examples below.
     *
     * This method goes through all possible migrations provided by the provider and runs the
     * ones whose names come alphabetically after the last migration that has been run. If the
     * list of executed migrations doesn't match the beginning of the list of possible migrations
     * an error is returned.
     *
     * ### Examples
     *
     * ```ts
     * import { promises as fs } from 'node:fs'
     * import path from 'node:path'
     * import * as Sqlite from 'better-sqlite3'
     * import { FileMigrationProvider, Migrator } from 'kysely'
     *
     * const migrator = new Migrator({
     *   db,
     *   provider: new FileMigrationProvider({
     *     fs,
     *     migrationFolder: 'some/path/to/migrations',
     *     path,
     *   })
     * })
     *
     * const { error, results } = await migrator.migrateToLatest()
     *
     * results?.forEach((it) => {
     *   if (it.status === 'Success') {
     *     console.log(`migration "${it.migrationName}" was executed successfully`)
     *   } else if (it.status === 'Error') {
     *     console.error(`failed to execute migration "${it.migrationName}"`)
     *   }
     * })
     *
     * if (error) {
     *   console.error('failed to run `migrateToLatest`')
     *   console.error(error)
     * }
     * ```
     */
    async migrateToLatest() {
        return this.#migrate(() => ({ direction: 'Up', step: Infinity }));
    }
    /**
     * Migrate up/down to a specific migration.
     *
     * This method returns a {@link MigrationResultSet} instance and _never_ throws.
     * {@link MigrationResultSet.error} holds the error if something went wrong.
     * {@link MigrationResultSet.results} contains information about which migrations
     * were executed and which failed.
     *
     * ### Examples
     *
     * ```ts
     * import { promises as fs } from 'node:fs'
     * import path from 'node:path'
     * import { FileMigrationProvider, Migrator } from 'kysely'
     *
     * const migrator = new Migrator({
     *   db,
     *   provider: new FileMigrationProvider({
     *     fs,
     *     // Path to the folder that contains all your migrations.
     *     migrationFolder: 'some/path/to/migrations',
     *     path,
     *   })
     * })
     *
     * await migrator.migrateTo('some_migration')
     * ```
     *
     * If you specify the name of the first migration, this method migrates
     * down to the first migration, but doesn't run the `down` method of
     * the first migration. In case you want to migrate all the way down,
     * you can use a special constant `NO_MIGRATIONS`:
     *
     * ```ts
     * import { promises as fs } from 'node:fs'
     * import path from 'node:path'
     * import { FileMigrationProvider, Migrator, NO_MIGRATIONS } from 'kysely'
     *
     * const migrator = new Migrator({
     *   db,
     *   provider: new FileMigrationProvider({
     *     fs,
     *     // Path to the folder that contains all your migrations.
     *     migrationFolder: 'some/path/to/migrations',
     *     path,
     *   })
     * })
     *
     * await migrator.migrateTo(NO_MIGRATIONS)
     * ```
     */
    async migrateTo(targetMigrationName) {
        return this.#migrate(({ migrations, executedMigrations, pendingMigrations, }) => {
            if ((0, object_utils_js_1.isObject)(targetMigrationName) &&
                targetMigrationName.__noMigrations__ === true) {
                return { direction: 'Down', step: Infinity };
            }
            if (!migrations.find((m) => m.name === targetMigrationName)) {
                throw new Error(`migration "${targetMigrationName}" doesn't exist`);
            }
            const executedIndex = executedMigrations.indexOf(targetMigrationName);
            const pendingIndex = pendingMigrations.findIndex((m) => m.name === targetMigrationName);
            if (executedIndex !== -1) {
                return {
                    direction: 'Down',
                    step: executedMigrations.length - executedIndex - 1,
                };
            }
            else if (pendingIndex !== -1) {
                return { direction: 'Up', step: pendingIndex + 1 };
            }
            else {
                throw new Error(`migration "${targetMigrationName}" isn't executed or pending`);
            }
        });
    }
    /**
     * Migrate one step up.
     *
     * This method returns a {@link MigrationResultSet} instance and _never_ throws.
     * {@link MigrationResultSet.error} holds the error if something went wrong.
     * {@link MigrationResultSet.results} contains information about which migrations
     * were executed and which failed.
     *
     * ### Examples
     *
     * ```ts
     * import { promises as fs } from 'node:fs'
     * import path from 'node:path'
     * import { FileMigrationProvider, Migrator } from 'kysely'
     *
     * const migrator = new Migrator({
     *   db,
     *   provider: new FileMigrationProvider({
     *     fs,
     *     // Path to the folder that contains all your migrations.
     *     migrationFolder: 'some/path/to/migrations',
     *     path,
     *   })
     * })
     *
     * await migrator.migrateUp()
     * ```
     */
    async migrateUp() {
        return this.#migrate(() => ({ direction: 'Up', step: 1 }));
    }
    /**
     * Migrate one step down.
     *
     * This method returns a {@link MigrationResultSet} instance and _never_ throws.
     * {@link MigrationResultSet.error} holds the error if something went wrong.
     * {@link MigrationResultSet.results} contains information about which migrations
     * were executed and which failed.
     *
     * ### Examples
     *
     * ```ts
     * import { promises as fs } from 'node:fs'
     * import path from 'node:path'
     * import { FileMigrationProvider, Migrator } from 'kysely'
     *
     * const migrator = new Migrator({
     *   db,
     *   provider: new FileMigrationProvider({
     *     fs,
     *     // Path to the folder that contains all your migrations.
     *     migrationFolder: 'some/path/to/migrations',
     *     path,
     *   })
     * })
     *
     * await migrator.migrateDown()
     * ```
     */
    async migrateDown() {
        return this.#migrate(() => ({ direction: 'Down', step: 1 }));
    }
    async #migrate(getMigrationDirectionAndStep) {
        try {
            await this.#ensureMigrationTableSchemaExists();
            await this.#ensureMigrationTableExists();
            await this.#ensureMigrationLockTableExists();
            await this.#ensureLockRowExists();
            return await this.#runMigrations(getMigrationDirectionAndStep);
        }
        catch (error) {
            if (error instanceof MigrationResultSetError) {
                return error.resultSet;
            }
            return { error };
        }
    }
    get #migrationTableSchema() {
        return this.#props.migrationTableSchema;
    }
    get #migrationTable() {
        return this.#props.migrationTableName ?? exports.DEFAULT_MIGRATION_TABLE;
    }
    get #migrationLockTable() {
        return this.#props.migrationLockTableName ?? exports.DEFAULT_MIGRATION_LOCK_TABLE;
    }
    get #allowUnorderedMigrations() {
        return (this.#props.allowUnorderedMigrations ?? exports.DEFAULT_ALLOW_UNORDERED_MIGRATIONS);
    }
    get #schemaPlugin() {
        if (this.#migrationTableSchema) {
            return new with_schema_plugin_js_1.WithSchemaPlugin(this.#migrationTableSchema);
        }
        return new noop_plugin_js_1.NoopPlugin();
    }
    async #ensureMigrationTableSchemaExists() {
        if (!this.#migrationTableSchema) {
            // Use default schema. Nothing to do.
            return;
        }
        const schemaExists = await this.#doesSchemaExist();
        if (schemaExists) {
            return;
        }
        try {
            await this.#createIfNotExists(this.#props.db.schema.createSchema(this.#migrationTableSchema));
        }
        catch (error) {
            const schemaExists = await this.#doesSchemaExist();
            // At least on PostgreSQL, `if not exists` doesn't guarantee the `create schema`
            // query doesn't throw if the schema already exits. That's why we check if
            // the schema exist here and ignore the error if it does.
            if (!schemaExists) {
                throw error;
            }
        }
    }
    async #ensureMigrationTableExists() {
        const tableExists = await this.#doesTableExist(this.#migrationTable);
        if (tableExists) {
            return;
        }
        try {
            await this.#createIfNotExists(this.#props.db.schema
                .withPlugin(this.#schemaPlugin)
                .createTable(this.#migrationTable)
                .addColumn('name', 'varchar(255)', (col) => col.notNull().primaryKey())
                // The migration run time as ISO string. This is not a real date type as we
                // can't know which data type is supported by all future dialects.
                .addColumn('timestamp', 'varchar(255)', (col) => col.notNull()));
        }
        catch (error) {
            const tableExists = await this.#doesTableExist(this.#migrationTable);
            // At least on PostgreSQL, `if not exists` doesn't guarantee the `create table`
            // query doesn't throw if the table already exits. That's why we check if
            // the table exist here and ignore the error if it does.
            if (!tableExists) {
                throw error;
            }
        }
    }
    async #ensureMigrationLockTableExists() {
        const tableExists = await this.#doesTableExist(this.#migrationLockTable);
        if (tableExists) {
            return;
        }
        try {
            await this.#createIfNotExists(this.#props.db.schema
                .withPlugin(this.#schemaPlugin)
                .createTable(this.#migrationLockTable)
                .addColumn('id', 'varchar(255)', (col) => col.notNull().primaryKey())
                .addColumn('is_locked', 'integer', (col) => col.notNull().defaultTo(0)));
        }
        catch (error) {
            const tableExists = await this.#doesTableExist(this.#migrationLockTable);
            // At least on PostgreSQL, `if not exists` doesn't guarantee the `create table`
            // query doesn't throw if the table already exits. That's why we check if
            // the table exist here and ignore the error if it does.
            if (!tableExists) {
                throw error;
            }
        }
    }
    async #ensureLockRowExists() {
        const lockRowExists = await this.#doesLockRowExists();
        if (lockRowExists) {
            return;
        }
        try {
            await this.#props.db
                .withPlugin(this.#schemaPlugin)
                .insertInto(this.#migrationLockTable)
                .values({ id: exports.MIGRATION_LOCK_ID, is_locked: 0 })
                .execute();
        }
        catch (error) {
            const lockRowExists = await this.#doesLockRowExists();
            if (!lockRowExists) {
                throw error;
            }
        }
    }
    async #doesSchemaExist() {
        const schemas = await this.#props.db.introspection.getSchemas();
        return schemas.some((it) => it.name === this.#migrationTableSchema);
    }
    async #doesTableExist(tableName) {
        const schema = this.#migrationTableSchema;
        const tables = await this.#props.db.introspection.getTables({
            withInternalKyselyTables: true,
        });
        return tables.some((it) => it.name === tableName && (!schema || it.schema === schema));
    }
    async #doesLockRowExists() {
        const lockRow = await this.#props.db
            .withPlugin(this.#schemaPlugin)
            .selectFrom(this.#migrationLockTable)
            .where('id', '=', exports.MIGRATION_LOCK_ID)
            .select('id')
            .executeTakeFirst();
        return !!lockRow;
    }
    async #runMigrations(getMigrationDirectionAndStep) {
        const adapter = this.#props.db.getExecutor().adapter;
        const lockOptions = (0, object_utils_js_1.freeze)({
            lockTable: this.#props.migrationLockTableName ?? exports.DEFAULT_MIGRATION_LOCK_TABLE,
            lockRowId: exports.MIGRATION_LOCK_ID,
            lockTableSchema: this.#props.migrationTableSchema,
        });
        const run = async (db) => {
            try {
                await adapter.acquireMigrationLock(db, lockOptions);
                const state = await this.#getState(db);
                if (state.migrations.length === 0) {
                    return { results: [] };
                }
                const { direction, step } = getMigrationDirectionAndStep(state);
                if (step <= 0) {
                    return { results: [] };
                }
                if (direction === 'Down') {
                    return await this.#migrateDown(db, state, step);
                }
                else if (direction === 'Up') {
                    return await this.#migrateUp(db, state, step);
                }
                return { results: [] };
            }
            finally {
                await adapter.releaseMigrationLock(db, lockOptions);
            }
        };
        if (adapter.supportsTransactionalDdl && !this.#props.disableTransactions) {
            return this.#props.db.transaction().execute(run);
        }
        else {
            return this.#props.db.connection().execute(run);
        }
    }
    async #getState(db) {
        const migrations = await this.#resolveMigrations();
        const executedMigrations = await this.#getExecutedMigrations(db);
        this.#ensureNoMissingMigrations(migrations, executedMigrations);
        if (!this.#allowUnorderedMigrations) {
            this.#ensureMigrationsInOrder(migrations, executedMigrations);
        }
        const pendingMigrations = this.#getPendingMigrations(migrations, executedMigrations);
        return (0, object_utils_js_1.freeze)({
            migrations,
            executedMigrations,
            lastMigration: (0, object_utils_js_1.getLast)(executedMigrations),
            pendingMigrations,
        });
    }
    #getPendingMigrations(migrations, executedMigrations) {
        return migrations.filter((migration) => {
            return !executedMigrations.includes(migration.name);
        });
    }
    async #resolveMigrations() {
        const allMigrations = await this.#props.provider.getMigrations();
        return Object.keys(allMigrations)
            .sort()
            .map((name) => ({
            ...allMigrations[name],
            name,
        }));
    }
    async #getExecutedMigrations(db) {
        const executedMigrations = await db
            .withPlugin(this.#schemaPlugin)
            .selectFrom(this.#migrationTable)
            .select(['name', 'timestamp'])
            .$narrowType()
            .execute();
        const nameComparator = this.#props.nameComparator || ((a, b) => a.localeCompare(b));
        return (executedMigrations
            // https://github.com/kysely-org/kysely/issues/843
            .sort((a, b) => {
            if (a.timestamp === b.timestamp) {
                return nameComparator(a.name, b.name);
            }
            return (new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
        })
            .map((it) => it.name));
    }
    #ensureNoMissingMigrations(migrations, executedMigrations) {
        // Ensure all executed migrations exist in the `migrations` list.
        for (const executed of executedMigrations) {
            if (!migrations.some((it) => it.name === executed)) {
                throw new Error(`corrupted migrations: previously executed migration ${executed} is missing`);
            }
        }
    }
    #ensureMigrationsInOrder(migrations, executedMigrations) {
        // Ensure the executed migrations are the first ones in the migration list.
        for (let i = 0; i < executedMigrations.length; ++i) {
            if (migrations[i].name !== executedMigrations[i]) {
                throw new Error(`corrupted migrations: expected previously executed migration ${executedMigrations[i]} to be at index ${i} but ${migrations[i].name} was found in its place. New migrations must always have a name that comes alphabetically after the last executed migration.`);
            }
        }
    }
    async #migrateDown(db, state, step) {
        const migrationsToRollback = state.executedMigrations
            .slice()
            .reverse()
            .slice(0, step)
            .map((name) => {
            return state.migrations.find((it) => it.name === name);
        });
        const results = migrationsToRollback.map((migration) => {
            return {
                migrationName: migration.name,
                direction: 'Down',
                status: 'NotExecuted',
            };
        });
        for (let i = 0; i < results.length; ++i) {
            const migration = migrationsToRollback[i];
            try {
                if (migration.down) {
                    await migration.down(db);
                    await db
                        .withPlugin(this.#schemaPlugin)
                        .deleteFrom(this.#migrationTable)
                        .where('name', '=', migration.name)
                        .execute();
                    results[i] = {
                        migrationName: migration.name,
                        direction: 'Down',
                        status: 'Success',
                    };
                }
            }
            catch (error) {
                results[i] = {
                    migrationName: migration.name,
                    direction: 'Down',
                    status: 'Error',
                };
                throw new MigrationResultSetError({
                    error,
                    results,
                });
            }
        }
        return { results };
    }
    async #migrateUp(db, state, step) {
        const migrationsToRun = state.pendingMigrations.slice(0, step);
        const results = migrationsToRun.map((migration) => {
            return {
                migrationName: migration.name,
                direction: 'Up',
                status: 'NotExecuted',
            };
        });
        for (let i = 0; i < results.length; i++) {
            const migration = state.pendingMigrations[i];
            try {
                await migration.up(db);
                await db
                    .withPlugin(this.#schemaPlugin)
                    .insertInto(this.#migrationTable)
                    .values({
                    name: migration.name,
                    timestamp: new Date().toISOString(),
                })
                    .execute();
                results[i] = {
                    migrationName: migration.name,
                    direction: 'Up',
                    status: 'Success',
                };
            }
            catch (error) {
                results[i] = {
                    migrationName: migration.name,
                    direction: 'Up',
                    status: 'Error',
                };
                throw new MigrationResultSetError({
                    error,
                    results,
                });
            }
        }
        return { results };
    }
    async #createIfNotExists(qb) {
        if (this.#props.db.getExecutor().adapter.supportsCreateIfNotExists) {
            qb = qb.ifNotExists();
        }
        await qb.execute();
    }
}
exports.Migrator = Migrator;
class MigrationResultSetError extends Error {
    #resultSet;
    constructor(result) {
        super();
        this.#resultSet = result;
    }
    get resultSet() {
        return this.#resultSet;
    }
}
