Basics
This article explains the fundamental concepts of Curiosus.Migrations that you need to understand before diving into specific features.
How the Migration Engine Works
The Curiosus.Migrations library follows a structured process to manage database schema and data changes safely and consistently:
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ 1. Configuration │ │ 2. Infrastructure │ │ 3. Version │
│ Setup │────▶│ Preparation │────▶│ Comparison │
└───────────────────┘ └───────────────────┘ └───────────────────┘
│
▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ 6. Result │ │ 5. Migration │ │ 4. Migration │
│ Handling │◀────│ Execution │◀────│ Planning │
└───────────────────┘ └───────────────────┘ └───────────────────┘
1. Configuration
The migration process begins with configuring the MigrationEngine using the MigrationEngineBuilder. This involves:
- Setting up migration providers (script migrations, code migrations)
- Configuring database connection details
- Defining migration policies for upgrades and downgrades
- Setting the target version (if needed)
- Configuring logging and error handling
var builder = new MigrationEngineBuilder(services)
.UseScriptMigrations().FromDirectory("./Migrations")
.ConfigureForPostgreSql("Host=localhost;Database=mydb;Username=postgres;Password=password")
.UseUpgradeMigrationPolicy(MigrationPolicy.AllAllowed);
2. Infrastructure Preparation
The MigrationEngine ensures the necessary infrastructure exists:
- Database: If the database doesn't exist, it's created according to connection settings
- Migration Journal: A table is created or verified to track applied migrations
3. Version Comparison
The migrator determines what needs to be done by comparing:
- Currently applied migrations (from the journal table)
- Available migrations (from providers)
- Target version (specified in configuration)
4. Migration Planning
Based on the comparison:
- For upgrades: Plans to apply new migrations in ascending version order
- For downgrades: Plans to apply downgrade migrations in descending version order
- Filters migrations according to the configured policy (e.g., only short-running)
5. Migration Execution
Migrations are executed according to the plan:
- Transactions are handled based on migration configuration
- Pre-migrations are executed first (if configured)
- Results and progress are logged
- Journal table is updated as migrations are applied
6. Result Handling
The process concludes by:
- Returning detailed results of the migration operation
- Logging completion status
- Handling any errors that occurred during migration
Versioning System
Curiosus.Migrations provides a flexible versioning system to organize and sequence your database changes.
Version Structure
A migration version consists of:
- Major: Required primary version number (e.g.,
1,20230101) - Minor: Optional secondary version number after a dot (e.g.,
.1,.42)
The complete pattern recognized is: ([\d\_]+)(\.(\d+))*
Version Examples
Valid version formats include:
| Version Format | Example | Comments |
|---|---|---|
| Simple number | 1 | Basic sequential numbering |
| Decimal number | 1.5 | Major.Minor versioning |
| Date-based | 20230101 | Using date as version (YYYYMMDD) |
| Timestamp | 20230101_1430 | Date with time (YYYYMMDD_HHMM) |
| Complex | 20230101_1430.5 | Timestamp with minor version |
Best Practices for Versioning
- Consistency: Choose one versioning scheme and stick with it
- Simplicity: For small projects, simple numbers (
1,2,3) are often sufficient - Timestamp-based: For larger teams, date-based versions (
YYYYMMDD) help avoid conflicts, especialy on mergre requests - Minor versions: Use minor versions to group related migrations that must run in sequence
// Examples of creating version objects
var simpleVersion = new MigrationVersion(1);
var decimalVersion = new MigrationVersion(1, 5);
var dateVersion = new MigrationVersion(20230101);
Target Version Management
The target version controls which migrations should be applied or rolled back.
Setting a Target Version
// Set a specific target version (migrate to exactly version 3)
builder.SetUpTargetVersion(new MigrationVersion(3));
// Migrate to the latest available version (default behavior)
// No need to call SetUpTargetVersion
Migration Direction
The migration direction is not determined automatically. You need to manualy specify what you want - upgrade or downgrade.
Example:
// Configure and build the migration engine
var builder = new MigrationEngineBuilder(services)
.UseScriptMigrations().FromDirectory("path/to/migrations")
.UseCodeMigrations().FromAssembly(Assembly.GetExecutingAssembly())
.ConfigureForPostgreSql("YourConnectionString")
.UseUpgradeMigrationPolicy(MigrationPolicy.AllAllowed)
.UseDowngradeMigrationPolicy(MigrationPolicy.ShortRunningAllowed);
// For upgrading to the latest version
var migrationEngine = builder.Build();
await migrationEngine.UpgradeDatabaseAsync();
// For upgrading to a specific version
builder.SetUpTargetVersion(new MigrationVersion(3));
var migrationEngine = builder.Build();
await migrationEngine.UpgradeDatabaseAsync();
// For downgrading to a specific version
builder.SetUpTargetVersion(new MigrationVersion(1));
var migrationEngine = builder.Build();
await migrationEngine.DowngradeDatabaseAsync();
Migration Types: Short-Running vs Long-Running
Curiosus.Migrations categorizes migrations as either short-running or long-running to help manage resource utilization and scheduling.
Key Differences
| Short-Running Migrations | Long-Running Migrations |
|---|---|
| Schema changes, small data updates | Large data transformations |
| Typically completes in seconds | May run for minutes or hours (even days) |
| Safe to run during application startup | Better scheduled during maintenance windows |
| Default for all migrations | Must be explicitly configured |
Use Cases
Short-Running Example: Adding a new column to a table
-- CURIOSUS: LONG-RUNNING = FALSE
ALTER TABLE users ADD COLUMN email VARCHAR(255);
Long-Running Example: Populating data for millions of rows
-- CURIOSUS: LONG-RUNNING = TRUE
UPDATE users SET email = CONCAT(username, '@example.com');
Configuring Migration Types
For code migrations:
public class PopulateUserEmails : CodeMigration
{
public override MigrationVersion Version => new MigrationVersion(1, 1);
public override string? Comment => "Populate user email addresses";
public PopulateUserEmails()
{
IsLongRunning = true; // Mark as long-running
}
public override async Task UpgradeAsync(DbTransaction? transaction = null,
CancellationToken cancellationToken = default)
{
// Implementation
}
}
For script migrations: Add a special comment at the top of your SQL file:
-- CURIOSUS: LONG-RUNNING = TRUE
Scripts written for Curiosity.Migrations (before the rename to Curiosus.Migrations) use the -- CURIOSITY: prefix.
It is still supported, so existing scripts don't need to be changed.
Migration Policies
Migration policies control which types of migrations are allowed to run in different scenarios.
Available Policies
- AllForbidden (0): No migrations are allowed to run
- ShortRunningAllowed (1): Only short-running migrations can run
- LongRunningAllowed (2): Only long-running migrations can run
- AllAllowed (3): All migrations can run, regardless of type
Configuring Policies
You can set different policies for upgrade and downgrade operations:
var builder = new MigrationEngineBuilder(services)
// Allow all migrations during upgrades
.UseUpgradeMigrationPolicy(MigrationPolicy.AllAllowed)
// Only allow short-running migrations during downgrades
.UseDowngradeMigrationPolicy(MigrationPolicy.ShortRunningAllowed);
Policy Usage Scenarios
-
Development Environment:
.UseUpgradeMigrationPolicy(MigrationPolicy.AllAllowed).UseDowngradeMigrationPolicy(MigrationPolicy.AllAllowed) -
Production Application Startup:
.UseUpgradeMigrationPolicy(MigrationPolicy.ShortRunningAllowed).UseDowngradeMigrationPolicy(MigrationPolicy.AllForbidden) -
Scheduled Maintenance Window:
.UseUpgradeMigrationPolicy(MigrationPolicy.AllAllowed).UseDowngradeMigrationPolicy(MigrationPolicy.ShortRunningAllowed)
Migration Providers
Migration providers determine where migrations come from. You can configure multiple providers to source migrations from different locations.
var builder = new MigrationEngineBuilder(services)
// Add migrations from SQL scripts
.UseScriptMigrations().FromDirectory("./Migrations/Scripts")
// Add migrations from embedded resources
.UseScriptMigrations().FromEmbeddedResources(Assembly.GetExecutingAssembly(), "MyNamespace.Migrations")
// Add migrations from code
.UseCodeMigrations().FromAssembly(Assembly.GetExecutingAssembly());
For more information on available providers and custom implementations, see the Migration Providers article.
Complete Configuration Example
Here's a complete example showing how to configure the migration engine with all major options:
using Curiosus.Migrations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
// Create service collection
var services = new ServiceCollection();
services.AddLogging(configure => configure.AddConsole());
// Configure and build the migration engine
var builder = new MigrationEngineBuilder(services)
// Add script migrations from directory
.UseScriptMigrations()
.FromDirectory("./DatabaseMigrations")
.WithScriptIncorrectNamingAction(ScriptIncorrectNamingAction.ThrowException)
// Add code migrations from assembly
.UseCodeMigrations()
.FromAssembly(typeof(Program).Assembly)
// Configure database connection
.ConfigureForPostgreSql("Host=localhost;Database=myapp;Username=postgres;Password=secret")
// Set migration policies
.UseUpgradeMigrationPolicy(MigrationPolicy.AllAllowed)
.UseDowngradeMigrationPolicy(MigrationPolicy.ShortRunningAllowed)
// Add variables for substitution in scripts
.UseVariable("%SCHEMA%", "public")
.UseVariable("%TABLE_PREFIX%", "app_")
// Configure journal table
.UseJournalTable("migration_history")
// Configure pre-migrations
.UsePreMigrations()
.AddPreMigrationScript("CREATE EXTENSION IF NOT EXISTS pg_trgm;")
// Set target version (optional)
.SetUpTargetVersion(new MigrationVersion(20230101));
// Build the engine
var migrationEngine = builder.Build();
// Execute migrations
var result = await migrationEngine.UpgradeDatabaseAsync();
// Handle results
if (result.IsSuccessful)
{
Console.WriteLine($"Successfully migrated to version {result.CurrentVersion}");
Console.WriteLine($"Applied {result.AppliedMigrations.Count} migrations");
}
else
{
Console.WriteLine($"Migration failed: {result.ErrorMessage}");
}
Troubleshooting Common Issues
Version Parsing Problems
Problem: Migration version cannot be parsed from filename or class.
Solution: Ensure your version format matches the pattern ([\d\_]+)(\.(\d+))*.
Check for common mistakes like using letters in version numbers.
// Correct
new MigrationVersion(1, 5); // 1.5
new MigrationVersion(20230101); // 20230101
// Incorrect (will cause errors)
// new MigrationVersion("v1"); // Cannot use letters
// new MigrationVersion(-1); // Cannot use negative numbers
Missing Dependencies
Problem: Migrations fail with errors about missing dependencies.
Solution: Ensure all referenced migrations exist and are available to the migration engine. Check that your dependency versions are correctly specified.
Transaction Errors
Problem: Migrations fail with transaction-related errors.
Solution: Some operations cannot run within a transaction (like certain DDL statements in some databases).
Set IsTransactionRequired = false for these migrations:
public class CreateIndexMigration : CodeMigration
{
public CreateIndexMigration()
{
IsTransactionRequired = false; // Disable transaction for this migration
}
// Implementation
}
Database Connection Issues
Problem: Migrations fail with connection errors.
Solution:
- Verify connection string parameters
- Ensure the database server is running and accessible
- Check network connectivity and firewall settings
- Verify the user has sufficient permissions
Policy Restrictions
Problem: Migrations don't run due to policy restrictions.
Solution: Check your migration policy settings and ensure they allow the types of migrations you're trying to run:
// Make sure your policy allows the migrations you want to run
builder.UseUpgradeMigrationPolicy(MigrationPolicy.AllAllowed);
For more detailed information on specific features, please refer to the corresponding feature articles.