# Migration System Architecture
## Document Status
- **Created**: 2026-03-13
- **Status**: Draft
- **Author**: Architecture design session
## Overview
This document describes the architecture for a comprehensive migration system that supports application-defined migrations for the Implexus database. The system provides ordered execution, transaction safety, and migration history tracking.
## Requirements
The migration system must:
1. **Handle initial setup routine** - Support an initial setup/bootstrap migration
2. **Single transaction execution** - Run migration logic within a single transaction
3. **Track migration history** - Keep track of which migrations have been run
4. **Migration interface** - Provide `up()` and `down()` functions for each migration
5. **Ordered execution** - Run pending migrations in the correct order
## Architecture Overview
```mermaid
graph TB
subgraph Application Layer
AM[Application Migrations
User-defined classes]
end
subgraph Migration System
MI[Migration Interface
up/down methods]
MR[MigrationRunner
Discovery and execution]
MS[MigrationStorage
History tracking]
ME[MigrationError
Error domain]
end
subgraph Engine Layer
EE[EmbeddedEngine]
TX[Transaction Support]
end
subgraph Storage Layer
DBM[Dbm Interface]
end
AM --> MI
MI --> MR
MR --> MS
MR --> TX
MS --> DBM
TX --> DBM
EE --> MR
EE --> MS
```
## Components
### 1. Migration Interface
The `Migration` interface defines the contract that all migrations must implement.
**File:** `src/Migrations/Migration.vala`
```vala
namespace Implexus.Migrations {
/**
* Interface for database migrations.
*
* Migrations are application-defined classes that modify the database
* schema or data in a controlled, versioned manner.
*
* Each migration must implement:
* - A unique identifier (version)
* - An up() method to apply the migration
* - A down() method to reverse the migration (optional for irreversible migrations)
*
* Example:
* {{{
* public class CreateUsersTable : Object, Migration {
* public string version { get { return "2026031301"; } }
* public string description { get { return "Create users container"; } }
*
* public void up(Core.Engine engine) throws MigrationError {
* var root = engine.get_root();
* try {
* root.create_container("users");
* } catch (Core.EngineError e) {
* throw new MigrationError.EXECUTION_FAILED(
* "Failed to create users container: %s".printf(e.message)
* );
* }
* }
*
* public void down(Core.Engine engine) throws MigrationError {
* var root = engine.get_root();
* try {
* var users = root.get_child("users");
* if (users != null) {
* users.delete();
* }
* } catch (Core.EngineError e) {
* throw new MigrationError.EXECUTION_FAILED(
* "Failed to delete users container: %s".printf(e.message)
* );
* }
* }
* }
* }}}
*/
public interface Migration : Object {
/**
* Unique identifier for this migration.
*
* Best practice: Use a timestamp format like YYYYMMDDNN where NN is a
* sequence number within the day. This ensures proper ordering.
*
* Examples: "2026031301", "2026031302", "2026031501"
*/
public abstract string version { owned get; }
/**
* Human-readable description of what this migration does.
*/
public abstract string description { owned get; }
/**
* Applies the migration.
*
* This method is called when migrating forward. All operations
* are executed within a single transaction.
*
* @param engine The database engine to operate on
* @throws MigrationError if the migration fails
*/
public abstract void up(Core.Engine engine) throws MigrationError;
/**
* Reverses the migration.
*
* This method is called when rolling back. All operations
* are executed within a single transaction.
*
* Implementations may throw MigrationError.IRREVERSIBLE if the
* migration cannot be safely reversed.
*
* @param engine The database engine to operate on
* @throws MigrationError if the rollback fails
*/
public abstract void down(Core.Engine engine) throws MigrationError;
}
} // namespace Implexus.Migrations
```
### 2. MigrationRunner
The `MigrationRunner` discovers and executes migrations in the correct order.
**File:** `src/Migrations/MigrationRunner.vala`
```vala
namespace Implexus.Migrations {
/**
* Delegate for migration progress notifications.
*/
public delegate void MigrationProgressDelegate(Migration migration, bool is_up);
/**
* Discovers and executes migrations in version order.
*
* The MigrationRunner is responsible for:
* - Registering available migrations
* - Determining which migrations need to run
* - Executing migrations in the correct order within transactions
* - Recording migration history
*
* Example usage:
* {{{
* var runner = new MigrationRunner(engine);
*
* // Register migrations
* runner.register_migration(new CreateUsersTable());
* runner.register_migration(new AddEmailIndex());
* runner.register_migration(new SeedInitialData());
*
* // Run all pending migrations
* try {
* int count = runner.run_pending();
* print("Ran %d migrations\n", count);
* } catch (MigrationError e) {
* stderr.printf("Migration failed: %s\n", e.message);
* }
* }}}
*/
public class MigrationRunner : Object {
// === Private Fields ===
private weak Core.Engine _engine;
private MigrationStorage _storage;
private Invercargill.DataStructures.Dictionary _migrations;
// === Constructors ===
/**
* Creates a new MigrationRunner for the given engine.
*
* @param engine The database engine to migrate
*/
public MigrationRunner(Core.Engine engine) {
_engine = engine;
_storage = new MigrationStorage(engine);
_migrations = new Invercargill.DataStructures.Dictionary();
}
// === Migration Registration ===
/**
* Registers a migration to be available for execution.
*
* Migrations must be registered before calling run_pending() or rollback_to().
*
* @param migration The migration to register
* @throws MigrationError.VERSION_CONFLICT if a migration with the same version exists
*/
public void register_migration(Migration migration) throws MigrationError {
var version = migration.version;
if (_migrations.has(version)) {
throw new MigrationError.VERSION_CONFLICT(
"Migration version %s is already registered".printf(version)
);
}
_migrations.set(version, migration);
}
/**
* Registers multiple migrations at once.
*
* @param migrations Array of migrations to register
* @throws MigrationError if any registration fails
*/
public void register_migrations(Migration[] migrations) throws MigrationError {
foreach (var migration in migrations) {
register_migration(migration);
}
}
// === Status Queries ===
/**
* Gets all migration versions that have been applied.
*
* @return Sorted list of applied migration versions
*/
public Invercargill.Enumerable get_applied_versions() {
return _storage.get_applied_versions();
}
/**
* Gets all migration versions that are registered but not yet applied.
*
* @return Sorted list of pending migration versions
*/
public Invercargill.Enumerable get_pending_versions() {
var pending = new Invercargill.DataStructures.Vector();
var applied = _storage.get_applied_set();
// Get all registered versions and sort them
var all_versions = new Invercargill.DataStructures.Vector();
foreach (var version in _migrations.keys) {
all_versions.add(version);
}
all_versions.sort((a, b) => a.compare(b));
// Filter to only pending
foreach (var version in all_versions) {
if (!applied.contains(version)) {
pending.add(version);
}
}
return pending.as_enumerable();
}
/**
* Checks if a specific migration version has been applied.
*
* @param version The migration version to check
* @return true if the migration has been applied
*/
public bool is_applied(string version) {
return _storage.is_applied(version);
}
/**
* Gets the count of pending migrations.
*
* @return Number of migrations waiting to be applied
*/
public int get_pending_count() {
int count = 0;
var applied = _storage.get_applied_set();
foreach (var version in _migrations.keys) {
if (!applied.contains(version)) {
count++;
}
}
return count;
}
// === Execution ===
/**
* Runs all pending migrations in version order.
*
* Each migration runs in its own transaction. If a migration fails,
* the process stops and the failing migration is rolled back.
*
* @param progress Optional callback for progress notifications
* @return Number of migrations that were run
* @throws MigrationError if any migration fails
*/
public int run_pending(MigrationProgressDelegate? progress = null) throws MigrationError {
var pending = get_pending_versions();
int count = 0;
foreach (var version in pending) {
var migration = _migrations.get(version);
if (migration == null) {
throw new MigrationError.NOT_FOUND(
"Migration %s not found".printf(version)
);
}
run_single((!) migration, true, progress);
count++;
}
return count;
}
/**
* Runs a specific migration.
*
* @param version The version of the migration to run
* @param progress Optional callback for progress notification
* @throws MigrationError if the migration fails or is already applied
*/
public void run_one(string version, MigrationProgressDelegate? progress = null) throws MigrationError {
if (_storage.is_applied(version)) {
throw new MigrationError.ALREADY_APPLIED(
"Migration %s is already applied".printf(version)
);
}
var migration = _migrations.get(version);
if (migration == null) {
throw new MigrationError.NOT_FOUND(
"Migration %s not found".printf(version)
);
}
run_single((!) migration, true, progress);
}
/**
* Rolls back to a specific version.
*
* Runs the down() method of all migrations after the target version,
* in reverse order.
*
* @param target_version The version to roll back to (exclusive - this version remains applied)
* @param progress Optional callback for progress notifications
* @return Number of migrations that were rolled back
* @throws MigrationError if any rollback fails
*/
public int rollback_to(string target_version, MigrationProgressDelegate? progress = null) throws MigrationError {
var applied = _storage.get_applied_versions();
int count = 0;
// Roll back in reverse order
var to_rollback = new Invercargill.DataStructures.Vector();
foreach (var version in applied) {
if (version.compare(target_version) > 0) {
to_rollback.add(version);
}
}
// Sort in reverse order
to_rollback.sort((a, b) => b.compare(a));
foreach (var version in to_rollback) {
var migration = _migrations.get(version);
if (migration == null) {
throw new MigrationError.NOT_FOUND(
"Migration %s not found for rollback".printf(version)
);
}
run_single((!) migration, false, progress);
count++;
}
return count;
}
/**
* Rolls back the most recently applied migration.
*
* @param progress Optional callback for progress notification
* @throws MigrationError if no migrations are applied or rollback fails
*/
public void rollback_last(MigrationProgressDelegate? progress = null) throws MigrationError {
var applied = _storage.get_applied_versions();
string? last_version = null;
foreach (var version in applied) {
last_version = version; // Last one due to sorted order
}
if (last_version == null) {
throw new MigrationError.NO_MIGRATIONS(
"No migrations have been applied"
);
}
var migration = _migrations.get((!) last_version);
if (migration == null) {
throw new MigrationError.NOT_FOUND(
"Migration %s not found for rollback".printf((!) last_version)
);
}
run_single((!) migration, false, progress);
}
// === Internal Methods ===
/**
* Runs a single migration within a transaction.
*/
private void run_single(Migration migration, bool is_up, MigrationProgressDelegate? progress)
throws MigrationError {
var engine = _engine;
if (engine == null) {
throw new MigrationError.ENGINE_ERROR("Engine reference is invalid");
}
// Execute within transaction
try {
var tx = ((!) engine).begin_transaction();
try {
if (is_up) {
migration.up((!) engine);
_storage.record_migration(migration.version, migration.description);
} else {
migration.down((!) engine);
_storage.remove_migration(migration.version);
}
tx.commit();
if (progress != null) {
progress(migration, is_up);
}
} catch (MigrationError e) {
tx.rollback();
throw e;
} catch (Core.EngineError e) {
tx.rollback();
throw new MigrationError.EXECUTION_FAILED(
"Migration %s failed: %s".printf(migration.version, e.message)
);
}
} catch (Core.EngineError e) {
throw new MigrationError.TRANSACTION_ERROR(
"Failed to begin transaction: %s".printf(e.message)
);
}
}
}
} // namespace Implexus.Migrations
```
### 3. MigrationStorage
The `MigrationStorage` class handles persistence of migration history using the existing Dbm infrastructure.
**File:** `src/Migrations/MigrationStorage.vala`
```vala
namespace Implexus.Migrations {
/**
* Stores and retrieves migration history.
*
* Migration history is persisted in the database using a dedicated
* key prefix. This ensures migration state survives application restarts.
*
* Key format: migration:
* Value: Serialized (timestamp, description)
*/
public class MigrationStorage : Object {
// === Constants ===
private const string PREFIX = "migration:";
private const string INDEX_KEY = "migration:index";
// === Private Fields ===
private weak Core.Engine _engine;
private Storage.Dbm? _dbm;
// === Constructors ===
/**
* Creates a new MigrationStorage for the given engine.
*
* @param engine The database engine
*/
public MigrationStorage(Core.Engine engine) {
_engine = engine;
// Get Dbm from engine configuration if available
var config = engine.configuration;
var storage = config.storage;
// Try to get Dbm from BasicStorage
var basic_storage = (storage as Storage.BasicStorage);
if (basic_storage != null) {
_dbm = ((!) basic_storage).dbm;
}
}
// === Recording Migrations ===
/**
* Records that a migration has been applied.
*
* @param version The migration version
* @param description The migration description
* @throws MigrationError if the record cannot be saved
*/
public void record_migration(string version, string description) throws MigrationError {
if (_dbm == null) {
throw new MigrationError.STORAGE_ERROR("Database storage not available");
}
var dbm = (!) _dbm;
string key = PREFIX + version;
// Store migration record
var writer = new Storage.ElementWriter();
writer.write_element(new Invercargill.NativeElement(new DateTime.now_utc().to_unix()));
writer.write_element(new Invercargill.NativeElement(description));
try {
dbm.set(key, writer.to_binary_data());
} catch (Storage.StorageError e) {
throw new MigrationError.STORAGE_ERROR(
"Failed to record migration: %s".printf(e.message)
);
}
// Update index
update_index(dbm, version, true);
}
/**
* Removes a migration record (for rollbacks).
*
* @param version The migration version to remove
* @throws MigrationError if the record cannot be removed
*/
public void remove_migration(string version) throws MigrationError {
if (_dbm == null) {
throw new MigrationError.STORAGE_ERROR("Database storage not available");
}
var dbm = (!) _dbm;
string key = PREFIX + version;
try {
dbm.delete(key);
} catch (Storage.StorageError e) {
// Key may not exist, that's fine
}
// Update index
update_index(dbm, version, false);
}
// === Querying Migrations ===
/**
* Gets all applied migration versions in sorted order.
*
* @return Sorted enumerable of version strings
*/
public Invercargill.Enumerable get_applied_versions() {
var versions = new Invercargill.DataStructures.Vector();
if (_dbm == null) {
return versions.as_enumerable();
}
// Load from index
var index_data = ((!) _dbm).get(INDEX_KEY);
if (index_data == null) {
// Fall back to scanning keys
foreach (var key in ((!) _dbm).keys) {
if (key.has_prefix(PREFIX)) {
var version = key.substring(PREFIX.length);
versions.add(version);
}
}
versions.sort((a, b) => a.compare(b));
return versions.as_enumerable();
}
// Parse index
var reader = new Storage.ElementReader((!) index_data);
try {
var element = reader.read_element();
if (!element.is_null()) {
var array = element.as>();
foreach (var item in array) {
if (!item.is_null()) {
versions.add(item.as());
}
}
}
} catch (Invercargill.ElementError e) {
// Fall back to empty list
}
return versions.as_enumerable();
}
/**
* Gets a set of applied migration versions for efficient lookup.
*
* @return Set of applied version strings
*/
public Invercargill.DataStructures.Set get_applied_set() {
var set = new Invercargill.DataStructures.HashSet();
foreach (var version in get_applied_versions()) {
set.add(version);
}
return set;
}
/**
* Checks if a specific migration has been applied.
*
* @param version The migration version to check
* @return true if the migration has been applied
*/
public bool is_applied(string version) {
if (_dbm == null) {
return false;
}
string key = PREFIX + version;
return ((!) _dbm).has_key(key);
}
/**
* Gets detailed information about an applied migration.
*
* @param version The migration version
* @return Migration record, or null if not applied
*/
public MigrationRecord? get_migration_record(string version) {
if (_dbm == null) {
return null;
}
string key = PREFIX + version;
var data = ((!) _dbm).get(key);
if (data == null) {
return null;
}
var reader = new Storage.ElementReader((!) data);
try {
var timestamp_element = reader.read_element();
var desc_element = reader.read_element();
int64? timestamp = timestamp_element.as();
string description = desc_element.as();
return new MigrationRecord(
version,
description,
timestamp != null ? new DateTime.from_unix_utc((!) timestamp) : null
);
} catch (Invercargill.ElementError e) {
return null;
}
}
// === Private Methods ===
/**
* Updates the migration index.
*/
private void update_index(Storage.Dbm dbm, string version, bool add) {
var versions = new Invercargill.DataStructures.Vector();
// Load existing index
var index_data = dbm.get(INDEX_KEY);
if (index_data != null) {
var reader = new Storage.ElementReader((!) index_data);
try {
var element = reader.read_element();
if (!element.is_null()) {
var array = element.as>();
foreach (var item in array) {
if (!item.is_null()) {
string existing = item.as();
if (!add || existing != version) {
versions.add(existing);
}
}
}
}
} catch (Invercargill.ElementError e) {
// Start fresh
}
}
if (add) {
versions.add(version);
}
// Sort
versions.sort((a, b) => a.compare(b));
// Save index
if (versions.length == 0) {
try {
dbm.delete(INDEX_KEY);
} catch (Storage.StorageError e) {
// Ignore
}
return;
}
var array = new Invercargill.DataStructures.Vector();
foreach (var v in versions) {
array.add(new Invercargill.NativeElement(v));
}
var writer = new Storage.ElementWriter();
writer.write_element(new Invercargill.NativeElement>(array));
try {
dbm.set(INDEX_KEY, writer.to_binary_data());
} catch (Storage.StorageError e) {
// Ignore - not critical
}
}
}
/**
* Represents a record of an applied migration.
*/
public class MigrationRecord : Object {
/**
* The migration version.
*/
public string version { get; construct set; }
/**
* The migration description.
*/
public string description { get; construct set; }
/**
* When the migration was applied, or null if unknown.
*/
public DateTime? applied_at { get; construct set; }
/**
* Creates a new MigrationRecord.
*/
public MigrationRecord(string version, string description, DateTime? applied_at) {
Object(version: version, description: description, applied_at: applied_at);
}
}
} // namespace Implexus.Migrations
```
### 4. MigrationError
Error domain for migration-related errors.
**File:** `src/Migrations/MigrationError.vala`
```vala
namespace Implexus.Migrations {
/**
* Error domain for migration operations.
*/
public errordomain MigrationError {
/**
* The migration was not found.
*/
NOT_FOUND,
/**
* A migration with this version is already registered or applied.
*/
VERSION_CONFLICT,
/**
* The migration is already applied.
*/
ALREADY_APPLIED,
/**
* The migration cannot be reversed.
*/
IRREVERSIBLE,
/**
* The migration execution failed.
*/
EXECUTION_FAILED,
/**
* Transaction error during migration.
*/
TRANSACTION_ERROR,
/**
* Storage error during migration.
*/
STORAGE_ERROR,
/**
* Engine reference error.
*/
ENGINE_ERROR,
/**
* No migrations are available or applied.
*/
NO_MIGRATIONS
}
} // namespace Implexus.Migrations
```
### 5. Bootstrap Migration
A special migration for initial database setup.
**File:** `src/Migrations/BootstrapMigration.vala`
```vala
namespace Implexus.Migrations {
/**
* A bootstrap migration that runs before any other migrations.
*
* This migration is automatically applied when the database is first
* created. It can be extended by applications to perform initial setup
* such as creating required containers or seeding default data.
*
* The bootstrap migration always uses version "0000000000" to ensure
* it runs first.
*
* Example:
* {{{
* public class MyAppBootstrap : BootstrapMigration {
* public override void up(Core.Engine engine) throws MigrationError {
* base.up(engine);
*
* var root = engine.get_root();
* try {
* // Create application-specific containers
* root.create_container("config");
* root.create_container("sessions");
*
* // Seed initial data
* var config = root.get_child("config");
* var settings = config.create_document("settings", "AppConfig");
* settings.set_entity_property("version", new Invercargill.NativeElement("1.0.0"));
* } catch (Core.EngineError e) {
* throw new MigrationError.EXECUTION_FAILED(
* "Bootstrap failed: %s".printf(e.message)
* );
* }
* }
* }
* }}}
*/
public class BootstrapMigration : Object, Migration {
/**
* Bootstrap version - always runs first.
*/
public string version { owned get { return "0000000000"; } }
/**
* Description of the bootstrap migration.
*/
public virtual string description { owned get { return "Initial database setup"; } }
/**
* Performs initial database setup.
*
* The base implementation ensures the root container exists.
* Override to add application-specific setup.
*/
public virtual void up(Core.Engine engine) throws MigrationError {
// Ensure root exists
try {
engine.get_root();
} catch (Core.EngineError e) {
throw new MigrationError.EXECUTION_FAILED(
"Failed to initialize root: %s".printf(e.message)
);
}
}
/**
* Bootstrap migration cannot be reversed.
*/
public void down(Core.Engine engine) throws MigrationError {
throw new MigrationError.IRREVERSIBLE(
"Bootstrap migration cannot be reversed"
);
}
}
} // namespace Implexus.Migrations
```
## File Organization
New files should be placed in the following structure:
```
src/
├── Migration/
│ ├── meson.build
│ ├── Migration.vala # Migration interface
│ ├── MigrationRunner.vala # Migration execution engine
│ ├── MigrationStorage.vala # History persistence
│ ├── MigrationError.vala # Error domain
│ └── BootstrapMigration.vala # Initial setup migration
```
### meson.build
```meson
migration_sources = files(
'Migration.vala',
'MigrationRunner.vala',
'MigrationStorage.vala',
'MigrationError.vala',
'BootstrapMigration.vala'
)
```
The migration sources should be added to the main library sources in `src/meson.build`.
## Integration with Engine
### Engine Interface Extension
Add migration support to the Engine interface:
```vala
// In src/Core/Engine.vala
public interface Engine : Object {
// ... existing methods ...
/**
* Creates a new MigrationRunner for this engine.
*
* @return A new MigrationRunner instance
*/
public abstract Migration.MigrationRunner create_migration_runner();
}
```
### EmbeddedEngine Implementation
```vala
// In src/Engine/EmbeddedEngine.vala
public class EmbeddedEngine : Object, Core.Engine {
// ... existing code ...
/**
* {@inheritDoc}
*/
public Migration.MigrationRunner create_migration_runner() {
return new Migration.MigrationRunner(this);
}
}
```
## API Examples
### Defining Migrations
```vala
// Migration 1: Create users container
public class CreateUsersContainer : Object, Implexus.Migrations.Migration {
public string version { owned get { return "2026031301"; } }
public string description { owned get { return "Create users container"; } }
public void up(Implexus.Core.Engine engine) throws Implexus.Migrations.MigrationError {
try {
var root = engine.get_root();
root.create_container("users");
} catch (Implexus.Core.EngineError e) {
throw new Implexus.Migrations.MigrationError.EXECUTION_FAILED(
"Failed to create users: %s".printf(e.message)
);
}
}
public void down(Implexus.Core.Engine engine) throws Implexus.Migrations.MigrationError {
try {
var root = engine.get_root();
var users = root.get_child("users");
if (users != null) {
users.delete();
}
} catch (Implexus.Core.EngineError e) {
throw new Implexus.Migrations.MigrationError.EXECUTION_FAILED(
"Failed to remove users: %s".printf(e.message)
);
}
}
}
// Migration 2: Add email index
public class AddEmailIndex : Object, Implexus.Migrations.Migration {
public string version { owned get { return "2026031302"; } }
public string description { owned get { return "Add email index for users"; } }
public void up(Implexus.Core.Engine engine) throws Implexus.Migrations.MigrationError {
try {
var root = engine.get_root();
var users = (Implexus.Entities.Container?) root.get_child("users");
if (users != null) {
((!) users).create_index("by_email", "User", "email");
}
} catch (Implexus.Core.EngineError e) {
throw new Implexus.Migrations.MigrationError.EXECUTION_FAILED(
"Failed to create email index: %s".printf(e.message)
);
}
}
public void down(Implexus.Core.Engine engine) throws Implexus.Migrations.MigrationError {
try {
var root = engine.get_root();
var users = root.get_child("users");
if (users != null) {
var index = ((!) users).get_child("by_email");
if (index != null) {
index.delete();
}
}
} catch (Implexus.Core.EngineError e) {
throw new Implexus.Migrations.MigrationError.EXECUTION_FAILED(
"Failed to remove email index: %s".printf(e.message)
);
}
}
}
// Migration 3: Seed initial data (irreversible)
public class SeedInitialData : Object, Implexus.Migrations.Migration {
public string version { owned get { return "2026031303"; } }
public string description { owned get { return "Seed initial configuration data"; } }
public void up(Implexus.Core.Engine engine) throws Implexus.Migrations.MigrationError {
try {
var root = engine.get_root();
var config = root.create_container("config");
var settings = config.create_document("settings", "AppSettings");
settings.set_entity_property("theme", new Invercargill.NativeElement("dark"));
settings.set_entity_property("max_users", new Invercargill.NativeElement(100));
} catch (Implexus.Core.EngineError e) {
throw new Implexus.Migrations.MigrationError.EXECUTION_FAILED(
"Failed to seed data: %s".printf(e.message)
);
}
}
public void down(Implexus.Core.Engine engine) throws Implexus.Migrations.MigrationError {
// This migration is irreversible - data loss is not acceptable
throw new Implexus.Migrations.MigrationError.IRREVERSIBLE(
"Seed data migration cannot be reversed"
);
}
}
```
### Running Migrations
```vala
// Basic migration run
public void run_migrations(Implexus.Core.Engine engine) {
var runner = engine.create_migration_runner();
try {
// Register all migrations
runner.register_migration(new CreateUsersContainer());
runner.register_migration(new AddEmailIndex());
runner.register_migration(new SeedInitialData());
// Run pending migrations
int count = runner.run_pending();
print("Successfully ran %d migrations\n", count);
} catch (Implexus.Migrations.MigrationError e) {
stderr.printf("Migration failed: %s\n", e.message);
}
}
// With progress reporting
public void run_migrations_with_progress(Implexus.Core.Engine engine) {
var runner = engine.create_migration_runner();
try {
runner.register_migrations(new Implexus.Migrations.Migration[] {
new CreateUsersContainer(),
new AddEmailIndex(),
new SeedInitialData()
});
// Run with progress callback
int count = runner.run_pending((migration, is_up) => {
string direction = is_up ? "Up" : "Down";
print("[%s] %s: %s\n", direction, migration.version, migration.description);
});
print("Completed %d migrations\n", count);
} catch (Implexus.Migrations.MigrationError e) {
stderr.printf("Migration failed: %s\n", e.message);
}
}
// Check status before running
public void check_and_run_migrations(Implexus.Core.Engine engine) {
var runner = engine.create_migration_runner();
try {
runner.register_migrations(new Implexus.Migrations.Migration[] {
new CreateUsersContainer(),
new AddEmailIndex(),
new SeedInitialData()
});
// Check status
int pending = runner.get_pending_count();
if (pending == 0) {
print("Database is up to date\n");
return;
}
print("There are %d pending migrations:\n", pending);
foreach (var version in runner.get_pending_versions()) {
print(" - %s\n", version);
}
// Run them
runner.run_pending();
} catch (Implexus.Migrations.MigrationError e) {
stderr.printf("Migration failed: %s\n", e.message);
}
}
```
### Rolling Back Migrations
```vala
// Roll back last migration
public void rollback_last_migration(Implexus.Core.Engine engine) {
var runner = engine.create_migration_runner();
try {
runner.register_migrations(new Implexus.Migrations.Migration[] {
new CreateUsersContainer(),
new AddEmailIndex(),
new SeedInitialData()
});
runner.rollback_last((migration, is_up) => {
print("Rolled back: %s\n", migration.version);
});
} catch (Implexus.Migrations.MigrationError e) {
if (e is Implexus.Migrations.MigrationError.IRREVERSIBLE) {
stderr.printf("Cannot roll back irreversible migration: %s\n", e.message);
} else {
stderr.printf("Rollback failed: %s\n", e.message);
}
}
}
// Roll back to specific version
public void rollback_to_version(Implexus.Core.Engine engine, string target_version) {
var runner = engine.create_migration_runner();
try {
runner.register_migrations(new Implexus.Migrations.Migration[] {
new CreateUsersContainer(),
new AddEmailIndex(),
new SeedInitialData()
});
int count = runner.rollback_to(target_version);
print("Rolled back %d migrations\n", count);
} catch (Implexus.Migrations.MigrationError e) {
stderr.printf("Rollback failed: %s\n", e.message);
}
}
```
### Using Bootstrap Migration
```vala
// Custom bootstrap for application
public class MyAppBootstrap : Implexus.Migrations.BootstrapMigration {
public override string description { owned get { return "MyApp initial setup"; } }
public override void up(Implexus.Core.Engine engine) throws Implexus.Migrations.MigrationError {
// Call base to ensure root exists
base.up(engine);
try {
var root = engine.get_root();
// Create application structure
root.create_container("users");
root.create_container("projects");
root.create_container("settings");
// Create default admin user
var users = (Implexus.Entities.Container) root.get_child("users");
var admin = users.create_document("admin", "User");
admin.set_entity_property("role", new Invercargill.NativeElement("admin"));
admin.set_entity_property("email", new Invercargill.NativeElement("admin@example.com"));
} catch (Implexus.Core.EngineError e) {
throw new Implexus.Migrations.MigrationError.EXECUTION_FAILED(
"Bootstrap failed: %s".printf(e.message)
);
}
}
}
// Using bootstrap with regular migrations
public void initialize_database(Implexus.Core.Engine engine) {
var runner = engine.create_migration_runner();
try {
// Register bootstrap first
runner.register_migration(new MyAppBootstrap());
// Then register regular migrations
runner.register_migration(new CreateUsersContainer());
runner.register_migration(new AddEmailIndex());
// Run all pending (bootstrap will run first due to version ordering)
runner.run_pending();
} catch (Implexus.Migrations.MigrationError e) {
stderr.printf("Database initialization failed: %s\n", e.message);
}
}
```
## Error Handling
### Error Handling Strategy
1. **Transaction Safety**: Each migration runs in its own transaction. If a migration fails, only that migration is rolled back.
2. **Atomic Operations**: Within a migration, all operations are atomic. Either all changes apply, or none do.
3. **Irreversible Migrations**: Migrations that cannot be safely reversed should throw `MigrationError.IRREVERSIBLE` in their `down()` method.
4. **Progress Preservation**: Successfully applied migrations are recorded even if later migrations fail. The system can be resumed after fixing the failing migration.
### Error Handling Flow
```mermaid
sequenceDiagram
participant App as Application
participant Runner as MigrationRunner
participant TX as Transaction
participant Storage as MigrationStorage
participant DB as Database
App->>Runner: run_pending()
loop For each pending migration
Runner->>TX: begin_transaction()
Runner->>App: migration.up()
alt Success
App->>DB: Apply changes
Runner->>Storage: record_migration()
Storage->>DB: Save record
Runner->>TX: commit()
Runner->>App: progress callback
else Failure
App-->>Runner: throws EngineError
Runner->>TX: rollback()
Runner-->>App: throws MigrationError
end
end
```
### Error Recovery
```vala
public void run_with_recovery(Implexus.Core.Engine engine) {
var runner = engine.create_migration_runner();
try {
runner.register_migrations(get_all_migrations());
runner.run_pending();
} catch (Implexus.Migrations.MigrationError e) {
// Check what was applied before failure
var applied = runner.get_applied_versions();
var pending = runner.get_pending_versions();
stderr.printf("Migration failed: %s\n", e.message);
stderr.printf("Applied: %d, Pending: %d\n",
applied.length, pending.length);
// Log for manual recovery
log_migration_failure(e, applied, pending);
// Depending on error type, may be able to retry
if (e is Implexus.Migrations.MigrationError.TRANSACTION_ERROR) {
stderr.printf("Transaction error - may be retried\n");
} else if (e is Implexus.Migrations.MigrationError.EXECUTION_FAILED) {
stderr.printf("Execution failed - check migration code\n");
}
}
}
```
## Storage Schema
### Key Schema
| Key Pattern | Description |
|-------------|-------------|
| `migration:` | Record of an applied migration |
| `migration:index` | Sorted list of all applied migration versions |
### Data Format
**migration:**
```
Element[] {
int64 timestamp, // Unix timestamp when applied
string description // Migration description
}
```
**migration:index**
```
Element[] {
string[] versions // Sorted array of applied version strings
}
```
## Design Decisions
1. **Version Format**: Using string-based versions (e.g., "2026031301") instead of integers allows for:
- Natural ordering by timestamp
- Human-readable identification
- No collision issues in distributed development
2. **One Transaction Per Migration**: Each migration runs in its own transaction rather than batching all migrations in one transaction. This:
- Allows partial progress recovery
- Prevents long-running transactions
- Matches common migration tool patterns
3. **Separate MigrationStorage**: Storage logic is separated from the runner to:
- Allow alternative storage implementations
- Enable testing with mock storage
- Follow single responsibility principle
4. **Bootstrap as Regular Migration**: The bootstrap migration is implemented as a regular migration with a special version number rather than a separate mechanism. This:
- Ensures it's tracked in history
- Allows rollback (if reversible)
- Uses the same infrastructure
5. **No Auto-Discovery**: Migrations must be explicitly registered rather than auto-discovered. This:
- Gives applications full control
- Avoids reflection/metadata complexity
- Works well with Vala's compilation model
## Future Considerations
1. **Migration Validation**: Could add a `validate()` method to migrations for pre-flight checks.
2. **Dry Run Mode**: Could add a `--dry-run` option that shows what would happen without executing.
3. **Migration Dependencies**: Could add support for migrations that depend on other migrations.
4. **Conditional Migrations**: Could add support for migrations that only run under certain conditions.
5. **Migration Groups**: Could add support for grouping migrations into named sets that can be applied together.