import { assert } from "../../../shared/src/asserts.js";
import { randInt } from "../../../shared/src/rand.js";
import "../../../shared/src/valita.js";
import { Database } from "../../../zqlite/src/db.js";
import { object, number } from "@badrap/valita";
async function runSchemaMigrations(log, debugName, dbPath, setupMigration, incrementalMigrationMap) {
  const start = Date.now();
  log = log.withContext(
    "initSchema",
    randInt(0, Number.MAX_SAFE_INTEGER).toString(36)
  );
  const db = new Database(log, dbPath);
  try {
    const versionMigrations = sorted(incrementalMigrationMap);
    assert(
      versionMigrations.length,
      `Must specify a at least one version migration`
    );
    assert(
      versionMigrations[0][0] > 0,
      `Versions must be non-zero positive numbers`
    );
    const codeVersion = versionMigrations[versionMigrations.length - 1][0];
    log.info?.(
      `Checking schema for compatibility with ${debugName} at schema v${codeVersion}`
    );
    let versions = await runTransaction(log, db, (tx) => {
      const versions2 = getVersionHistory(tx);
      if (codeVersion < versions2.minSafeVersion) {
        throw new Error(
          `Cannot run ${debugName} at schema v${codeVersion} because rollback limit is v${versions2.minSafeVersion}`
        );
      }
      if (versions2.dataVersion > codeVersion) {
        log.info?.(
          `Data is at v${versions2.dataVersion}. Resetting to v${codeVersion}`
        );
        return updateVersionHistory(log, tx, versions2, codeVersion);
      }
      return versions2;
    });
    if (versions.dataVersion < codeVersion) {
      db.unsafeMode(true);
      db.pragma("locking_mode = EXCLUSIVE");
      db.pragma("foreign_keys = OFF");
      db.pragma("journal_mode = OFF");
      db.pragma("synchronous = OFF");
      const migrations = versions.dataVersion === 0 ? (
        // For the empty database v0, only run the setup migration.
        [[codeVersion, setupMigration]]
      ) : versionMigrations;
      for (const [dest, migration] of migrations) {
        if (versions.dataVersion < dest) {
          log.info?.(
            `Migrating schema from v${versions.dataVersion} to v${dest}`
          );
          void log.flush();
          versions = await runTransaction(log, db, async (tx) => {
            let versions2 = getVersionHistory(tx);
            if (versions2.dataVersion < dest) {
              versions2 = await runMigration(log, tx, versions2, dest, migration);
              assert(
                versions2.dataVersion === dest,
                () => `Migration did not reach target version: expected ${dest}, got ${versions2.dataVersion}`
              );
            }
            return versions2;
          });
        }
      }
      db.exec("ANALYZE main");
      log.info?.("ANALYZE completed");
    } else {
      db.pragma("optimize = 0x10002");
    }
    db.pragma("synchronous = NORMAL");
    db.unsafeMode(false);
    assert(
      versions.dataVersion === codeVersion,
      () => `Final dataVersion (${versions.dataVersion}) does not match codeVersion (${codeVersion})`
    );
    log.info?.(
      `Running ${debugName} at schema v${codeVersion} (${Date.now() - start} ms)`
    );
  } catch (e) {
    log.error?.("Error in ensureSchemaMigrated", e);
    throw e;
  } finally {
    db.close();
    void log.flush();
  }
}
function sorted(incrementalMigrationMap) {
  const versionMigrations = [];
  for (const [v2, m] of Object.entries(incrementalMigrationMap)) {
    versionMigrations.push([Number(v2), m]);
  }
  return versionMigrations.sort(([a], [b]) => a - b);
}
object({
  /**
   * The `schemaVersion` is highest code version that has ever been run
   * on the database, and is used to delineate the structure of the tables
   * in the database. A schemaVersion only moves forward; rolling back to
   * an earlier (safe) code version does not revert schema changes that
   * have already been applied.
   */
  schemaVersion: number(),
  /**
   * The data version is the code version of the latest server that ran.
   * Note that this may be less than the schemaVersion in the case that
   * a server is rolled back to an earlier version after a schema change.
   * In such a case, data (but not schema), may need to be re-migrated
   * when rolling forward again.
   */
  dataVersion: number(),
  /**
   * The minimum code version that is safe to run. This is used when
   * a schema migration is not backwards compatible with an older version
   * of the code.
   */
  minSafeVersion: number()
});
function getVersionHistory(db) {
  db.prepare(
    `
    CREATE TABLE IF NOT EXISTS "_zero.versionHistory" (
      dataVersion INTEGER NOT NULL,
      schemaVersion INTEGER NOT NULL,
      minSafeVersion INTEGER NOT NULL,

      lock INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)
    );
  `
  ).run();
  const result = db.prepare(
    'SELECT dataVersion, schemaVersion, minSafeVersion FROM "_zero.versionHistory"'
  ).get();
  return result ?? { dataVersion: 0, schemaVersion: 0, minSafeVersion: 0 };
}
function updateVersionHistory(log, db, prev, newVersion, minSafeVersion) {
  assert(newVersion > 0, "newVersion must be positive");
  const meta = {
    ...prev,
    dataVersion: newVersion,
    // The schemaVersion never moves backwards.
    schemaVersion: Math.max(newVersion, prev.schemaVersion),
    minSafeVersion: getMinSafeVersion(log, prev, minSafeVersion)
  };
  db.prepare(
    `
    INSERT INTO "_zero.versionHistory" (dataVersion, schemaVersion, minSafeVersion, lock)
    VALUES (@dataVersion, @schemaVersion, @minSafeVersion, 1)
    ON CONFLICT (lock) DO UPDATE
    SET dataVersion=@dataVersion,
        schemaVersion=@schemaVersion,
        minSafeVersion=@minSafeVersion
  `
  ).run(meta);
  return meta;
}
async function runMigration(log, tx, versions, destinationVersion, migration) {
  if (versions.schemaVersion < destinationVersion) {
    await migration.migrateSchema?.(log, tx);
  }
  if (versions.dataVersion < destinationVersion) {
    await migration.migrateData?.(log, tx);
  }
  return updateVersionHistory(
    log,
    tx,
    versions,
    destinationVersion,
    migration.minSafeVersion
  );
}
function getMinSafeVersion(log, current, proposedSafeVersion) {
  if (proposedSafeVersion === void 0) {
    return current.minSafeVersion;
  }
  if (current.minSafeVersion >= proposedSafeVersion) {
    log.debug?.(
      `rollback limit is already at ${current.minSafeVersion}, don't need to bump to ${proposedSafeVersion}`
    );
    return current.minSafeVersion;
  }
  log.info?.(
    `bumping rollback limit from ${current.minSafeVersion} to ${proposedSafeVersion}`
  );
  return proposedSafeVersion;
}
async function runTransaction(log, db, tx) {
  db.prepare("BEGIN EXCLUSIVE").run();
  try {
    const result = await tx(db);
    db.prepare("COMMIT").run();
    return result;
  } catch (e) {
    log.error?.("Aborted transaction due to error", e);
    db.prepare("ROLLBACK").run();
    throw e;
  }
}
export {
  getVersionHistory,
  runSchemaMigrations
};
//# sourceMappingURL=migration-lite.js.map
