|
@@ -2,128 +2,511 @@ using Invercargill.DataStructures;
|
|
|
using InvercargillSql.Dialects;
|
|
using InvercargillSql.Dialects;
|
|
|
|
|
|
|
|
namespace InvercargillSql.Migrations {
|
|
namespace InvercargillSql.Migrations {
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Represents a record of an applied migration from the database.
|
|
|
|
|
+ *
|
|
|
|
|
+ * MigrationRecord tracks the application order, namespace, serial, name,
|
|
|
|
|
+ * and timestamp of when a migration was applied.
|
|
|
|
|
+ */
|
|
|
|
|
+ public class MigrationRecord : Object {
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The order in which this migration was applied across all namespaces.
|
|
|
|
|
+ *
|
|
|
|
|
+ * This is used for time-based rollback to ensure historically
|
|
|
|
|
+ * consistent database state.
|
|
|
|
|
+ */
|
|
|
|
|
+ public int64 application_order { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The namespace this migration belongs to.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Named `migration_namespace` to avoid conflict with Vala's `namespace` keyword.
|
|
|
|
|
+ */
|
|
|
|
|
+ public string migration_namespace { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The serial number of the migration within its namespace.
|
|
|
|
|
+ */
|
|
|
|
|
+ public uint64 serial { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The human-readable name of the migration.
|
|
|
|
|
+ */
|
|
|
|
|
+ public string name { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Unix timestamp of when the migration was applied.
|
|
|
|
|
+ */
|
|
|
|
|
+ public int64 applied_at { get; set; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Manages migration execution with namespace support.
|
|
|
|
|
+ *
|
|
|
|
|
+ * MigrationRunner handles:
|
|
|
|
|
+ * - Registering migrations from multiple namespaces
|
|
|
|
|
+ * - Dependency resolution using topological sort
|
|
|
|
|
+ * - Circular dependency detection
|
|
|
|
|
+ * - Time-based rollback across namespaces
|
|
|
|
|
+ * - Applying and reverting migrations in the correct order
|
|
|
|
|
+ *
|
|
|
|
|
+ * Example usage:
|
|
|
|
|
+ * {{{
|
|
|
|
|
+ * var runner = new MigrationRunner(connection, dialect);
|
|
|
|
|
+ * runner.register_migration(new Auth_V001_CreateUsers());
|
|
|
|
|
+ * runner.register_migration(new App_V001_CreateOrders());
|
|
|
|
|
+ *
|
|
|
|
|
+ * runner.validate_dependencies(); // Check for cycles
|
|
|
|
|
+ * runner.migrate_to_latest(); // Apply all pending migrations
|
|
|
|
|
+ * }}}
|
|
|
|
|
+ */
|
|
|
public class MigrationRunner : Object {
|
|
public class MigrationRunner : Object {
|
|
|
private Connection _connection;
|
|
private Connection _connection;
|
|
|
private SqlDialect _dialect;
|
|
private SqlDialect _dialect;
|
|
|
private Vector<Migration> _migrations;
|
|
private Vector<Migration> _migrations;
|
|
|
-
|
|
|
|
|
|
|
+ private Dictionary<string, Vector<Migration>> _migrations_by_namespace;
|
|
|
|
|
+ private bool _migrations_table_ensured = false;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Creates a new MigrationRunner.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param connection the database connection to use
|
|
|
|
|
+ * @param dialect the SQL dialect for generating SQL statements
|
|
|
|
|
+ */
|
|
|
public MigrationRunner(Connection connection, SqlDialect dialect) {
|
|
public MigrationRunner(Connection connection, SqlDialect dialect) {
|
|
|
_connection = connection;
|
|
_connection = connection;
|
|
|
_dialect = dialect;
|
|
_dialect = dialect;
|
|
|
_migrations = new Vector<Migration>();
|
|
_migrations = new Vector<Migration>();
|
|
|
|
|
+ _migrations_by_namespace = new Dictionary<string, Vector<Migration>>();
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Registers a migration instance.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Migrations must be registered before calling migrate_to_latest()
|
|
|
|
|
+ * or other migration methods.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param migration the migration to register
|
|
|
|
|
+ */
|
|
|
public void register_migration(Migration migration) {
|
|
public void register_migration(Migration migration) {
|
|
|
_migrations.add(migration);
|
|
_migrations.add(migration);
|
|
|
|
|
+
|
|
|
|
|
+ // Index by namespace
|
|
|
|
|
+ var ns = migration.migration_namespace;
|
|
|
|
|
+ Vector<Migration>? existing = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ existing = _migrations_by_namespace.get(ns);
|
|
|
|
|
+ } catch (Invercargill.IndexError e) {
|
|
|
|
|
+ // Key doesn't exist yet
|
|
|
|
|
+ }
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ existing = new Vector<Migration>();
|
|
|
|
|
+ _migrations_by_namespace.set(ns, existing);
|
|
|
|
|
+ }
|
|
|
|
|
+ existing.add(migration);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Ensures the __migrations table exists with the namespace-aware schema.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @throws SqlError if table creation fails
|
|
|
|
|
+ */
|
|
|
private void ensure_migrations_table() throws SqlError {
|
|
private void ensure_migrations_table() throws SqlError {
|
|
|
|
|
+ if (_migrations_table_ensured) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
_connection.execute("
|
|
_connection.execute("
|
|
|
CREATE TABLE IF NOT EXISTS __migrations (
|
|
CREATE TABLE IF NOT EXISTS __migrations (
|
|
|
- version INTEGER PRIMARY KEY,
|
|
|
|
|
|
|
+ application_order INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
+ namespace TEXT NOT NULL,
|
|
|
|
|
+ serial INTEGER NOT NULL,
|
|
|
name TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
|
- applied_at INTEGER NOT NULL
|
|
|
|
|
|
|
+ applied_at INTEGER NOT NULL,
|
|
|
|
|
+ UNIQUE (namespace, serial)
|
|
|
)
|
|
)
|
|
|
");
|
|
");
|
|
|
|
|
+ _migrations_table_ensured = true;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- public Vector<int> get_applied_versions() throws SqlError {
|
|
|
|
|
- var versions = new Vector<int>();
|
|
|
|
|
- var results = _connection.create_command("SELECT version FROM __migrations ORDER BY version")
|
|
|
|
|
- .execute_query();
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Gets all applied migrations, optionally filtered by namespace.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param namespace optional namespace filter; if null, returns all applied migrations
|
|
|
|
|
+ * @return a vector of MigrationRecord objects ordered by application_order
|
|
|
|
|
+ * @throws SqlError if the query fails
|
|
|
|
|
+ */
|
|
|
|
|
+ public Vector<MigrationRecord> get_applied_migrations(string? namespace = null) throws SqlError {
|
|
|
|
|
+ ensure_migrations_table();
|
|
|
|
|
+
|
|
|
|
|
+ var records = new Vector<MigrationRecord>();
|
|
|
|
|
+
|
|
|
|
|
+ string sql;
|
|
|
|
|
+ Command cmd;
|
|
|
|
|
+ if (namespace != null) {
|
|
|
|
|
+ sql = "SELECT application_order, namespace, serial, name, applied_at FROM __migrations WHERE namespace = :namespace ORDER BY application_order";
|
|
|
|
|
+ cmd = _connection.create_command(sql).with_parameter("namespace", namespace);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ sql = "SELECT application_order, namespace, serial, name, applied_at FROM __migrations ORDER BY application_order";
|
|
|
|
|
+ cmd = _connection.create_command(sql);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var results = cmd.execute_query();
|
|
|
|
|
|
|
|
foreach (var row in results) {
|
|
foreach (var row in results) {
|
|
|
- var version = row.get("version")?.as_int_or_null();
|
|
|
|
|
- if (version != null) {
|
|
|
|
|
- versions.add(version);
|
|
|
|
|
|
|
+ var record = new MigrationRecord();
|
|
|
|
|
+ var app_order = row.get("application_order")?.as_int_or_null();
|
|
|
|
|
+ var ns = row.get("namespace")?.as_string_or_null();
|
|
|
|
|
+ var serial_val = row.get("serial")?.as_int_or_null();
|
|
|
|
|
+ var name = row.get("name")?.as_string_or_null();
|
|
|
|
|
+ var applied_at = row.get("applied_at")?.as_int_or_null();
|
|
|
|
|
+
|
|
|
|
|
+ if (app_order != null && ns != null && serial_val != null && name != null && applied_at != null) {
|
|
|
|
|
+ record.application_order = app_order;
|
|
|
|
|
+ record.migration_namespace = ns;
|
|
|
|
|
+ record.serial = (uint64)serial_val;
|
|
|
|
|
+ record.name = name;
|
|
|
|
|
+ record.applied_at = applied_at;
|
|
|
|
|
+ records.add(record);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- return versions;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ return records;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- public int get_current_version() throws SqlError {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Gets all pending migrations, optionally filtered by namespace.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Pending migrations are registered migrations that have not yet been
|
|
|
|
|
+ * applied to the database.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param namespace optional namespace filter; if null, returns all pending migrations
|
|
|
|
|
+ * @return a vector of Migration objects sorted by namespace then serial
|
|
|
|
|
+ * @throws SqlError if the query fails
|
|
|
|
|
+ */
|
|
|
|
|
+ public Vector<Migration> get_pending_migrations(string? namespace = null) throws SqlError {
|
|
|
ensure_migrations_table();
|
|
ensure_migrations_table();
|
|
|
- var versions = get_applied_versions();
|
|
|
|
|
- if (versions.length == 0) {
|
|
|
|
|
- return 0;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Build a set of applied migrations for quick lookup
|
|
|
|
|
+ var applied = new HashSet<string>();
|
|
|
|
|
+ var applied_records = get_applied_migrations(namespace);
|
|
|
|
|
+ foreach (var record in applied_records) {
|
|
|
|
|
+ applied.add("%s:%s".printf(record.migration_namespace, record.serial.to_string()));
|
|
|
}
|
|
}
|
|
|
- return versions.last();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public Vector<Migration> get_pending_migrations() throws SqlError {
|
|
|
|
|
- ensure_migrations_table();
|
|
|
|
|
- var applied = get_applied_versions();
|
|
|
|
|
|
|
+
|
|
|
var pending = new Vector<Migration>();
|
|
var pending = new Vector<Migration>();
|
|
|
|
|
|
|
|
foreach (var migration in _migrations) {
|
|
foreach (var migration in _migrations) {
|
|
|
- if (!applied.contains(migration.version)) {
|
|
|
|
|
|
|
+ // Filter by namespace if specified
|
|
|
|
|
+ if (namespace != null && migration.migration_namespace != namespace) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if already applied
|
|
|
|
|
+ string key = "%s:%s".printf(migration.migration_namespace, migration.serial.to_string());
|
|
|
|
|
+ if (!applied.contains(key)) {
|
|
|
pending.add(migration);
|
|
pending.add(migration);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Sort by version
|
|
|
|
|
- pending.sort((a, b) => a.version - b.version);
|
|
|
|
|
|
|
+ // Sort by namespace, then by serial
|
|
|
|
|
+ pending.sort((a, b) => {
|
|
|
|
|
+ int ns_cmp = strcmp(a.migration_namespace, b.migration_namespace);
|
|
|
|
|
+ if (ns_cmp != 0) {
|
|
|
|
|
+ return ns_cmp;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (a.serial < b.serial) {
|
|
|
|
|
+ return -1;
|
|
|
|
|
+ } else if (a.serial > b.serial) {
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
return pending;
|
|
return pending;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Applies all pending migrations across all namespaces.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Dependencies are resolved to determine execution order using
|
|
|
|
|
+ * topological sort. Circular dependencies are detected and reported.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @throws SqlError if any migration fails or if circular dependencies are detected
|
|
|
|
|
+ */
|
|
|
public void migrate_to_latest() throws SqlError {
|
|
public void migrate_to_latest() throws SqlError {
|
|
|
ensure_migrations_table();
|
|
ensure_migrations_table();
|
|
|
- var pending = get_pending_migrations();
|
|
|
|
|
|
|
+ validate_dependencies();
|
|
|
|
|
+
|
|
|
|
|
+ var sorted = topological_sort();
|
|
|
|
|
|
|
|
- foreach (var migration in pending) {
|
|
|
|
|
- apply_migration(migration);
|
|
|
|
|
|
|
+ // Get applied migrations
|
|
|
|
|
+ var applied = new HashSet<string>();
|
|
|
|
|
+ foreach (var record in get_applied_migrations()) {
|
|
|
|
|
+ applied.add("%s:%s".printf(record.migration_namespace, record.serial.to_string()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Apply migrations in topological order
|
|
|
|
|
+ foreach (var migration in sorted) {
|
|
|
|
|
+ string key = "%s:%s".printf(migration.migration_namespace, migration.serial.to_string());
|
|
|
|
|
+ if (!applied.contains(key)) {
|
|
|
|
|
+ apply_migration(migration);
|
|
|
|
|
+ applied.add(key);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- public void migrate_to(int target_version) throws SqlError {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Migrates a specific namespace to its latest version.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Dependencies from other namespaces are applied first if needed.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param namespace the namespace to migrate
|
|
|
|
|
+ * @throws SqlError if any migration fails
|
|
|
|
|
+ */
|
|
|
|
|
+ public void migrate_to_latest_for_namespace(string namespace) throws SqlError {
|
|
|
ensure_migrations_table();
|
|
ensure_migrations_table();
|
|
|
- var current = get_current_version();
|
|
|
|
|
|
|
+ validate_dependencies();
|
|
|
|
|
|
|
|
- if (target_version > current) {
|
|
|
|
|
- // Apply migrations up to target
|
|
|
|
|
- foreach (var migration in _migrations) {
|
|
|
|
|
- if (migration.version > current && migration.version <= target_version) {
|
|
|
|
|
- apply_migration(migration);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ var sorted = topological_sort();
|
|
|
|
|
+
|
|
|
|
|
+ // Get applied migrations
|
|
|
|
|
+ var applied = new HashSet<string>();
|
|
|
|
|
+ foreach (var record in get_applied_migrations()) {
|
|
|
|
|
+ applied.add("%s:%s".printf(record.migration_namespace, record.serial.to_string()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Determine which migrations are needed for this namespace
|
|
|
|
|
+ // and their transitive dependencies
|
|
|
|
|
+ var needed = compute_transitive_dependencies(namespace);
|
|
|
|
|
+
|
|
|
|
|
+ // Apply migrations in topological order
|
|
|
|
|
+ foreach (var migration in sorted) {
|
|
|
|
|
+ string key = "%s:%s".printf(migration.migration_namespace, migration.serial.to_string());
|
|
|
|
|
+ if (!applied.contains(key) && needed.contains(key)) {
|
|
|
|
|
+ apply_migration(migration);
|
|
|
|
|
+ applied.add(key);
|
|
|
}
|
|
}
|
|
|
- } else if (target_version < current) {
|
|
|
|
|
- // Rollback migrations down to target
|
|
|
|
|
- var applied = get_applied_versions();
|
|
|
|
|
- for (int i = (int) applied.length - 1; i >= 0 && applied[i] > target_version; i--) {
|
|
|
|
|
- var migration = find_migration(applied[i]);
|
|
|
|
|
- if (migration != null) {
|
|
|
|
|
- revert_migration(migration);
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Migrates to a specific migration using time-based rollback if needed.
|
|
|
|
|
+ *
|
|
|
|
|
+ * If the target serial is greater than the current serial for the namespace,
|
|
|
|
|
+ * pending migrations are applied. If the target is less, migrations are
|
|
|
|
|
+ * rolled back using time-based rollback.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param namespace the namespace containing the target migration
|
|
|
|
|
+ * @param serial the target serial number
|
|
|
|
|
+ * @throws SqlError if any operation fails
|
|
|
|
|
+ */
|
|
|
|
|
+ public void migrate_to(string namespace, uint64 serial) throws SqlError {
|
|
|
|
|
+ ensure_migrations_table();
|
|
|
|
|
+
|
|
|
|
|
+ var current = get_current_serial(namespace);
|
|
|
|
|
+
|
|
|
|
|
+ if (serial > current) {
|
|
|
|
|
+ // Need to apply migrations up to this serial
|
|
|
|
|
+ validate_dependencies();
|
|
|
|
|
+ var sorted = topological_sort();
|
|
|
|
|
+
|
|
|
|
|
+ var applied = new HashSet<string>();
|
|
|
|
|
+ foreach (var record in get_applied_migrations()) {
|
|
|
|
|
+ applied.add("%s:%s".printf(record.migration_namespace, record.serial.to_string()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Compute which migrations are needed
|
|
|
|
|
+ var needed = compute_transitive_dependencies_for_target(namespace, serial);
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var migration in sorted) {
|
|
|
|
|
+ string key = "%s:%s".printf(migration.migration_namespace, migration.serial.to_string());
|
|
|
|
|
+ if (!applied.contains(key) && needed.contains(key)) {
|
|
|
|
|
+ apply_migration(migration);
|
|
|
|
|
+ applied.add(key);
|
|
|
|
|
+
|
|
|
|
|
+ // Stop if we've applied the target migration
|
|
|
|
|
+ if (migration.migration_namespace == namespace && migration.serial == serial) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ } else if (serial < current) {
|
|
|
|
|
+ // Need to roll back using time-based rollback
|
|
|
|
|
+ rollback_to(namespace, serial);
|
|
|
}
|
|
}
|
|
|
|
|
+ // If serial == current, nothing to do
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Rolls back the last N migrations across all namespaces.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Rollback is performed in reverse application order regardless
|
|
|
|
|
+ * of namespace boundaries.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param steps the number of migrations to roll back (default: 1)
|
|
|
|
|
+ * @throws SqlError if any rollback fails
|
|
|
|
|
+ */
|
|
|
public void rollback(int steps = 1) throws SqlError {
|
|
public void rollback(int steps = 1) throws SqlError {
|
|
|
ensure_migrations_table();
|
|
ensure_migrations_table();
|
|
|
- var applied = get_applied_versions();
|
|
|
|
|
|
|
+
|
|
|
|
|
+ var applied = get_applied_migrations();
|
|
|
|
|
|
|
|
int count = 0;
|
|
int count = 0;
|
|
|
for (int i = (int) applied.length - 1; i >= 0 && count < steps; i--, count++) {
|
|
for (int i = (int) applied.length - 1; i >= 0 && count < steps; i--, count++) {
|
|
|
- var migration = find_migration(applied[i]);
|
|
|
|
|
|
|
+ var record = applied[i];
|
|
|
|
|
+ var migration = find_migration(record.migration_namespace, record.serial);
|
|
|
if (migration != null) {
|
|
if (migration != null) {
|
|
|
revert_migration(migration);
|
|
revert_migration(migration);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Rolls back to a specific migration using time-based rollback.
|
|
|
|
|
+ *
|
|
|
|
|
+ * All migrations applied after the target are rolled back in reverse
|
|
|
|
|
+ * application order, regardless of namespace. This ensures the database
|
|
|
|
|
+ * is in a historically consistent state.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param namespace the namespace containing the target migration
|
|
|
|
|
+ * @param serial the target serial number
|
|
|
|
|
+ * @throws SqlError if any rollback fails
|
|
|
|
|
+ */
|
|
|
|
|
+ public void rollback_to(string namespace, uint64 serial) throws SqlError {
|
|
|
|
|
+ ensure_migrations_table();
|
|
|
|
|
+
|
|
|
|
|
+ // Find the target migration's application order
|
|
|
|
|
+ var applied = get_applied_migrations();
|
|
|
|
|
+ int64? target_order = null;
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var record in applied) {
|
|
|
|
|
+ if (record.migration_namespace == namespace && record.serial == serial) {
|
|
|
|
|
+ target_order = record.application_order;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (target_order == null) {
|
|
|
|
|
+ throw new SqlError.GENERAL_ERROR(
|
|
|
|
|
+ "Cannot roll back to %s:%s - migration not found in applied migrations".printf(namespace, serial.to_string())
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Roll back all migrations with higher application_order in reverse order
|
|
|
|
|
+ for (int i = (int) applied.length - 1; i >= 0; i--) {
|
|
|
|
|
+ var record = applied[i];
|
|
|
|
|
+ if (record.application_order > target_order) {
|
|
|
|
|
+ var migration = find_migration(record.migration_namespace, record.serial);
|
|
|
|
|
+ if (migration != null) {
|
|
|
|
|
+ revert_migration(migration);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Rolls back all migrations across all namespaces.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @throws SqlError if any rollback fails
|
|
|
|
|
+ */
|
|
|
public void rollback_all() throws SqlError {
|
|
public void rollback_all() throws SqlError {
|
|
|
ensure_migrations_table();
|
|
ensure_migrations_table();
|
|
|
- var applied = get_applied_versions();
|
|
|
|
|
|
|
+
|
|
|
|
|
+ var applied = get_applied_migrations();
|
|
|
|
|
|
|
|
for (int i = (int) applied.length - 1; i >= 0; i--) {
|
|
for (int i = (int) applied.length - 1; i >= 0; i--) {
|
|
|
- var migration = find_migration(applied[i]);
|
|
|
|
|
|
|
+ var record = applied[i];
|
|
|
|
|
+ var migration = find_migration(record.migration_namespace, record.serial);
|
|
|
if (migration != null) {
|
|
if (migration != null) {
|
|
|
revert_migration(migration);
|
|
revert_migration(migration);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Gets the current highest serial for a namespace.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param namespace the namespace to query
|
|
|
|
|
+ * @return the highest applied serial, or 0 if no migrations have been applied
|
|
|
|
|
+ * @throws SqlError if the query fails
|
|
|
|
|
+ */
|
|
|
|
|
+ public uint64 get_current_serial(string namespace) throws SqlError {
|
|
|
|
|
+ ensure_migrations_table();
|
|
|
|
|
+
|
|
|
|
|
+ var applied = get_applied_migrations(namespace);
|
|
|
|
|
+
|
|
|
|
|
+ uint64 max_serial = 0;
|
|
|
|
|
+ foreach (var record in applied) {
|
|
|
|
|
+ if (record.serial > max_serial) {
|
|
|
|
|
+ max_serial = record.serial;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return max_serial;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Validates the dependency graph for cycles and unsatisfied dependencies.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @throws SqlError if a circular dependency is detected or dependencies are unsatisfied
|
|
|
|
|
+ */
|
|
|
|
|
+ public void validate_dependencies() throws SqlError {
|
|
|
|
|
+ // First check for unsatisfied dependencies
|
|
|
|
|
+ validate_all_dependencies_satisfied();
|
|
|
|
|
+
|
|
|
|
|
+ // Then check for cycles in the dependency graph
|
|
|
|
|
+ detect_cycles();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Validates that all dependencies point to existing migrations.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @throws SqlError if any dependency is unsatisfied
|
|
|
|
|
+ */
|
|
|
|
|
+ private void validate_all_dependencies_satisfied() throws SqlError {
|
|
|
|
|
+ foreach (var migration in _migrations) {
|
|
|
|
|
+ foreach (var dep_str in migration.dependencies) {
|
|
|
|
|
+ var dep = Dependency.parse(dep_str);
|
|
|
|
|
+
|
|
|
|
|
+ if (dep.serial != null) {
|
|
|
|
|
+ // Specific serial dependency
|
|
|
|
|
+ var dep_migration = find_migration(dep.namespace, dep.serial);
|
|
|
|
|
+ if (dep_migration == null) {
|
|
|
|
|
+ throw new SqlError.GENERAL_ERROR(
|
|
|
|
|
+ "Unsatisfied dependency: %s:%s requires %s but no migration with serial %s is registered in namespace '%s'".printf(
|
|
|
|
|
+ migration.migration_namespace,
|
|
|
|
|
+ migration.serial.to_string(),
|
|
|
|
|
+ dep_str,
|
|
|
|
|
+ dep.serial.to_string(),
|
|
|
|
|
+ dep.namespace
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Namespace-only dependency
|
|
|
|
|
+ if (!has_namespace(dep.namespace)) {
|
|
|
|
|
+ throw new SqlError.GENERAL_ERROR(
|
|
|
|
|
+ "Unsatisfied dependency: %s:%s requires namespace '%s' but no migrations are registered in that namespace".printf(
|
|
|
|
|
+ migration.migration_namespace,
|
|
|
|
|
+ migration.serial.to_string(),
|
|
|
|
|
+ dep.namespace
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Applies a single migration within a transaction.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param migration the migration to apply
|
|
|
|
|
+ * @throws SqlError if the migration fails
|
|
|
|
|
+ */
|
|
|
private void apply_migration(Migration migration) throws SqlError {
|
|
private void apply_migration(Migration migration) throws SqlError {
|
|
|
var builder = new MigrationBuilder(_dialect);
|
|
var builder = new MigrationBuilder(_dialect);
|
|
|
migration.up(builder);
|
|
migration.up(builder);
|
|
@@ -136,9 +519,10 @@ namespace InvercargillSql.Migrations {
|
|
|
var now = new DateTime.now_utc();
|
|
var now = new DateTime.now_utc();
|
|
|
int64 timestamp = now.to_unix();
|
|
int64 timestamp = now.to_unix();
|
|
|
_connection.create_command(
|
|
_connection.create_command(
|
|
|
- "INSERT INTO __migrations (version, name, applied_at) VALUES (:version, :name, :applied_at)"
|
|
|
|
|
|
|
+ "INSERT INTO __migrations (namespace, serial, name, applied_at) VALUES (:namespace, :serial, :name, :applied_at)"
|
|
|
)
|
|
)
|
|
|
- .with_parameter("version", migration.version)
|
|
|
|
|
|
|
+ .with_parameter("namespace", migration.migration_namespace)
|
|
|
|
|
+ .with_parameter<uint64?>("serial", migration.serial)
|
|
|
.with_parameter("name", migration.name)
|
|
.with_parameter("name", migration.name)
|
|
|
.with_parameter<int64?>("applied_at", timestamp)
|
|
.with_parameter<int64?>("applied_at", timestamp)
|
|
|
.execute_non_query();
|
|
.execute_non_query();
|
|
@@ -149,7 +533,13 @@ namespace InvercargillSql.Migrations {
|
|
|
throw e;
|
|
throw e;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Reverts a single migration within a transaction.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param migration the migration to revert
|
|
|
|
|
+ * @throws SqlError if the rollback fails
|
|
|
|
|
+ */
|
|
|
private void revert_migration(Migration migration) throws SqlError {
|
|
private void revert_migration(Migration migration) throws SqlError {
|
|
|
var builder = new MigrationBuilder(_dialect);
|
|
var builder = new MigrationBuilder(_dialect);
|
|
|
migration.down(builder);
|
|
migration.down(builder);
|
|
@@ -159,9 +549,12 @@ namespace InvercargillSql.Migrations {
|
|
|
builder.execute(_connection);
|
|
builder.execute(_connection);
|
|
|
|
|
|
|
|
// Remove migration record
|
|
// Remove migration record
|
|
|
- _connection.create_command("DELETE FROM __migrations WHERE version = :version")
|
|
|
|
|
- .with_parameter("version", migration.version)
|
|
|
|
|
- .execute_non_query();
|
|
|
|
|
|
|
+ _connection.create_command(
|
|
|
|
|
+ "DELETE FROM __migrations WHERE namespace = :namespace AND serial = :serial"
|
|
|
|
|
+ )
|
|
|
|
|
+ .with_parameter("namespace", migration.migration_namespace)
|
|
|
|
|
+ .with_parameter<uint64?>("serial", migration.serial)
|
|
|
|
|
+ .execute_non_query();
|
|
|
|
|
|
|
|
transaction.commit();
|
|
transaction.commit();
|
|
|
} catch (SqlError e) {
|
|
} catch (SqlError e) {
|
|
@@ -169,14 +562,581 @@ namespace InvercargillSql.Migrations {
|
|
|
throw e;
|
|
throw e;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Finds a registered migration by namespace and serial.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param namespace the namespace to search
|
|
|
|
|
+ * @param serial the serial number to find
|
|
|
|
|
+ * @return the migration, or null if not found
|
|
|
|
|
+ */
|
|
|
|
|
+ private Migration? find_migration(string namespace, uint64 serial) {
|
|
|
|
|
+ Vector<Migration>? ns_list = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ ns_list = _migrations_by_namespace.get(namespace);
|
|
|
|
|
+ } catch (Invercargill.IndexError e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ns_list == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var migration in ns_list) {
|
|
|
|
|
+ if (migration.serial == serial) {
|
|
|
|
|
+ return migration;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Checks if a namespace has any migrations registered.
|
|
|
|
|
+ */
|
|
|
|
|
+ private bool has_namespace(string namespace) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return _migrations_by_namespace.get(namespace) != null;
|
|
|
|
|
+ } catch (Invercargill.IndexError e) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Performs topological sort on migrations using Kahn's algorithm.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return a vector of migrations in dependency order
|
|
|
|
|
+ * @throws SqlError if a circular dependency is detected
|
|
|
|
|
+ */
|
|
|
|
|
+ private Vector<Migration> topological_sort() throws SqlError {
|
|
|
|
|
+ // Build adjacency list and in-degree count
|
|
|
|
|
+ // Use HashSet for edges to prevent duplicates
|
|
|
|
|
+ var in_degree = new Dictionary<string, int>();
|
|
|
|
|
+ var edges = new Dictionary<string, HashSet<string>>();
|
|
|
|
|
+ var migration_map = new Dictionary<string, Migration>();
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize all nodes
|
|
|
|
|
+ foreach (var migration in _migrations) {
|
|
|
|
|
+ string key = "%s:%s".printf(migration.migration_namespace, migration.serial.to_string());
|
|
|
|
|
+ in_degree.set(key, 0);
|
|
|
|
|
+ edges.set(key, new HashSet<string>());
|
|
|
|
|
+ migration_map.set(key, migration);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build edges based on dependencies
|
|
|
|
|
+ foreach (var migration in _migrations) {
|
|
|
|
|
+ string key = "%s:%s".printf(migration.migration_namespace, migration.serial.to_string());
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var dep_str in migration.dependencies) {
|
|
|
|
|
+ var dep = Dependency.parse(dep_str);
|
|
|
|
|
+
|
|
|
|
|
+ if (dep.serial != null) {
|
|
|
|
|
+ // Specific dependency: "namespace:serial"
|
|
|
|
|
+ string dep_key = "%s:%s".printf(dep.namespace, dep.serial.to_string());
|
|
|
|
|
+
|
|
|
|
|
+ Migration? dep_migration = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ dep_migration = migration_map.get(dep_key);
|
|
|
|
|
+ } catch (Invercargill.IndexError e) {
|
|
|
|
|
+ // Key doesn't exist
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (dep_migration == null) {
|
|
|
|
|
+ throw new SqlError.GENERAL_ERROR(
|
|
|
|
|
+ "Unsatisfied dependency: %s:%s requires %s but no migration with serial %s is registered in namespace '%s'".printf(
|
|
|
|
|
+ migration.migration_namespace,
|
|
|
|
|
+ migration.serial.to_string(),
|
|
|
|
|
+ dep_str,
|
|
|
|
|
+ dep.serial.to_string(),
|
|
|
|
|
+ dep.namespace
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Edge: dep_key -> key (dep must come before this migration)
|
|
|
|
|
+ // All registered migrations should have an entry in edges, so edge_list should never be null
|
|
|
|
|
+ HashSet<string>? edge_list = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ edge_list = edges.get(dep_key);
|
|
|
|
|
+ } catch (Invercargill.IndexError e) {
|
|
|
|
|
+ // This should never happen for registered migrations
|
|
|
|
|
+ // If it does, the dependency points to an unregistered migration
|
|
|
|
|
+ }
|
|
|
|
|
+ if (edge_list != null) {
|
|
|
|
|
+ // Only increment in_degree if this is a new edge
|
|
|
|
|
+ if (!edge_list.contains(key)) {
|
|
|
|
|
+ edge_list.add(key);
|
|
|
|
|
+ in_degree.set(key, in_degree.get(key) + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // If edge_list is null, the dependency migration is not registered
|
|
|
|
|
+ // This should have been caught by the dep_migration == null check above
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Namespace-only dependency: "namespace"
|
|
|
|
|
+ // This migration depends on any migration in that namespace
|
|
|
|
|
+ // We add an edge from the HIGHEST serial migration in that namespace
|
|
|
|
|
+ // This ensures all migrations in that namespace run before this one
|
|
|
|
|
+ if (!has_namespace(dep.namespace)) {
|
|
|
|
|
+ throw new SqlError.GENERAL_ERROR(
|
|
|
|
|
+ "Unsatisfied dependency: %s:%s requires namespace '%s' but no migrations are registered in that namespace".printf(
|
|
|
|
|
+ migration.migration_namespace,
|
|
|
|
|
+ migration.serial.to_string(),
|
|
|
|
|
+ dep.namespace
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Find the highest serial migration in the dependency namespace
|
|
|
|
|
+ Vector<Migration>? dep_ns_list = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ dep_ns_list = _migrations_by_namespace.get(dep.namespace);
|
|
|
|
|
+ } catch (Invercargill.IndexError e) {
|
|
|
|
|
+ // Key doesn't exist
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dep_ns_list != null && dep_ns_list.length > 0) {
|
|
|
|
|
+ // Find the migration with the highest serial
|
|
|
|
|
+ uint64 max_serial = 0;
|
|
|
|
|
+ Migration? highest = null;
|
|
|
|
|
+ foreach (var ns_migration in dep_ns_list) {
|
|
|
|
|
+ if (ns_migration.serial > max_serial) {
|
|
|
|
|
+ max_serial = ns_migration.serial;
|
|
|
|
|
+ highest = ns_migration;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (highest != null) {
|
|
|
|
|
+ string highest_key = "%s:%s".printf(highest.migration_namespace, highest.serial.to_string());
|
|
|
|
|
+ HashSet<string>? ns_edge_list = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ ns_edge_list = edges.get(highest_key);
|
|
|
|
|
+ } catch (Invercargill.IndexError e) {
|
|
|
|
|
+ // Key doesn't exist
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ns_edge_list != null && !ns_edge_list.contains(key)) {
|
|
|
|
|
+ ns_edge_list.add(key);
|
|
|
|
|
+ in_degree.set(key, in_degree.get(key) + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Also add edges for within-namespace ordering (lower serials before higher)
|
|
|
|
|
+ var namespaces = new Dictionary<string, Vector<Migration>>();
|
|
|
|
|
+ foreach (var migration in _migrations) {
|
|
|
|
|
+ Vector<Migration>? ns_list = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ ns_list = namespaces.get(migration.migration_namespace);
|
|
|
|
|
+ } catch (Invercargill.IndexError e) {
|
|
|
|
|
+ // Key doesn't exist yet
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ns_list == null) {
|
|
|
|
|
+ ns_list = new Vector<Migration>();
|
|
|
|
|
+ namespaces.set(migration.migration_namespace, ns_list);
|
|
|
|
|
+ }
|
|
|
|
|
+ ns_list.add(migration);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var entry in namespaces) {
|
|
|
|
|
+ var ns_migrations = entry.value;
|
|
|
|
|
+
|
|
|
|
|
+ // Create a sorted copy manually to ensure correct order
|
|
|
|
|
+ var sorted_migrations = new Vector<Migration>();
|
|
|
|
|
+ foreach (var m in ns_migrations) {
|
|
|
|
|
+ sorted_migrations.add(m);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Sort by serial (ascending - lower serials first) using simple bubble sort
|
|
|
|
|
+ for (int i = 0; i < (int)sorted_migrations.length - 1; i++) {
|
|
|
|
|
+ for (int j = 0; j < (int)sorted_migrations.length - i - 1; j++) {
|
|
|
|
|
+ if (sorted_migrations[j].serial > sorted_migrations[j + 1].serial) {
|
|
|
|
|
+ var tmp = sorted_migrations[j];
|
|
|
|
|
+ sorted_migrations[j] = sorted_migrations[j + 1];
|
|
|
|
|
+ sorted_migrations[j + 1] = tmp;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Add edges between consecutive migrations in the same namespace
|
|
|
|
|
+ // This ensures lower serials run before higher serials within a namespace
|
|
|
|
|
+ for (int i = 0; i < (int)sorted_migrations.length - 1; i++) {
|
|
|
|
|
+ var prev = sorted_migrations[i];
|
|
|
|
|
+ var next = sorted_migrations[i + 1];
|
|
|
|
|
+
|
|
|
|
|
+ string prev_key = "%s:%s".printf(prev.migration_namespace, prev.serial.to_string());
|
|
|
|
|
+ string next_key = "%s:%s".printf(next.migration_namespace, next.serial.to_string());
|
|
|
|
|
+
|
|
|
|
|
+ var edge_list = edges.get(prev_key);
|
|
|
|
|
+ if (edge_list != null) {
|
|
|
|
|
+ // Only increment in_degree if this is a new edge
|
|
|
|
|
+ // Note: explicit dependencies may have already added this edge
|
|
|
|
|
+ if (!edge_list.contains(next_key)) {
|
|
|
|
|
+ edge_list.add(next_key);
|
|
|
|
|
+ in_degree.set(next_key, in_degree.get(next_key) + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Kahn's algorithm
|
|
|
|
|
+ var queue = new Vector<string>();
|
|
|
|
|
+ foreach (var entry in in_degree) {
|
|
|
|
|
+ if (entry.value == 0) {
|
|
|
|
|
+ queue.add(entry.key);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var result = new Vector<Migration>();
|
|
|
|
|
+
|
|
|
|
|
+ while (queue.length > 0) {
|
|
|
|
|
+ // Find the node with smallest serial in the earliest namespace for determinism
|
|
|
|
|
+ queue.sort((a, b) => {
|
|
|
|
|
+ var m_a = migration_map.get(a);
|
|
|
|
|
+ var m_b = migration_map.get(b);
|
|
|
|
|
+ if (m_a == null || m_b == null) return 0;
|
|
|
|
|
+ int ns_cmp = strcmp(m_a.migration_namespace, m_b.migration_namespace);
|
|
|
|
|
+ if (ns_cmp != 0) return ns_cmp;
|
|
|
|
|
+ if (m_a.serial < m_b.serial) return -1;
|
|
|
|
|
+ if (m_a.serial > m_b.serial) return 1;
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ var current = queue.first();
|
|
|
|
|
+ queue.remove_at(0);
|
|
|
|
|
+ var current_migration = migration_map.get(current);
|
|
|
|
|
+ if (current_migration != null) {
|
|
|
|
|
+ result.add(current_migration);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var current_edges = edges.get(current);
|
|
|
|
|
+ if (current_edges != null) {
|
|
|
|
|
+ foreach (var neighbor in current_edges) {
|
|
|
|
|
+ in_degree.set(neighbor, in_degree.get(neighbor) - 1);
|
|
|
|
|
+ if (in_degree.get(neighbor) == 0) {
|
|
|
|
|
+ queue.add(neighbor);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check for cycle
|
|
|
|
|
+ if (result.length != _migrations.length) {
|
|
|
|
|
+ // Debug: show which nodes have non-zero in_degree
|
|
|
|
|
+ var stuck_nodes = new Vector<string>();
|
|
|
|
|
+ foreach (var entry in in_degree) {
|
|
|
|
|
+ if (entry.value > 0) {
|
|
|
|
|
+ stuck_nodes.add("%s (in_degree=%d)".printf(entry.key, entry.value));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // Find the cycle for error reporting
|
|
|
|
|
+ string cycle = find_cycle();
|
|
|
|
|
+ throw new SqlError.GENERAL_ERROR("Circular dependency detected: %s. Stuck nodes: %s".printf(
|
|
|
|
|
+ cycle,
|
|
|
|
|
+ string.joinv(", ", stuck_nodes.to_array())
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Detects cycles in the dependency graph.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @throws SqlError if a cycle is detected, with the cycle path in the message
|
|
|
|
|
+ */
|
|
|
|
|
+ private void detect_cycles() throws SqlError {
|
|
|
|
|
+ // Use DFS to detect cycles
|
|
|
|
|
+ var visited = new HashSet<string>();
|
|
|
|
|
+ var recursion_stack = new HashSet<string>();
|
|
|
|
|
+ var path = new Vector<string>();
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var migration in _migrations) {
|
|
|
|
|
+ string key = "%s:%s".printf(migration.migration_namespace, migration.serial.to_string());
|
|
|
|
|
+ if (!visited.contains(key)) {
|
|
|
|
|
+ if (dfs_detect_cycle(key, visited, recursion_stack, path)) {
|
|
|
|
|
+ // Build cycle string from path
|
|
|
|
|
+ var cycle_start = -1;
|
|
|
|
|
+ for (int i = 0; i < (int)path.length; i++) {
|
|
|
|
|
+ if (path[i] == path.last()) {
|
|
|
|
|
+ cycle_start = i;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var cycle_parts = new Vector<string>();
|
|
|
|
|
+ if (cycle_start >= 0) {
|
|
|
|
|
+ for (int i = cycle_start; i < (int)path.length; i++) {
|
|
|
|
|
+ cycle_parts.add(path[i]);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cycle_parts.add_all(path);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ throw new SqlError.GENERAL_ERROR(
|
|
|
|
|
+ "Circular dependency detected: %s".printf(string.joinv(" → ", cycle_parts.to_array()))
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * DFS helper for cycle detection.
|
|
|
|
|
+ */
|
|
|
|
|
+ private bool dfs_detect_cycle(
|
|
|
|
|
+ string node,
|
|
|
|
|
+ HashSet<string> visited,
|
|
|
|
|
+ HashSet<string> recursion_stack,
|
|
|
|
|
+ Vector<string> path
|
|
|
|
|
+ ) throws SqlError {
|
|
|
|
|
+ visited.add(node);
|
|
|
|
|
+ recursion_stack.add(node);
|
|
|
|
|
+ path.add(node);
|
|
|
|
|
+
|
|
|
|
|
+ // Get neighbors (dependencies)
|
|
|
|
|
+ var neighbors = get_dependency_neighbors(node);
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var neighbor in neighbors) {
|
|
|
|
|
+ if (!visited.contains(neighbor)) {
|
|
|
|
|
+ if (dfs_detect_cycle(neighbor, visited, recursion_stack, path)) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (recursion_stack.contains(neighbor)) {
|
|
|
|
|
+ path.add(neighbor);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ recursion_stack.remove(node);
|
|
|
|
|
+ path.remove_at(path.length - 1);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
|
|
|
- private Migration? find_migration(int version) {
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Gets the dependency neighbors for a migration key.
|
|
|
|
|
+ */
|
|
|
|
|
+ private Vector<string> get_dependency_neighbors(string key) throws SqlError {
|
|
|
|
|
+ var neighbors = new Vector<string>();
|
|
|
|
|
+
|
|
|
|
|
+ var parts = key.split(":");
|
|
|
|
|
+ if (parts.length != 2) {
|
|
|
|
|
+ return neighbors;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ string ns = parts[0];
|
|
|
|
|
+ uint64 serial = uint64.parse(parts[1]);
|
|
|
|
|
+
|
|
|
|
|
+ var migration = find_migration(ns, serial);
|
|
|
|
|
+ if (migration == null) {
|
|
|
|
|
+ return neighbors;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Add explicit dependencies
|
|
|
|
|
+ foreach (var dep_str in migration.dependencies) {
|
|
|
|
|
+ var dep = Dependency.parse(dep_str);
|
|
|
|
|
+ if (dep.serial != null) {
|
|
|
|
|
+ neighbors.add("%s:%s".printf(dep.namespace, dep.serial.to_string()));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Add all migrations in the namespace as dependencies
|
|
|
|
|
+ Vector<Migration>? dep_ns_list = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ dep_ns_list = _migrations_by_namespace.get(dep.namespace);
|
|
|
|
|
+ } catch (Invercargill.IndexError e) {
|
|
|
|
|
+ // Namespace doesn't exist
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dep_ns_list != null) {
|
|
|
|
|
+ foreach (var ns_migration in dep_ns_list) {
|
|
|
|
|
+ neighbors.add("%s:%s".printf(ns_migration.migration_namespace, ns_migration.serial.to_string()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return neighbors;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Finds a cycle in the dependency graph and returns it as a string.
|
|
|
|
|
+ */
|
|
|
|
|
+ private string find_cycle() throws SqlError {
|
|
|
|
|
+ var visited = new HashSet<string>();
|
|
|
|
|
+ var path = new Vector<string>();
|
|
|
|
|
+
|
|
|
foreach (var migration in _migrations) {
|
|
foreach (var migration in _migrations) {
|
|
|
- if (migration.version == version) {
|
|
|
|
|
- return migration;
|
|
|
|
|
|
|
+ string key = "%s:%s".printf(migration.migration_namespace, migration.serial.to_string());
|
|
|
|
|
+ if (!visited.contains(key)) {
|
|
|
|
|
+ var cycle = find_cycle_dfs(key, visited, path);
|
|
|
|
|
+ if (cycle != null) {
|
|
|
|
|
+ return cycle;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return "unknown cycle";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * DFS helper for finding a cycle.
|
|
|
|
|
+ */
|
|
|
|
|
+ private string? find_cycle_dfs(
|
|
|
|
|
+ string node,
|
|
|
|
|
+ HashSet<string> visited,
|
|
|
|
|
+ Vector<string> path
|
|
|
|
|
+ ) throws SqlError {
|
|
|
|
|
+ visited.add(node);
|
|
|
|
|
+ path.add(node);
|
|
|
|
|
+
|
|
|
|
|
+ var neighbors = get_dependency_neighbors(node);
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var neighbor in neighbors) {
|
|
|
|
|
+ // Check if neighbor is in current path (cycle detected)
|
|
|
|
|
+ for (int i = 0; i < (int)path.length; i++) {
|
|
|
|
|
+ if (path[i] == neighbor) {
|
|
|
|
|
+ // Build cycle string
|
|
|
|
|
+ var cycle_parts = new Vector<string>();
|
|
|
|
|
+ for (int j = i; j < (int)path.length; j++) {
|
|
|
|
|
+ cycle_parts.add(path[j]);
|
|
|
|
|
+ }
|
|
|
|
|
+ cycle_parts.add(neighbor);
|
|
|
|
|
+ return string.joinv(" → ", cycle_parts.to_array());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!visited.contains(neighbor)) {
|
|
|
|
|
+ var cycle = find_cycle_dfs(neighbor, visited, path);
|
|
|
|
|
+ if (cycle != null) {
|
|
|
|
|
+ return cycle;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ path.remove_at(path.length - 1);
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Computes all migrations that are transitive dependencies for a namespace.
|
|
|
|
|
+ */
|
|
|
|
|
+ private HashSet<string> compute_transitive_dependencies(string target_namespace) throws SqlError {
|
|
|
|
|
+ var needed = new HashSet<string>();
|
|
|
|
|
+ var to_process = new Vector<string>();
|
|
|
|
|
+
|
|
|
|
|
+ // Start with all migrations in the target namespace
|
|
|
|
|
+ var ns_migrations = _migrations_by_namespace.get(target_namespace);
|
|
|
|
|
+ if (ns_migrations != null) {
|
|
|
|
|
+ foreach (var migration in ns_migrations) {
|
|
|
|
|
+ string key = "%s:%s".printf(migration.migration_namespace, migration.serial.to_string());
|
|
|
|
|
+ needed.add(key);
|
|
|
|
|
+ to_process.add(key);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Process dependencies transitively
|
|
|
|
|
+ while (to_process.length > 0) {
|
|
|
|
|
+ var current = to_process.first();
|
|
|
|
|
+ to_process.remove_at(0);
|
|
|
|
|
+
|
|
|
|
|
+ var parts = current.split(":");
|
|
|
|
|
+ if (parts.length != 2) continue;
|
|
|
|
|
+
|
|
|
|
|
+ var migration = find_migration(parts[0], uint64.parse(parts[1]));
|
|
|
|
|
+ if (migration == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var dep_str in migration.dependencies) {
|
|
|
|
|
+ var dep = Dependency.parse(dep_str);
|
|
|
|
|
+
|
|
|
|
|
+ if (dep.serial != null) {
|
|
|
|
|
+ string dep_key = "%s:%s".printf(dep.namespace, dep.serial.to_string());
|
|
|
|
|
+ if (!needed.contains(dep_key)) {
|
|
|
|
|
+ needed.add(dep_key);
|
|
|
|
|
+ to_process.add(dep_key);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Add all migrations in the namespace
|
|
|
|
|
+ var dep_ns_migrations = _migrations_by_namespace.get(dep.namespace);
|
|
|
|
|
+ if (dep_ns_migrations != null) {
|
|
|
|
|
+ foreach (var ns_migration in dep_ns_migrations) {
|
|
|
|
|
+ string ns_key = "%s:%s".printf(ns_migration.migration_namespace, ns_migration.serial.to_string());
|
|
|
|
|
+ if (!needed.contains(ns_key)) {
|
|
|
|
|
+ needed.add(ns_key);
|
|
|
|
|
+ to_process.add(ns_key);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return needed;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Computes all migrations needed to reach a target serial in a namespace.
|
|
|
|
|
+ */
|
|
|
|
|
+ private HashSet<string> compute_transitive_dependencies_for_target(
|
|
|
|
|
+ string target_namespace,
|
|
|
|
|
+ uint64 target_serial
|
|
|
|
|
+ ) throws SqlError {
|
|
|
|
|
+ var needed = new HashSet<string>();
|
|
|
|
|
+ var to_process = new Vector<string>();
|
|
|
|
|
+
|
|
|
|
|
+ // Start with the target migration
|
|
|
|
|
+ string target_key = "%s:%s".printf(target_namespace, target_serial.to_string());
|
|
|
|
|
+ needed.add(target_key);
|
|
|
|
|
+ to_process.add(target_key);
|
|
|
|
|
+
|
|
|
|
|
+ // Also add all migrations in the target namespace with serial <= target
|
|
|
|
|
+ var ns_migrations = _migrations_by_namespace.get(target_namespace);
|
|
|
|
|
+ if (ns_migrations != null) {
|
|
|
|
|
+ foreach (var migration in ns_migrations) {
|
|
|
|
|
+ if (migration.serial <= target_serial) {
|
|
|
|
|
+ string key = "%s:%s".printf(migration.migration_namespace, migration.serial.to_string());
|
|
|
|
|
+ if (!needed.contains(key)) {
|
|
|
|
|
+ needed.add(key);
|
|
|
|
|
+ to_process.add(key);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Process dependencies transitively
|
|
|
|
|
+ while (to_process.length > 0) {
|
|
|
|
|
+ var current = to_process.first();
|
|
|
|
|
+ to_process.remove_at(0);
|
|
|
|
|
+
|
|
|
|
|
+ var parts = current.split(":");
|
|
|
|
|
+ if (parts.length != 2) continue;
|
|
|
|
|
+
|
|
|
|
|
+ var migration = find_migration(parts[0], uint64.parse(parts[1]));
|
|
|
|
|
+ if (migration == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var dep_str in migration.dependencies) {
|
|
|
|
|
+ var dep = Dependency.parse(dep_str);
|
|
|
|
|
+
|
|
|
|
|
+ if (dep.serial != null) {
|
|
|
|
|
+ string dep_key = "%s:%s".printf(dep.namespace, dep.serial.to_string());
|
|
|
|
|
+ if (!needed.contains(dep_key)) {
|
|
|
|
|
+ needed.add(dep_key);
|
|
|
|
|
+ to_process.add(dep_key);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Add all migrations in the namespace
|
|
|
|
|
+ var dep_ns_migrations = _migrations_by_namespace.get(dep.namespace);
|
|
|
|
|
+ if (dep_ns_migrations != null) {
|
|
|
|
|
+ foreach (var ns_migration in dep_ns_migrations) {
|
|
|
|
|
+ string ns_key = "%s:%s".printf(ns_migration.migration_namespace, ns_migration.serial.to_string());
|
|
|
|
|
+ if (!needed.contains(ns_key)) {
|
|
|
|
|
+ needed.add(ns_key);
|
|
|
|
|
+ to_process.add(ns_key);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return needed;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|