Ver código fonte

Merge branch 'orm-phase-8' of Tilo15/Invercargill-Sql into master

Billy Barrow 1 mês atrás
pai
commit
006f0f8a2e

+ 3 - 2
examples/demo.vala

@@ -84,8 +84,9 @@ void run_migrations(Connection conn, SqlDialect dialect) throws SqlError {
     // Run all pending migrations
     runner.migrate_to_latest();
     
-    int current_version = runner.get_current_version();
-    print("Applied migrations to version %d\n", current_version);
+    // Get applied migrations count
+    var applied = runner.get_applied_migrations();
+    print("Applied %d migrations\n", (int)applied.length);
 }
 
 void register_entities(TypeRegistry registry, Connection conn, SqlDialect dialect) throws SqlError {

+ 2 - 1
examples/migrations/v001-create-users.vala

@@ -5,7 +5,8 @@ using InvercargillSql.Migrations;
  * Migration to create the users table with indexes.
  */
 public class V001_CreateUsers : Migration {
-    public override int version { get { return 1; } }
+    public override string migration_namespace { get { return "app"; } }
+    public override uint64 serial { get { return 1; } }
     public override string name { get { return "CreateUsers"; } }
     
     public override void up(MigrationBuilder b) throws SqlError {

+ 2 - 1
examples/migrations/v002-create-products.vala

@@ -5,7 +5,8 @@ using InvercargillSql.Migrations;
  * Migration to create the products table with category index.
  */
 public class V002_CreateProducts : Migration {
-    public override int version { get { return 2; } }
+    public override string migration_namespace { get { return "app"; } }
+    public override uint64 serial { get { return 2; } }
     public override string name { get { return "CreateProducts"; } }
     
     public override void up(MigrationBuilder b) throws SqlError {

+ 2 - 1
examples/migrations/v003-create-orders.vala

@@ -10,7 +10,8 @@ using InvercargillSql.Migrations;
  * - Chaining FK options like .name() and .on_delete_cascade()
  */
 public class V003_CreateOrders : Migration {
-    public override int version { get { return 3; } }
+    public override string migration_namespace { get { return "app"; } }
+    public override uint64 serial { get { return 3; } }
     public override string name { get { return "CreateOrders"; } }
     
     public override void up(MigrationBuilder b) throws SqlError {

+ 2 - 1
examples/migrations/v004-add-category.vala

@@ -11,7 +11,8 @@ using InvercargillSql.Migrations;
  * - Using drop_foreign_key_on() in down() for auto-generated FK names
  */
 public class V004_AddCategory : Migration {
-    public override int version { get { return 4; } }
+    public override string migration_namespace { get { return "app"; } }
+    public override uint64 serial { get { return 4; } }
     public override string name { get { return "AddCategory"; } }
     
     public override void up(MigrationBuilder b) throws SqlError {

+ 1 - 0
src/meson.build

@@ -74,6 +74,7 @@ sources += files('migrations/alter-table-builder.vala')
 sources += files('migrations/migration.vala')
 sources += files('migrations/migration-builder.vala')
 sources += files('migrations/migration-runner.vala')
+sources += files('migrations/dependency.vala')
 
 # Build shared library
 invercargill_sql = shared_library('invercargill-sql', sources,

+ 175 - 0
src/migrations/dependency.vala

@@ -0,0 +1,175 @@
+namespace InvercargillSql.Migrations {
+
+    /**
+     * Represents a parsed migration dependency.
+     * 
+     * Dependencies are declared as strings in one of two formats:
+     * - "namespace" - Any migration in that namespace must be applied
+     * - "namespace:serial" - A specific migration must be applied
+     * 
+     * Example usage:
+     * {{{
+     * try {
+     *     var dep1 = Dependency.parse("auth");
+     *     assert(dep1.namespace == "auth");
+     *     assert(dep1.serial == null);
+     *     
+     *     var dep2 = Dependency.parse("auth:2");
+     *     assert(dep2.namespace == "auth");
+     *     assert(dep2.serial == 2);
+     * } catch (SqlError e) {
+     *     stderr.printf("Parse error: %s\n", e.message);
+     * }
+     * }}}
+     */
+    public class Dependency : Object {
+
+        /**
+         * The namespace name for this dependency.
+         * 
+         * This is required and identifies which namespace the dependency
+         * refers to (e.g., "auth", "logging", "core").
+         */
+        public string namespace { get; private set; }
+
+        /**
+         * The optional serial number within the namespace.
+         * 
+         * If null, the dependency means "at least one migration in the
+         * namespace must be applied". If set, it means "the specific
+         * migration with this serial must be applied".
+         */
+        public uint64? serial { get; private set; }
+
+        /**
+         * Private constructor - use parse() to create instances.
+         */
+        private Dependency(string namespace, uint64? serial = null) {
+            this.namespace = namespace;
+            this.serial = serial;
+        }
+
+        /**
+         * Parses a dependency string into a Dependency object.
+         * 
+         * Valid formats:
+         * - "auth" - namespace only, serial is null
+         * - "auth:2" - namespace with serial number
+         * - "auth:0" - namespace with serial 0 (valid)
+         * 
+         * Invalid formats (will throw SqlError):
+         * - ":1" - empty namespace
+         * - "auth:" - empty serial
+         * - "auth:1:extra" - too many colons
+         * - "auth:abc" - non-numeric serial
+         * 
+         * @param dep the dependency string to parse
+         * @return a new Dependency object
+         * @throws SqlError if the dependency syntax is invalid
+         */
+        public static Dependency parse(string dep) throws SqlError {
+            // Count colons to detect multiple colons
+            int colon_count = 0;
+            int first_colon_pos = -1;
+            for (int i = 0; i < dep.length; i++) {
+                if (dep[i] == ':') {
+                    colon_count++;
+                    if (first_colon_pos == -1) {
+                        first_colon_pos = i;
+                    }
+                }
+            }
+
+            // Multiple colons is invalid syntax
+            if (colon_count > 1) {
+                throw new SqlError.GENERAL_ERROR(
+                    "Invalid dependency syntax: '%s' - expected 'namespace' or 'namespace:serial'".printf(dep)
+                );
+            }
+
+            // No colon - just a namespace
+            if (colon_count == 0) {
+                // Empty namespace is invalid
+                if (dep.length == 0) {
+                    throw new SqlError.GENERAL_ERROR(
+                        "Invalid dependency syntax: '' - namespace cannot be empty"
+                    );
+                }
+                return new Dependency(dep, null);
+            }
+
+            // One colon - split into namespace and serial
+            string ns = dep.substring(0, first_colon_pos);
+            string serial_str = dep.substring(first_colon_pos + 1);
+
+            // Empty namespace is invalid
+            if (ns.length == 0) {
+                throw new SqlError.GENERAL_ERROR(
+                    "Invalid dependency syntax: '%s' - namespace cannot be empty".printf(dep)
+                );
+            }
+
+            // Empty serial is invalid
+            if (serial_str.length == 0) {
+                throw new SqlError.GENERAL_ERROR(
+                    "Invalid dependency syntax: '%s' - expected 'namespace' or 'namespace:serial'".printf(dep)
+                );
+            }
+
+            // Parse serial as uint64
+            uint64 serial_value;
+            if (!uint64.try_parse(serial_str, out serial_value)) {
+                throw new SqlError.GENERAL_ERROR(
+                    "Invalid dependency syntax: '%s' - serial must be a number".printf(dep)
+                );
+            }
+
+            return new Dependency(ns, serial_value);
+        }
+
+        /**
+         * Converts this dependency back to its string representation.
+         * 
+         * @return the string form of this dependency
+         */
+        public string to_string() {
+            if (serial == null) {
+                return namespace;
+            }
+            return "%s:%s".printf(namespace, serial.to_string());
+        }
+
+        /**
+         * Checks equality with another object.
+         * 
+         * @param obj the object to compare with
+         * @return true if the objects are equal
+         */
+        public bool equals_dependency(Dependency other) {
+            if (namespace != other.namespace) {
+                return false;
+            }
+            // Both null or both equal
+            if (serial == null && other.serial == null) {
+                return true;
+            }
+            if (serial == null || other.serial == null) {
+                return false;
+            }
+            return serial == other.serial;
+        }
+
+        /**
+         * Returns a hash code for this dependency.
+         * 
+         * @return the hash code
+         */
+        public uint hash_dependency() {
+            uint hash = namespace.hash();
+            if (serial != null) {
+                hash ^= direct_hash((void*) serial);
+            }
+            return hash;
+        }
+    }
+}

+ 1024 - 64
src/migrations/migration-runner.vala

@@ -2,128 +2,511 @@ using Invercargill.DataStructures;
 using InvercargillSql.Dialects;
 
 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 {
         private Connection _connection;
         private SqlDialect _dialect;
         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) {
             _connection = connection;
             _dialect = dialect;
             _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) {
             _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 {
+            if (_migrations_table_ensured) {
+                return;
+            }
+            
             _connection.execute("
                 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,
-                    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) {
-                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();
-            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>();
             
             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);
                 }
             }
             
-            // 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;
         }
-        
+
+        /**
+         * 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 {
             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();
-            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 {
             ensure_migrations_table();
-            var applied = get_applied_versions();
+            
+            var applied = get_applied_migrations();
             
             int count = 0;
             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) {
                     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 {
             ensure_migrations_table();
-            var applied = get_applied_versions();
+            
+            var applied = get_applied_migrations();
             
             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) {
                     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 {
             var builder = new MigrationBuilder(_dialect);
             migration.up(builder);
@@ -136,9 +519,10 @@ namespace InvercargillSql.Migrations {
                 var now = new DateTime.now_utc();
                 int64 timestamp = now.to_unix();
                 _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<int64?>("applied_at", timestamp)
                 .execute_non_query();
@@ -149,7 +533,13 @@ namespace InvercargillSql.Migrations {
                 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 {
             var builder = new MigrationBuilder(_dialect);
             migration.down(builder);
@@ -159,9 +549,12 @@ namespace InvercargillSql.Migrations {
                 builder.execute(_connection);
                 
                 // 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();
             } catch (SqlError e) {
@@ -169,14 +562,581 @@ namespace InvercargillSql.Migrations {
                 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) {
-                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;
         }
+
+        /**
+         * 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;
+        }
     }
 }

+ 38 - 5
src/migrations/migration.vala

@@ -6,10 +6,14 @@ namespace InvercargillSql.Migrations {
      * Each migration represents a database schema change and must implement
      * the up() and down() methods to define the forward and reverse operations.
      * 
+     * Migrations are organized into namespaces to allow different modules or
+     * features to maintain their own independent migration histories.
+     * 
      * Example implementation:
      * {{{
      * public class CreateUserTable : Migration {
-     *     public override int version { get { return 2026031901; } }
+     *     public override uint64 serial { get { return 2026031901; } }
+     *     public override string migration_namespace { get { return "core"; } }
      *     public override string name { get { return "create_user_table"; } }
      *     
      *     public override void up(MigrationBuilder b) throws SqlError {
@@ -29,15 +33,30 @@ namespace InvercargillSql.Migrations {
     public abstract class Migration : Object {
         
         /**
-         * The version number of this migration.
+         * The serial number of this migration within its namespace.
          * 
-         * Version numbers should be monotonically increasing integers.
+         * Serial numbers should be monotonically increasing within a namespace.
          * A common pattern is to use a timestamp format like YYYYMMDDNN
          * (e.g., 2026031901 for the first migration on March 19, 2026).
          * 
-         * @return the unique version identifier for this migration
+         * The type is uint64 to support date-based serials like 20260320.
+         * 
+         * @return the unique serial identifier for this migration within its namespace
+         */
+        public abstract uint64 serial { get; }
+        
+        /**
+         * The namespace this migration belongs to.
+         * 
+         * Namespaces allow different modules or features to maintain their own
+         * independent migration histories. Each namespace tracks its own set
+         * of applied migrations independently.
+         * 
+         * Common namespace examples: "core", "auth", "billing", "inventory"
+         * 
+         * @return the namespace identifier for this migration
          */
-        public abstract int version { get; }
+        public abstract string migration_namespace { get; }
         
         /**
          * A human-readable name for this migration.
@@ -50,6 +69,20 @@ namespace InvercargillSql.Migrations {
          */
         public abstract string name { get; }
         
+        /**
+         * List of migration dependencies as qualified names.
+         * 
+         * Dependencies are specified as "namespace:serial" strings indicating
+         * migrations that must be applied before this one. This allows
+         * cross-namespace dependencies when one module depends on another's
+         * schema changes.
+         * 
+         * Example: { "core:2026031901", "auth:2026032005" }
+         * 
+         * @return an array of dependency identifiers, empty by default
+         */
+        public virtual owned string[] dependencies { owned get { return new string[]{}; } }
+        
         /**
          * Apply the migration forward.
          * 

+ 1 - 0
src/orm/type-registry.vala

@@ -60,6 +60,7 @@ namespace InvercargillSql.Orm {
             owned GLib.Func<EntityMapperBuilder<T>> build_action
         ) throws SqlError {
             var builder = new EntityMapperBuilder<T>();
+            builder.table(table_name);
             build_action(builder);
             
             // Introspect schema and apply to mapper

+ 14 - 0
src/sqlite/sqlite-elements.vala

@@ -19,6 +19,7 @@ namespace InvercargillSql {
         
         public bool assignable_to_type(GLib.Type type) {
             return type == typeof(int64) || type == typeof(int64?) ||
+                   type == typeof(uint64) || type == typeof(uint64?) ||
                    type == typeof(int) || type == typeof(int?) ||
                    type == typeof(bool) || type == typeof(bool?) ||
                    type == typeof(DateTime) || type == typeof(DateTime?) ||
@@ -48,6 +49,10 @@ namespace InvercargillSql {
             if (requested_type == typeof(int64)) {
                 return Invercargill.Nullable.convert_nullability<int64?, T>(_value, out result);
             }
+            // Convert to uint64
+            if (requested_type == typeof(uint64)) {
+                return Invercargill.Nullable.convert_nullability<uint64?, T>((uint64)_value, out result);
+            }
             // Convert to int32
             if (requested_type == typeof(int)) {
                 return Invercargill.Nullable.convert_nullability<int?, T>((int)_value, out result);
@@ -423,6 +428,15 @@ namespace InvercargillSql {
                 }
             }
             
+            // Try uint64
+            if (element.assignable_to_type(typeof(uint64))) {
+                uint64? u;
+                if (element.try_get_as<uint64?>(out u) && u != null) {
+                    stmt.bind_int64(param_index, (int64)u);
+                    return;
+                }
+            }
+            
             // Try double
             if (element.assignable_to_type(typeof(double))) {
                 double? d;

Diferenças do arquivo suprimidas por serem muito extensas
+ 1279 - 96
src/tests/migration-test.vala


+ 8 - 12
src/tests/orm-test.vala

@@ -859,8 +859,7 @@ OrmSession setup_test_session() throws SqlError {
     """);
     
     // Register TestUser mapper with schema introspection on registry
-    registry.register_entity<TestUser>(EntityMapper.build_for<TestUser>(b => {
-        b.table("users");
+    registry.register_with_schema<TestUser>("users", conn, dialect, b => {
         b.column<int64?>("id", u => u.id, (u, v) => u.id = v);
         b.column<string>("name", u => u.name, (u, v) => u.name = v);
         b.column<string>("email", u => u.email, (u, v) => u.email = v);
@@ -868,7 +867,7 @@ OrmSession setup_test_session() throws SqlError {
         b.column<double?>("salary", u => u.salary, (u, v) => u.salary = v);
         b.column<bool?>("is_active", u => u.is_active, (u, v) => u.is_active = v);
         b.column<DateTime?>("created_at", u => u.created_at, (u, v) => u.created_at = v);
-    }));
+    });
     
     return new OrmSession(conn, registry, dialect);
 }
@@ -912,12 +911,11 @@ void test_orm_session_register_with_schema() throws Error {
     """);
     
     // Register with schema introspection on registry - PK and auto-increment discovered automatically
-    registry.register_entity<TestUser>(EntityMapper.build_for<TestUser>(b => {
-        b.table("users");
+    registry.register_with_schema<TestUser>("users", conn, dialect, b => {
         b.column<int64?>("id", u => u.id, (u, v) => u.id = v);
         b.column<string>("name", u => u.name, (u, v) => u.name = v);
         b.column<string>("email", u => u.email, (u, v) => u.email = v);
-    }));
+    });
     
     var session = new OrmSession(conn, registry, dialect);
     
@@ -1281,14 +1279,13 @@ OrmSession setup_product_session() throws SqlError {
         )
     """);
     
-    registry.register_entity<TestProduct>(EntityMapper.build_for<TestProduct>(b => {
-        b.table("products");
+    registry.register_with_schema<TestProduct>("products", conn, dialect, b => {
         b.column<int64?>("id", p => p.id, (p, v) => p.id = v);
         b.column<string>("name", p => p.name, (p, v) => p.name = v);
         b.column<string>("category", p => p.category, (p, v) => p.category = v);
         b.column<double?>("price", p => p.price, (p, v) => p.price = v);
         b.column<int64?>("stock", p => p.stock, (p, v) => p.stock = v);
-    }));
+    });
     
     return new OrmSession(conn, registry, dialect);
 }
@@ -1313,8 +1310,7 @@ OrmSession setup_order_session() throws SqlError {
         )
     """);
     
-    registry.register_entity<TestOrder>(EntityMapper.build_for<TestOrder>(b => {
-        b.table("orders");
+    registry.register_with_schema<TestOrder>("orders", conn, dialect, b => {
         b.column<int64?>("id", o => o.id, (o, v) => o.id = v);
         b.column<int64?>("user_id", o => o.user_id, (o, v) => o.user_id = v);
         b.column<int64?>("product_id", o => o.product_id, (o, v) => o.product_id = v);
@@ -1322,7 +1318,7 @@ OrmSession setup_order_session() throws SqlError {
         b.column<double?>("total", o => o.total, (o, v) => o.total = v);
         b.column<string>("status", o => o.status, (o, v) => o.status = v);
         b.column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
-    }));
+    });
     
     return new OrmSession(conn, registry, dialect);
 }

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff