Header menu logo Migrondi

The IMigrondi interface is the main entry point for the Migrondi library. It coordinates between your migration source and the database to provide a simple API for managing migrations.

Creating a Migrondi Service

Use the MigrondiFactory to create a new service instance:

open Migrondi.Core

let config = {
  MigrondiConfig.Default with
    connection = "Data Source=./migrondi.db"
    migrations = "./migrations"
    driver = MigrondiDriver.Sqlite
}

// Create service with default local file system source
let migrondi = Migrondi.MigrondiFactory(config, ".")

// Optionally, provide a custom logger
open Microsoft.Extensions.Logging
let logger = LoggerFactory.Create(fun builder -> builder.AddConsole() |> ignore)
                    .CreateLogger<IMigrondi>()
let migrondi = Migrondi.MigrondiFactory(config, ".", logger = logger)

Custom Migration Sources

You can provide a custom IMiMigrationSource implementation to use non-local storage. This is done via the source parameter:

open Migrondi.Core.FileSystem

let customSource =
  { new IMiMigrationSource with
      member _.ReadContent(uri) = "..."
      member _.ReadContentAsync(uri, ct) = task { return "..." }
      member _.WriteContent(uri, content) = ()
      member _.WriteContentAsync(uri, content, ct) = task { () }
      member _.ListFiles(locationUri) = Seq.empty
      member _.ListFilesAsync(locationUri, ct) = task { return Seq.empty }
  }

let migrondi = Migrondi.MigrondiFactory(
  config,
  ".",
  ?logger = Some logger,
  source = customSource
)

This allows you to store migrations in:

Initialization

Before using the database, you must initialize it:

// Synchronous
migrondi.Initialize()

// Asynchronous
migrondi.InitializeAsync() |> Async.AwaitTask

This creates the migrations tracking table if it doesn't exist. It's safe to call multiple times - it won't recreate an existing table.

Creating Migrations

Create new migration files with RunNew:

// Synchronous
let migration = migrondi.RunNew(
  "create-users-table",
  upContent = "CREATE TABLE users (id INT, name VARCHAR(255));",
  downContent = "DROP TABLE users;"
)

// Asynchronous
let! migration = migrondi.RunNewAsync(
  "create-users-table",
  upContent = "CREATE TABLE users (id INT, name VARCHAR(255));",
  downContent = "DROP TABLE users;"
)

If you omit upContent and downContent, default templates will be used:

let migration = migrondi.RunNew("create-users-table")

Manual Transactions

By default, Migrondi wraps each migration in a transaction. If you need to manage your own transactions (e.g., for CREATE INDEX CONCURRENTLY or multi-step processes), set manualTransaction to true:

// Create migration without automatic transaction handling
let migration = migrondi.RunNew(
  "create-index-concurrently",
  upContent = "CREATE INDEX CONCURRENTLY idx_users_email ON users(email);",
  downContent = "DROP INDEX CONCURRENTLY IF EXISTS idx_users_email;",
  manualTransaction = true
)

When manualTransaction is true, Migrondi will execute the SQL directly without an enclosing transaction. You are responsible for managing any transaction boundaries in your SQL.

Listing Migrations

Get the status of all migrations:

// Synchronous
let migrations = migrondi.MigrationsList()

// Asynchronous
let! migrations = migrondi.MigrationsListAsync()

Returns a MigrationStatus IReadOnlyList where each item is either:

Example:

for migration in migrations do
  match migration with
  | Applied m ->
      printfn "Applied: %s" m.name
  | Pending m ->
      printfn "Pending: %s" m.name

Applying Migrations

Apply pending migrations with RunUp:

// Apply all pending migrations
migrondi.RunUp()

// Apply specific number of migrations
migrondi.RunUp(amount = 3)

// Asynchronous
migrondi.RunUpAsync() |> Async.AwaitTask
migrondi.RunUpAsync(amount = 3) |> Async.AwaitTask

Returns a MigrationRecord IReadOnlyList of migrations that were applied.

Dry Run

Preview migrations without applying them:

let pending = migrondi.DryRunUp()

for migration in pending do
  printfn "Would apply: %s" migration.name
  printfn "SQL: %s" migration.upContent

Rolling Back Migrations

Revert applied migrations with RunDown:

// Rollback last migration
migrondi.RunDown()

// Rollback specific number of migrations
migrondi.RunDown(amount = 2)

// Asynchronous
migrondi.RunDownAsync() |> Async.AwaitTask

Returns a MigrationRecord IReadOnlyList of migrations that were rolled back.

Dry Run

Preview migrations without rolling them back:

let toRollback = migrondi.DryRunDown()

for migration in toRollback do
  printfn "Would rollback: %s" migration.name
  printfn "SQL: %s" migration.downContent

Checking Migration Status

Check if a specific migration has been applied:

let status = migrondi.ScriptStatus("1708216610033_create-users-table.sql")

match status with
| Applied migration ->
    printfn "Migration applied: %s" migration.name
| Pending migration ->
    printfn "Migration pending: %s" migration.name

Error Handling

Migrondi methods throw specific exceptions:

Example:

try
  migrondi.RunUp()
with
| :? MigrationApplicationFailed as ex ->
  printfn "Failed to apply migration: %s" ex.Message
  // Migration was rolled back automatically
| :? SourceNotFound as ex ->
  printfn "Migration not found: %s" ex.Message
| ex ->
  printfn "Unexpected error: %s" ex.Message

Complete Workflow Example

open Migrondi.Core
open Microsoft.Extensions.Logging

// 1. Create configuration
let config = {
  MigrondiConfig.Default with
    connection = "Data Source=./myapp.db"
    migrations = "./migrations"
}

// 2. Create logger
let logger = LoggerFactory.Create(fun builder -> builder.AddConsole() |> ignore)
                .CreateLogger<IMigrondi>()

// 3. Create service
let migrondi = Migrondi.MigrondiFactory(config, ".", ?logger = Some logger)

// 4. Initialize database
migrondi.Initialize()

// 5. List current status
let migrations = migrondi.MigrationsList()
printfn "Current migrations: %d" migrations.Count

// 6. Dry run to see what would apply
let pending = migrondi.DryRunUp()
printfn "Pending migrations: %d" pending.Count

// 7. Apply migrations
let applied = migrondi.RunUp()
printfn "Applied %d migrations" applied.Count

Async API Reference

All methods have async equivalents:

All async methods support optional CancellationToken:

open System.Threading

let cancellationToken = new CancellationTokenSource(5000).Token
let! migrations = migrondi.MigrationsListAsync(cancellationToken)

Migration Ordering

Migrations are ordered by timestamp, not filename. The V1 format ({timestamp}_{name}.sql) ensures proper ordering:

When creating migrations with RunNew, the current time is used for the timestamp automatically.

Migration Format Versions

Migrondi supports two migration file formats and automatically detects which format your migrations use:

V1 Format (current):

V0 Format (deprecated):

The library handles both formats transparently - you don't need to manually migrate old files. New migrations created programmatically will use the V1 format.

val config: 'a
val migrondi: obj
namespace Microsoft
val logger: obj
val ignore: value: 'T -> unit
val customSource: obj
val task: TaskBuilder
module Seq from Microsoft.FSharp.Collections
val empty<'T> : 'T seq
union case Option.Some: Value: 'T -> Option<'T>
Multiple items
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * obj -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...

--------------------
type Async<'T>
static member Async.AwaitTask: task: System.Threading.Tasks.Task -> Async<unit>
static member Async.AwaitTask: task: System.Threading.Tasks.Task<'T> -> Async<'T>
val migration: obj
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
namespace System
namespace System.Threading

Type something to start searching.