Jelajahi Sumber

Phase 8 plan

Billy Barrow 1 bulan lalu
induk
melakukan
a11e76f62c
2 mengubah file dengan 471 tambahan dan 505 penghapusan
  1. 471 0
      plans/phase-8-async-crud-methods.md
  2. 0 505
      plans/phase-8-namespaces.md

+ 471 - 0
plans/phase-8-async-crud-methods.md

@@ -0,0 +1,471 @@
+# Phase 8: Async CRUD Methods for OrmSession - Implementation Plan
+
+---
+
+> ## ⚠️ CRITICAL IMPLEMENTATION NOTES - READ FIRST ⚠️
+>
+> ### Code Style Requirements
+>
+> 1. **DO NOT use GLib.List, GLib.HashTable, or Libgee collections**
+>    - Use ONLY `Invercargill.DataStructures` for all collections
+>    - `Vector<T>` instead of `GLib.List<T>` or arrays
+>    - `Dictionary<TKey, TValue>` instead of `GLib.HashTable` or `GLib.Map`
+>    - `HashSet<T>` for set operations
+>
+> 2. **Reference the Invercargill Library**
+>    - Analyze `../Invercargill/src/lib/` for DataStructures and Expressions patterns
+>    - Key directories: `DataStructures/`, `Expressions/`, `Mapping/`
+>    - Use `Invercargill.Expressions` for all expression handling
+>
+> 3. **No Raw SQL in High-Level APIs**
+>    - All expression parameters use **Invercargill.Expressions** syntax
+>    - Raw SQL is only generated internally by dialect implementations
+>
+> ### Async Pattern Requirements
+>
+> 1. **Thread Offloading at Lowest Level**
+>    - SQLite has no native async API - thread offloading is acceptable
+>    - Thread offloading happens in interfaces (`Command`, `Connection`, `Transaction`)
+>    - Higher-level code should use `yield` to propagate async correctly
+>
+> 2. **No Blocking Async Methods**
+>    - Async methods must NOT simply call their sync counterparts
+>    - Use `yield` to call async methods on dependencies
+>    - Maintain the async chain all the way down to the interface level
+
+---
+
+## Executive Summary
+
+This document provides a detailed implementation plan for Phase 8: Adding async versions of the CRUD methods (`insert_async`, `update_async`, `delete_async`) to [`OrmSession`](src/orm/orm-session.vala).
+
+## Current State Analysis
+
+### Existing Sync Methods in OrmSession
+
+The [`OrmSession`](src/orm/orm-session.vala) class currently provides three synchronous CRUD methods:
+
+| Method | Lines | Description |
+|--------|-------|-------------|
+| [`insert<T>(T entity)`](src/orm/orm-session.vala:80) | 80-125 | Inserts an entity, back-populates auto-generated primary key |
+| [`update<T>(T entity)`](src/orm/orm-session.vala:133) | 133-170 | Updates an entity by primary key |
+| [`delete<T>(T entity)`](src/orm/orm-session.vala:178) | 178-199 | Deletes an entity by primary key |
+
+### Async Pattern Reference
+
+The codebase already has a well-established async pattern in [`Command`](src/interfaces/command.vala):
+
+```vala
+public async virtual int execute_non_query_async() throws SqlError {
+    SourceFunc callback = execute_non_query_async.callback;
+    int result = 0;
+    SqlError? error = null;
+    
+    new Thread<void*>(null, () => {
+        try {
+            result = execute_non_query();
+        } catch (SqlError e) {
+            error = e;
+        }
+        Idle.add((owned)callback);
+        return null;
+    });
+    yield;
+    
+    if (error != null) {
+        throw error;
+    }
+    return result;
+}
+```
+
+The async methods in `OrmSession` should use `yield command.execute_non_query_async()` to properly propagate async behavior.
+
+## Implementation Plan
+
+### Part 1: Add `insert_async<T>()` Method
+
+**File:** [`src/orm/orm-session.vala`](src/orm/orm-session.vala)
+
+**Location:** After the existing [`insert<T>()`](src/orm/orm-session.vala:80) method (around line 125)
+
+**Implementation:**
+
+```vala
+/**
+ * Inserts an entity into the database asynchronously.
+ * 
+ * This method performs the same operation as insert() but in a non-blocking
+ * manner, allowing the main loop to process other events while waiting
+ * for the database operation to complete.
+ * 
+ * After insertion, if the entity has an auto-increment primary key,
+ * the key value is back-populated into the entity.
+ * 
+ * @param entity The entity to insert
+ * @throws SqlError if insertion fails
+ */
+public async void insert_async<T>(T entity) throws Error {
+    var mapper = get_mapper<T>();
+    Invercargill.Properties properties;
+    try {
+        properties = mapper.map_from(entity);
+    } catch (Error e) {
+        throw new SqlError.GENERAL_ERROR("Failed to map entity: %s".printf(e.message));
+    }
+    
+    // Build column list, excluding auto-increment columns
+    var columns = new Vector<string>();
+    foreach (var col in mapper.columns) {
+        if (!mapper.is_auto_increment(col.name)) {
+            columns.add(col.name);
+        }
+    }
+    
+    var sql = _dialect.build_insert_sql(mapper.table_name, columns);
+    var command = _connection.create_command(sql);
+    
+    // Add parameters in column order (excluding auto-increment)
+    foreach (var col in mapper.columns) {
+        if (mapper.is_auto_increment(col.name)) {
+            continue;  // Skip auto-increment columns
+        }
+        var value = properties.get(col.name);
+        if (value != null) {
+            command.with_parameter<Invercargill.Element>(col.name, value);
+        } else {
+            command.with_null(col.name);
+        }
+    }
+    
+    // Execute asynchronously - THIS IS THE KEY CHANGE
+    yield command.execute_non_query_async();
+    
+    // Back-populate the generated primary key
+    var pk_column = mapper.get_effective_primary_key();
+    if (pk_column != null && mapper.is_auto_increment(pk_column)) {
+        int64 generated_id = _connection.last_insert_rowid;
+        try {
+            mapper.set_property_value(entity, pk_column, generated_id);
+        } catch (Error e) {
+            throw new SqlError.GENERAL_ERROR("Failed to back-populate primary key: %s".printf(e.message));
+        }
+    }
+}
+```
+
+### Part 2: Add `update_async<T>()` Method
+
+**File:** [`src/orm/orm-session.vala`](src/orm/orm-session.vala)
+
+**Location:** After the existing [`update<T>()`](src/orm/orm-session.vala:133) method (around line 170)
+
+**Implementation:**
+
+```vala
+/**
+ * Updates an entity in the database asynchronously.
+ * 
+ * This method performs the same operation as update() but in a non-blocking
+ * manner, allowing the main loop to process other events while waiting
+ * for the database operation to complete.
+ * 
+ * The entity is identified by its primary key.
+ * 
+ * @param entity The entity to update
+ * @throws SqlError if update fails
+ */
+public async void update_async<T>(T entity) throws Error {
+    var mapper = get_mapper<T>();
+    Invercargill.Properties properties;
+    try {
+        properties = mapper.map_from(entity);
+    } catch (Error e) {
+        throw new SqlError.GENERAL_ERROR("Failed to map entity: %s".printf(e.message));
+    }
+    
+    var columns = new Vector<string>();
+    foreach (var col in mapper.columns) {
+        columns.add(col.name);
+    }
+    
+    var pk_column = mapper.get_effective_primary_key();
+    var sql = _dialect.build_update_sql(mapper.table_name, columns, pk_column);
+    var command = _connection.create_command(sql);
+    
+    // Add parameters for SET clause
+    foreach (var col in mapper.columns) {
+        var value = properties.get(col.name);
+        if (value != null) {
+            command.with_parameter<Invercargill.Element>(col.name, value);
+        } else {
+            command.with_null(col.name);
+        }
+    }
+    
+    // Add primary key parameter for WHERE clause
+    var pk_value = properties.get(pk_column);
+    if (pk_value != null) {
+        command.with_parameter<Invercargill.Element>(pk_column, pk_value);
+    } else {
+        command.with_null(pk_column);
+    }
+    
+    // Execute asynchronously - THIS IS THE KEY CHANGE
+    yield command.execute_non_query_async();
+}
+```
+
+### Part 3: Add `delete_async<T>()` Method
+
+**File:** [`src/orm/orm-session.vala`](src/orm/orm-session.vala)
+
+**Location:** After the existing [`delete<T>()`](src/orm/orm-session.vala:178) method (around line 199)
+
+**Implementation:**
+
+```vala
+/**
+ * Deletes an entity from the database asynchronously.
+ * 
+ * This method performs the same operation as delete() but in a non-blocking
+ * manner, allowing the main loop to process other events while waiting
+ * for the database operation to complete.
+ * 
+ * The entity is identified by its primary key.
+ * 
+ * @param entity The entity to delete
+ * @throws SqlError if deletion fails
+ */
+public async void delete_async<T>(T entity) throws Error {
+    var mapper = get_mapper<T>();
+    Invercargill.Properties properties;
+    try {
+        properties = mapper.map_from(entity);
+    } catch (Error e) {
+        throw new SqlError.GENERAL_ERROR("Failed to map entity: %s".printf(e.message));
+    }
+    
+    var pk_column = mapper.get_effective_primary_key();
+    var sql = _dialect.build_delete_sql(mapper.table_name, pk_column);
+    var command = _connection.create_command(sql);
+    
+    var pk_value = properties.get(pk_column);
+    if (pk_value != null) {
+        command.with_parameter<Invercargill.Element>(pk_column, pk_value);
+    } else {
+        command.with_null(pk_column);
+    }
+    
+    // Execute asynchronously - THIS IS THE KEY CHANGE
+    yield command.execute_non_query_async();
+}
+```
+
+## Architecture Flow
+
+```mermaid
+sequenceDiagram
+    participant User Code
+    participant OrmSession
+    participant Command
+    participant Thread Pool
+    participant SQLite
+    
+    User Code->>OrmSession: insert_async entity
+    OrmSession->>OrmSession: Map entity to properties
+    OrmSession->>OrmSession: Build INSERT SQL
+    OrmSession->>Command: create_command sql
+    OrmSession->>Command: with_parameter for each column
+    OrmSession->>Command: execute_non_query_async
+    Command->>Thread Pool: new Thread
+    Thread Pool->>SQLite: execute_non_query
+    SQLite-->>Thread Pool: success
+    Thread Pool->>Command: Idle.add callback
+    Command-->>OrmSession: yield returns
+    OrmSession->>OrmSession: Back-populate primary key
+    OrmSession-->>User Code: async completes
+```
+
+## Files to Modify
+
+| File | Changes |
+|------|---------|
+| `src/orm/orm-session.vala` | Add `insert_async<T>()`, `update_async<T>()`, `delete_async<T>()` methods |
+| `src/tests/orm-test.vala` | Add async tests for new methods |
+
+## Implementation Order
+
+1. **Step 1:** Add `insert_async<T>()` method
+   - Follows the same logic as `insert<T>()` but uses `yield command.execute_non_query_async()`
+   - Includes back-population of auto-generated primary key
+
+2. **Step 2:** Add `update_async<T>()` method
+   - Follows the same logic as `update<T>()` but uses `yield command.execute_non_query_async()`
+
+3. **Step 3:** Add `delete_async<T>()` method
+   - Follows the same logic as `delete<T>()` but uses `yield command.execute_non_query_async()`
+
+4. **Step 4:** Add tests for all three async methods
+   - Test basic functionality
+   - Test that async methods actually yield
+   - Test that results match sync counterparts
+
+## Test Cases
+
+### Tests for `insert_async<T>()`
+
+```vala
+/**
+ * Test: Async insert basic functionality.
+ */
+async void test_insert_async_basic() throws Error {
+    print("Test: insert_async basic... ");
+    var session = setup_test_session();
+    
+    var user = new TestUser();
+    user.name = "AsyncUser";
+    user.email = "async@example.com";
+    user.age = 30;
+    
+    assert(user.id == 0);
+    
+    yield session.insert_async(user);
+    
+    // Verify back-population
+    assert(user.id > 0);
+    
+    // Verify data was persisted
+    var results = session.query<TestUser>()
+        .where("id == " + user.id.to_string())
+        .materialise();
+    var arr = results.to_array();
+    assert(arr.length == 1);
+    assert(arr[0].name == "AsyncUser");
+    
+    print("PASSED\n");
+}
+
+/**
+ * Test: Async insert yields to main loop.
+ */
+async void test_insert_async_yields() throws Error {
+    print("Test: insert_async yields... ");
+    var session = setup_test_session();
+    
+    bool callback_executed = false;
+    var source = new IdleSource();
+    source.set_callback(() => {
+        callback_executed = true;
+        return Source.REMOVE;
+    });
+    source.attach(MainContext.default());
+    
+    var user = new TestUser();
+    user.name = "YieldTest";
+    user.age = 25;
+    
+    yield session.insert_async(user);
+    
+    // If async properly yielded, the idle callback should have executed
+    assert(callback_executed);
+    
+    print("PASSED\n");
+}
+```
+
+### Tests for `update_async<T>()`
+
+```vala
+/**
+ * Test: Async update basic functionality.
+ */
+async void test_update_async_basic() throws Error {
+    print("Test: update_async basic... ");
+    var session = setup_test_session();
+    
+    // Insert a user first
+    var user = new TestUser();
+    user.name = "Original";
+    user.age = 25;
+    session.insert(user);
+    
+    // Update the user
+    user.name = "Updated";
+    user.age = 26;
+    yield session.update_async(user);
+    
+    // Verify update
+    var updated = session.query<TestUser>()
+        .where("id == " + user.id.to_string())
+        .first();
+    assert(updated != null);
+    assert(updated.name == "Updated");
+    assert(updated.age == 26);
+    
+    print("PASSED\n");
+}
+```
+
+### Tests for `delete_async<T>()`
+
+```vala
+/**
+ * Test: Async delete basic functionality.
+ */
+async void test_delete_async_basic() throws Error {
+    print("Test: delete_async basic... ");
+    var session = setup_test_session();
+    
+    // Insert a user first
+    var user = new TestUser();
+    user.name = "ToDelete";
+    user.age = 99;
+    session.insert(user);
+    var user_id = user.id;
+    
+    // Delete the user
+    yield session.delete_async(user);
+    
+    // Verify deletion
+    var results = session.query<TestUser>()
+        .where("id == " + user_id.to_string())
+        .materialise();
+    var arr = results.to_array();
+    assert(arr.length == 0);
+    
+    print("PASSED\n");
+}
+```
+
+## Design Decisions
+
+### Why Use `yield command.execute_non_query_async()`
+
+The async methods use `yield command.execute_non_query_async()` instead of calling the sync version because:
+
+1. **Proper async propagation** - The async/await pattern requires the whole call chain to be async
+2. **Main loop integration** - Yielding allows the main loop to process other events
+3. **Consistent API** - Matches the pattern used in Phase 5 for query methods
+
+### Back-Population in `insert_async`
+
+The `insert_async` method maintains the same back-population behavior as the sync version:
+
+1. After the async INSERT completes, check if there's an auto-increment primary key
+2. Retrieve the generated ID via `_connection.last_insert_rowid`
+3. Set the property value on the entity using `mapper.set_property_value()`
+
+This ensures that after `yield session.insert_async(entity)`, the entity's ID property contains the generated value.
+
+## Summary
+
+This phase adds async versions of the three CRUD methods to `OrmSession`:
+
+| Method | Purpose | Key Implementation Detail |
+|--------|---------|---------------------------|
+| `insert_async<T>()` | Async insert with back-population | Uses `yield command.execute_non_query_async()` |
+| `update_async<T>()` | Async update by primary key | Uses `yield command.execute_non_query_async()` |
+| `delete_async<T>()` | Async delete by primary key | Uses `yield command.execute_non_query_async()` |
+
+These methods follow the established async patterns from Phase 5 and integrate seamlessly with the existing `Command.execute_non_query_async()` implementation.

+ 0 - 505
plans/phase-8-namespaces.md

@@ -1,505 +0,0 @@
-# Phase 8: Migration Namespaces
-
-## Overview
-
-This document describes the design for adding namespace support to the migration system. Namespaces allow libraries to share the same database while maintaining their own migration histories without interfering with each other's migration order.
-
-## Problem Statement
-
-Without namespaces, all migrations share a single version sequence. If a library provides migrations with versions 1, 2, 3 and the application also uses 1, 2, 3, they would conflict. Libraries cannot safely ship migrations that integrate with an application's database.
-
-## Design Goals
-
-- **Namespace isolation**: Each namespace has its own migration sequence
-- **Cross-namespace dependencies**: Migrations can declare dependencies on other namespaces
-- **Safe foreign keys**: Libraries can create tables with foreign keys to other libraries' tables
-- **Explicit ordering**: Dependencies determine cross-namespace execution order
-- **Time-based rollback**: Rolling back to a point in time affects all namespaces consistently
-
-## Architecture
-
-### Migration Class Changes
-
-The `Migration` abstract class is extended with namespace support:
-
-```vala
-public abstract class Migration : Object {
-    /**
-     * The serial number of this migration within its namespace.
-     * 
-     * Serials must be unique within a namespace and are used to determine
-     * the order of migrations within that namespace. They do not need to
-     * be sequential (e.g., 20260112, 20260315 are valid).
-     * 
-     * Migrations within a namespace are executed in ascending serial order.
-     */
-    public abstract uint64 serial { get; }
-    
-    /**
-     * The namespace this migration belongs to.
-     *
-     * Namespaces isolate migration histories. Each namespace maintains
-     * its own sequence of applied migrations. Libraries should use a
-     * unique namespace (e.g., "auth", "logging", "my_library").
-     *
-     * This property is abstract and must be implemented by all migrations.
-     * Named `migration_namespace` to avoid conflict with Vala's `namespace` keyword.
-     */
-    public abstract string migration_namespace { get; }
-    
-    /**
-     * A human-readable name for this migration.
-     */
-    public abstract string name { get; }
-    
-    /**
-     * Dependencies on other namespaces or specific migrations.
-     * 
-     * Dependencies are declared as strings in one of two formats:
-     * - "namespace" - Any migration in that namespace must be applied
-     * - "namespace:serial" - The specific migration must be applied
-     * 
-     * Examples:
-     * - {"auth"} - Auth namespace must have at least one migration applied
-     * - {"auth:2"} - Auth namespace must have migration serial 2 applied
-     * - {"auth:2", "logging"} - Both conditions must be met
-     * 
-     * Default implementation returns an empty array (no dependencies).
-     */
-    public virtual string[] dependencies { get { return {}; } }
-    
-    /**
-     * Apply the migration forward.
-     */
-    public abstract void up(MigrationBuilder b) throws SqlError;
-    
-    /**
-     * Roll back the migration.
-     */
-    public abstract void down(MigrationBuilder b) throws SqlError;
-}
-```
-
-### Migration Tracking Table
-
-The `__migrations` table is updated to support namespaces:
-
-```sql
-CREATE TABLE __migrations (
-    application_order INTEGER PRIMARY KEY AUTOINCREMENT,
-    namespace TEXT NOT NULL,
-    serial INTEGER NOT NULL,
-    name TEXT NOT NULL,
-    applied_at INTEGER NOT NULL,
-    UNIQUE (namespace, serial)
-);
-```
-
-**Key changes from previous design:**
-- `application_order`: Tracks the exact order migrations were applied across all namespaces
-- `namespace`: The migration's namespace
-- `serial`: The migration's serial number (was `version`)
-- Composite unique constraint on `(namespace, serial)`
-
-### Dependency Syntax
-
-Dependencies are declared as strings with the following semantics:
-
-| Dependency String | Meaning |
-|------------------|---------|
-| `"auth"` | At least one migration in namespace "auth" must be applied |
-| `"auth:2"` | Migration with serial 2 in namespace "auth" must be applied (exact match) |
-| `"auth:0"` | Migration with serial 0 in namespace "auth" must be applied |
-| `":1"` | **Invalid** - parse error |
-| `"auth:"` | **Invalid** - parse error |
-| `"auth:1:extra"` | **Invalid** - parse error |
-
-### Migration Ordering
-
-Migrations are ordered using two mechanisms:
-
-1. **Within namespace**: Sorted by ascending serial number
-   - Migrations with serials 6, 2, 9 run in order: 2 → 6 → 9
-
-2. **Across namespaces**: Determined by dependency graph (topological sort)
-   - If `app:1` depends on `auth:2`, then `auth:2` must run before `app:1`
-   - Registration order does not affect execution order
-
-```mermaid
-flowchart TB
-    subgraph auth_namespace [auth namespace]
-        auth_v1[auth:1 - CreateUsers]
-        auth_v2[auth:2 - AddRoles]
-    end
-    
-    subgraph app_namespace [app namespace]
-        app_v1[app:1 - CreateOrders<br/>depends on auth:1]
-        app_v2[app:2 - AddUserOrders<br/>depends on auth:2]
-    end
-    
-    auth_v1 --> auth_v2
-    auth_v1 --> app_v1
-    auth_v2 --> app_v2
-    
-    style app_v1 fill:#f9f,stroke:#333
-    style app_v2 fill:#f9f,stroke:#333
-```
-
-### Rollback Behavior
-
-Rollbacks are **time-based**, not dependency-based. When rolling back to a specific migration, all migrations applied after that point are rolled back in reverse application order.
-
-**Example:**
-
-Applied order:
-1. `auth:1`
-2. `auth:2`
-3. `app:5` (depends on `auth:2`)
-4. `logging:1` (no dependencies)
-
-Rollback to `auth:1`:
-1. Roll back `logging:1` (applied 4th)
-2. Roll back `app:5` (applied 3rd)
-3. Roll back `auth:2` (applied 2nd)
-4. Stop - `auth:1` remains
-
-This ensures the database is always in a historically consistent state.
-
-```mermaid
-flowchart LR
-    subgraph Applied [Applied Order]
-        A1[1. auth:1]
-        A2[2. auth:2]
-        A3[3. app:5]
-        A4[4. logging:1]
-    end
-    
-    subgraph Rollback [Rollback to auth:1]
-        R1[1. Roll back logging:1]
-        R2[2. Roll back app:5]
-        R3[3. Roll back auth:2]
-        R4[4. Done - auth:1 remains]
-    end
-    
-    A1 --> A2 --> A3 --> A4
-    R1 --> R2 --> R3 --> R4
-```
-
-### Circular Dependency Detection
-
-The migration runner must detect circular dependencies at registration time or when `migrate_to_latest()` is called. If a cycle is detected, an error is thrown with a clear message:
-
-```
-Circular dependency detected: auth:2 → app:1 → logging:1 → auth:2
-```
-
-## MigrationRunner API Changes
-
-The `MigrationRunner` API is extended with optional namespace parameters:
-
-```vala
-public class MigrationRunner : Object {
-    
-    /**
-     * Creates a new migration runner.
-     */
-    public MigrationRunner(Connection connection, SqlDialect dialect);
-    
-    /**
-     * Registers a migration instance.
-     */
-    public void register_migration(Migration migration);
-    
-    /**
-     * Gets all applied migrations, optionally filtered by namespace.
-     */
-    public Vector<MigrationRecord> get_applied_migrations(string? namespace = null);
-    
-    /**
-     * Gets all pending migrations, optionally filtered by namespace.
-     */
-    public Vector<Migration> get_pending_migrations(string? namespace = null);
-    
-    /**
-     * Applies all pending migrations across all namespaces.
-     * Dependencies are resolved to determine execution order.
-     */
-    public void migrate_to_latest() throws SqlError;
-    
-    /**
-     * Migrates a specific namespace to its latest version.
-     * Dependencies from other namespaces are applied first if needed.
-     */
-    public void migrate_to_latest(string namespace) throws SqlError;
-    
-    /**
-     * Migrates to a specific migration.
-     * All dependencies are applied first if needed.
-     * If rolling back, uses time-based rollback.
-     */
-    public void migrate_to(string namespace, uint64 serial) throws SqlError;
-    
-    /**
-     * Rolls back the last N migrations across all namespaces.
-     */
-    public void rollback(int steps = 1) throws SqlError;
-    
-    /**
-     * Rolls back to a specific migration using time-based rollback.
-     * All migrations applied after the target are rolled back.
-     */
-    public void rollback_to(string namespace, uint64 serial) throws SqlError;
-    
-    /**
-     * Rolls back all migrations across all namespaces.
-     */
-    public void rollback_all() throws SqlError;
-    
-    /**
-     * Gets the current highest serial for a namespace.
-     * Returns 0 if no migrations have been applied.
-     */
-    public uint64 get_current_serial(string namespace) throws SqlError;
-    
-    /**
-     * Validates the dependency graph for cycles.
-     * Throws SqlError if a cycle is detected.
-     */
-    public void validate_dependencies() throws SqlError;
-}
-```
-
-## Example Usage
-
-### Library Migrations
-
-**Auth library** (`auth` namespace):
-
-```vala
-public class Auth_V001_CreateUsers : Migration {
-    public override string migration_namespace { get { return "auth"; } }
-    public override uint64 serial { get { return 1; } }
-    public override string name { get { return "CreateUsers"; } }
-    // No dependencies - this is a base migration
-    
-    public override void up(MigrationBuilder b) throws SqlError {
-        b.create_table("users", t => {
-            t.column<int64?>("id").primary_key().auto_increment();
-            t.column<string>("email").not_null().unique();
-            t.column<string>("password_hash").not_null();
-        });
-    }
-    
-    public override void down(MigrationBuilder b) throws SqlError {
-        b.drop_table("users");
-    }
-}
-
-public class Auth_V002_AddRoles : Migration {
-    public override string migration_namespace { get { return "auth"; } }
-    public override uint64 serial { get { return 2; } }
-    public override string name { get { return "AddRoles"; } }
-    // No external dependencies
-    
-    public override void up(MigrationBuilder b) throws SqlError {
-        b.create_table("roles", t => {
-            t.column<int64?>("id").primary_key().auto_increment();
-            t.column<string>("name").not_null().unique();
-        });
-        
-        b.alter_table("users", t => {
-            t.add_column<int64?>("role_id")
-                .references("roles", "id");
-        });
-    }
-    
-    public override void down(MigrationBuilder b) throws SqlError {
-        b.alter_table("users", t => {
-            t.drop_column("role_id");
-        });
-        b.drop_table("roles");
-    }
-}
-```
-
-**Application** (`app` namespace) with dependencies on auth:
-
-```vala
-public class App_V001_CreateOrders : Migration {
-    public override string migration_namespace { get { return "app"; } }
-    public override uint64 serial { get { return 1; } }
-    public override string name { get { return "CreateOrders"; } }
-    
-    // Requires auth namespace to have users table
-    public override string[] dependencies { get { return {"auth"}; } }
-    
-    public override void up(MigrationBuilder b) throws SqlError {
-        b.create_table("orders", t => {
-            t.column<int64?>("id").primary_key().auto_increment();
-            t.column<int64?>("user_id").not_null()
-                .references("users", "id");  // FK to auth.users
-            t.column<double>("total").not_null();
-            t.column<string>("status").not_null();
-        });
-        
-        b.create_index("idx_orders_user_id")
-            .on_table("orders")
-            .columns("user_id");
-    }
-    
-    public override void down(MigrationBuilder b) throws SqlError {
-        b.drop_index("idx_orders_user_id");
-        b.drop_table("orders");
-    }
-}
-
-public class App_V002_CreateOrderItems : Migration {
-    public override string migration_namespace { get { return "app"; } }
-    public override uint64 serial { get { return 20260320 }; }  // Date-based serial
-    public override string name { get { return "CreateOrderItems"; } }
-    
-    // Requires auth:1 specifically (users table) and app:1 (orders table)
-    public override string[] dependencies { get { return {"auth:1", "app:1"}; } }
-    
-    public override void up(MigrationBuilder b) throws SqlError {
-        b.create_table("order_items", t => {
-            t.column<int64?>("id").primary_key().auto_increment();
-            t.column<int64?>("order_id").not_null()
-                .references("orders", "id");
-            t.column<string>("product_name").not_null();
-            t.column<int>("quantity").not_null();
-            t.column<double>("price").not_null();
-        });
-    }
-    
-    public override void down(MigrationBuilder b) throws SqlError {
-        b.drop_table("order_items");
-    }
-}
-```
-
-### Running Migrations
-
-```vala
-void main() {
-    try {
-        var conn = ConnectionFactory.create_and_open("sqlite:///myapp.db");
-        var dialect = new SqliteDialect();
-        var runner = new MigrationRunner(conn, dialect);
-        
-        // Register migrations from all libraries
-        // Auth library
-        runner.register_migration(new Auth_V001_CreateUsers());
-        runner.register_migration(new Auth_V002_AddRoles());
-        
-        // Application
-        runner.register_migration(new App_V001_CreateOrders());
-        runner.register_migration(new App_V002_CreateOrderItems());
-        
-        // Validate dependencies (detects cycles)
-        runner.validate_dependencies();
-        
-        // Apply all pending migrations in dependency order
-        runner.migrate_to_latest();
-        
-        // Or migrate a specific namespace
-        // runner.migrate_to_latest("app");
-        
-        // Or migrate to a specific point
-        // runner.migrate_to("auth", 2);
-        
-        // Rollback examples
-        // runner.rollback(1);  // Roll back last migration
-        // runner.rollback_to("auth", 1);  // Roll back to auth:1
-        
-        conn.close();
-    } catch (SqlError e) {
-        stderr.printf("Migration error: %s\n", e.message);
-    }
-}
-```
-
-## Error Handling
-
-### Missing Dependency
-
-When a dependency cannot be satisfied:
-
-```
-SqlError: Unsatisfied dependency: app:1 requires auth:2 but no migration 
-with serial 2 is registered in namespace 'auth'
-```
-
-### Circular Dependency
-
-When a cycle is detected:
-
-```
-SqlError: Circular dependency detected: auth:2 → app:1 → logging:1 → auth:2
-```
-
-### Missing Namespace
-
-When a dependency references a namespace with no registered migrations:
-
-```
-SqlError: Unsatisfied dependency: app:1 requires namespace 'auth' but no 
-migrations are registered in that namespace
-```
-
-### Invalid Dependency Syntax
-
-When a dependency string is malformed:
-
-```
-SqlError: Invalid dependency syntax: 'auth:' - expected 'namespace' or 
-'namespace:serial'
-```
-
-## Implementation Checklist
-
-- [ ] Update `Migration` class
-  - [ ] Rename `version` to `serial`
-  - [ ] Change serial type from `int` to `uint64`
-  - [ ] Add abstract `migration_namespace` property
-  - [ ] Add virtual `dependencies` property with empty default
-- [ ] Update `__migrations` table schema
-  - [ ] Add `application_order` column (auto-increment)
-  - [ ] Add `namespace` column
-  - [ ] Rename `version` to `serial`
-  - [ ] Add composite unique constraint on (namespace, serial)
-- [ ] Create `Dependency` parser class
-  - [ ] Parse "namespace" format
-  - [ ] Parse "namespace:serial" format
-  - [ ] Validate syntax and throw on invalid format
-- [ ] Update `MigrationRunner`
-  - [ ] Track application order in migrations table
-  - [ ] Implement dependency graph building
-  - [ ] Implement topological sort for execution order
-  - [ ] Implement circular dependency detection
-  - [ ] Implement time-based rollback using application_order
-  - [ ] Add namespace parameter to existing methods
-  - [ ] Add `validate_dependencies()` method
-  - [ ] Add `get_current_serial(namespace)` method
-- [ ] Update existing migrations in examples
-  - [ ] Add namespace property
-  - [ ] Rename version to serial
-- [ ] Update tests
-  - [ ] Test single namespace migration (backward compat)
-  - [ ] Test multi-namespace migration
-  - [ ] Test cross-namespace dependencies
-  - [ ] Test circular dependency detection
-  - [ ] Test time-based rollback
-  - [ ] Test dependency syntax parsing
-  - [ ] Test error messages
-
-## No Migration Required
-
-**Important**: This is a greenfields project with no existing dependencies or deployed databases. There is no need to migrate the `__migrations` table itself - the new schema will be used directly. Breaking changes to the migration system are acceptable at this stage.
-
-## Future Enhancements
-
-1. **Namespace isolation validation**: Warn if migrations in different namespaces modify the same tables
-2. **Dependency versioning**: Support range-based dependencies like `"auth:>=2"`
-3. **Migration status reporting**: CLI-friendly output showing migration status across namespaces
-4. **Dry run mode**: Show execution plan without applying changes