Overview
What is F# DbUp By Example?
F# DbUp By Example is a code-first tutorial series teaching experienced developers how to manage PostgreSQL schema evolution using DbUp from F#. Through 75+ heavily annotated, self-contained examples, you achieve 95% coverage of DbUp patterns—from writing your first SQL migration script to advanced deployment strategies, idempotency guards, and assembly-embedded script discovery.
This tutorial assumes you are an experienced developer familiar with F#, PostgreSQL, and relational database concepts. If you are new to F#, start with foundational F# tutorials first.
Why By Example?
Philosophy: Show the code first, run it second, understand through direct interaction.
Traditional tutorials explain concepts then show code. By-example tutorials reverse this: every example is a working, runnable code snippet with inline annotations showing exactly what happens at each step—SQL executed, DbUp journal state, migration results, and common pitfalls.
Target Audience: Experienced developers who:
- Already know F# fundamentals (modules, pipelines, computation expressions)
- Understand relational databases and SQL DDL
- Prefer learning through working code rather than narrative explanations
- Want comprehensive reference material covering 95% of production migration patterns
Not For: Developers new to F# or databases. This tutorial moves quickly and assumes foundational knowledge.
What Does 95% Coverage Mean?
95% coverage means the depth and breadth of DbUp features needed for production work, not toy examples.
Included in 95% Coverage
- Script Authoring: SQL DDL conventions, naming patterns, sequential numbering, IF NOT EXISTS guards
- DeployChanges Builder: PostgresqlDatabase, WithScriptsEmbeddedInAssembly, LogToConsole, Build, PerformUpgrade
- Journal Table: SchemaVersions tracking, idempotency guarantees, migration history queries
- Connection Setup: NpgsqlConnection in F#, connection string patterns, PostgreSQL-specific types
- Schema Operations: CREATE TABLE, ALTER TABLE, DROP with safety guards, column types, constraints
- Indexes: Single-column, composite, unique, partial indexes
- Constraints: Foreign keys, CHECK constraints, UNIQUE constraints, NOT NULL with defaults
- Data Types: UUID primary keys, TIMESTAMPTZ defaults, DECIMAL precision, BYTEA, BOOLEAN, ENUM via CHECK
- Relationships: One-to-many, many-to-many junction tables, cascade behavior
- Data Migrations: Seed data scripts, backfill patterns, safe column renames
- Assembly Integration: Script discovery from embedded resources, ordering guarantees
- Error Handling: Checking Successful property, ErrorScript details, rollback patterns
- Advanced Patterns: Multiple script sources, filtered scripts, pre-deployment scripts
Excluded from 95% (the remaining 5%)
- Rare Adapters: MySQL, SQLite, SQL Server specific behaviors outside core patterns
- Custom Journal: Implementing ISchemaVersionJournal from scratch
- Internal Mechanics: DbUp source connection pooling, adapter internals
- Legacy API: Deprecated pre-4.x DbUp builder patterns
Tutorial Structure
75+ Examples Across Three Levels
Sequential numbering: Examples 1-75+ (unified reference system)
Distribution:
- Beginner (Examples 1-30): 0-40% coverage — Script authoring, DeployChanges builder, PostgreSQL setup, basic DDL patterns, schema operations
- Intermediate (Examples 31-60): 40-75% coverage — Advanced DDL, data migrations, multiple script sources, error handling, deployment strategies
- Advanced (Examples 61-75+): 75-95% coverage — Custom filters, journal queries, CI/CD integration, idempotency patterns, multi-schema deployments
Rationale: This distribution mirrors real production adoption: most teams need beginner and intermediate patterns daily; advanced patterns arise for complex multi-tenant or CI/CD scenarios.
Five-Part Example Format
Every example follows a mandatory five-part structure:
Part 1: Brief Explanation (2-3 sentences)
Answers:
- What is this concept or pattern?
- Why does it matter in production migrations?
- When should you use it?
Example:
Example 7: Console Logging with WithConsoleLogger
WithConsoleLogger attaches a console sink to the DbUp upgrade engine, printing each script name and execution status during migration runs. Visibility into which scripts executed—and in what order—is essential for debugging migration failures in CI/CD pipelines and local development.
Part 2: Mermaid Diagram (when appropriate)
Included when (~35% of examples):
- DbUp execution flow involves multiple stages
- Relationships between SQL files and the journal table are non-obvious
- Assembly embedding and script discovery need illustration
- Error handling branches require visualization
Skipped when:
- Simple single-file SQL DDL operations
- Linear ALTER TABLE statements
- Trivial index additions
Diagram requirements:
- Use color-blind friendly palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
- Vertical orientation (mobile-first)
- Clear labels on all nodes and edges
- Comment syntax:
%%(NOT%%{ }%%)
Part 3: Heavily Annotated Code
Core requirement: Every significant line must have an inline comment.
Comment annotations use -- => for SQL and // => for F#:
let result =
DeployChanges.To // => Entry point for the fluent builder
.PostgresqlDatabase(connStr) // => Targets PostgreSQL with Npgsql driver
.WithScriptsEmbeddedInAssembly(asm) // => Discovers *.sql embedded resources
.LogToConsole() // => Prints script names and results to stdout
.Build() // => Returns UpgradeEngine instance
.PerformUpgrade() // => Executes pending scripts; returns DatabaseUpgradeResult
// => result.Successful is true when all scripts ran without error
// => result.Scripts contains list of ScriptName strings that were executedRequired annotations:
- Builder steps: Show what each fluent call configures
- SQL results: Document which columns, constraints, or indexes the statement creates
- DbUp state: Show journal table changes after execution
- Error cases: Document Successful/ErrorScript properties and when they occur
- Expected outputs: Show console output with
-- =>prefix in SQL examples
Part 4: Key Takeaway (1-2 sentences)
Purpose: Distill the core insight to its essence.
Must highlight:
- The most important pattern or concept
- When to apply this in production
- Common pitfalls to avoid
Example:
Key Takeaway: Always check
result.Successfulbefore proceeding with application startup; on failure,result.Error.Messagegives the exact SQL error andresult.ErrorScriptidentifies the offending script.
Part 5: Why It Matters (50-100 words)
Purpose: Contextualize the example within production concerns.
Covers:
- Production impact of ignoring this pattern
- How it prevents common migration failures
- Relationship to broader database reliability practices
Self-Containment Rules
Critical requirement: Examples must be copy-paste-runnable within their chapter scope.
Beginner Level Self-Containment
Rule: Each SQL example is completely standalone; each F# snippet is runnable with the stated dependencies.
Requirements:
- Complete SQL DDL statements with no external table references (or explicit dependency noted)
- Full F# snippets including open statements and let bindings
- No references to previous examples
- Runnable against a live PostgreSQL instance with DbUp 4.x NuGet packages
Intermediate and Advanced Level Self-Containment
Rule: Examples assume beginner concepts but include all necessary code.
Allowed assumptions:
- Reader understands DeployChanges builder and PerformUpgrade from beginner examples
- Reader can create a PostgreSQL connection string from environment variables
- Reader knows F# module syntax and basic pattern matching
How to Use This Tutorial
Prerequisites
Before starting, ensure you have:
- .NET 8+ SDK installed
- PostgreSQL 14+ running (local or Docker)
- Basic F# knowledge (modules, functions, pipelines)
- Basic SQL knowledge (DDL: CREATE, ALTER, DROP)
- DbUp NuGet package:
dbup-postgresql(4.x or 5.x)
Running Examples
SQL examples run directly against PostgreSQL:
psql $DATABASE_URL -f 001-create-users.sqlF# examples run as part of your application startup or test setup:
// Add to project file: <PackageReference Include="dbup-postgresql" Version="5.*" />
open DbUp
let connStr = System.Environment.GetEnvironmentVariable("DATABASE_URL")
let result =
DeployChanges.To
.PostgresqlDatabase(connStr)
.WithScriptsEmbeddedInAssembly(System.Reflection.Assembly.GetExecutingAssembly())
.LogToConsole()
.Build()
.PerformUpgrade()Learning Path
For F# developers adopting DbUp:
- Work through beginner examples (1-30) — learn script authoring and builder setup
- Deep dive intermediate (31-60) — master complex DDL and data migration patterns
- Reference advanced (61-75+) — learn CI/CD integration and deployment strategies
For developers migrating from Flyway or Liquibase:
- Read this overview to understand DbUp philosophy (SQL-first, no XML/YAML)
- Jump to intermediate examples (31-60) to see how DbUp handles common scenarios
- Use beginner examples as syntax reference for PostgreSQL-specific DDL
Coverage Progression
As you progress through examples, you achieve cumulative coverage:
- After Beginner (Example 30): 40% — Can manage basic schema evolution for production tables
- After Intermediate (Example 60): 75% — Can handle most production migration scenarios
- After Advanced (Example 75+): 95% — Expert-level DbUp mastery for complex deployments
Code Annotation Philosophy
Every example uses educational annotations to show exactly what happens:
-- Example 1: Creates the users table with UUID primary key
CREATE TABLE users (
-- => UUID type requires the pgcrypto extension or PostgreSQL 13+ gen_random_uuid()
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- => VARCHAR without length limit stores up to 1 GB; add CHECK constraint for practical limits
username VARCHAR NOT NULL,
-- => TIMESTAMPTZ stores timezone-aware instants; prefer over TIMESTAMP for distributed systems
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- => After execution: users table exists in schemaversions journal as 001-create-users.sqlAnnotations show:
- Column type choices and their tradeoffs
- Default value behavior and when defaults apply
- Constraint enforcement and what violations look like
- DbUp journal state after script execution
- Common gotchas and safe alternatives
Quality Standards
Every example in this tutorial meets these standards:
- Self-contained: Copy-paste-runnable within chapter scope
- Annotated: Every significant line has an inline comment using
-- =>(SQL) or// =>(F#) - Production-relevant: Real-world patterns drawn from actual F#/PostgreSQL projects
- Accessible: Color-blind friendly diagrams, clear structure
Next Steps
Ready to start? Begin with:
- Beginner Examples (1-30) — Script authoring and DeployChanges builder fundamentals
Examples by Level
Beginner (Examples 1–30)
- Example 1: First DbUp Migration Script (SQL File)
- Example 2: DeployChanges Builder Setup
- Example 3: PostgresqlDatabase Target
- Example 4: Embedded Resource Scripts
- Example 5: Script Naming Conventions (Sequential Numbering)
- Example 6: SchemaVersions Journal Table
- Example 7: Console Logging with WithConsoleLogger
- Example 8: Creating Tables
- Example 9: Adding Columns
- Example 10: Adding Indexes
- Example 11: Adding Foreign Keys
- Example 12: Adding Unique Constraints
- Example 13: Adding NOT NULL with Defaults
- Example 14: Running Migrations with PerformUpgrade
- Example 15: Checking Migration Results (Successful/ErrorScript)
- Example 16: NpgsqlConnection Setup in F
- Example 17: Multiple Scripts in Sequence
- Example 18: Dropping Columns Safely
- Example 19: Dropping Tables Safely
- Example 20: UUID Primary Keys in PostgreSQL
- Example 21: Timestamp Columns with Defaults
- Example 22: Enum Types via SQL
- Example 23: CHECK Constraints
- Example 24: Composite Indexes
- Example 25: Junction Tables (Many-to-Many)
- Example 26: Seed Data in Migration Scripts
- Example 27: IF NOT EXISTS Guards
- Example 28: Cascade Delete Foreign Keys
- Example 29: Script Discovery from Assembly
- Example 30: Migration Execution Order
Intermediate (Examples 31–60)
- Example 31: Script Filtering with IScriptFilter
- Example 32: Custom Journal Table Name
- Example 33: Transaction Per Script Strategy
- Example 34: Single Transaction Strategy
- Example 35: No Transaction Strategy
- Example 36: Variables in SQL Scripts
- Example 37: Script Preprocessing
- Example 38: Always-Run Scripts
- Example 39: Script Naming Groups and Ordering
- Example 40: F# Type-Safe Migration Wrapper
- Example 41: Data Migration with INSERT...SELECT
- Example 42: Seed Data Migration Pattern
- Example 43: Foreign Key with ON UPDATE CASCADE
- Example 44: Composite Primary Keys
- Example 45: Partial Indexes
- Example 46: Full-Text Search Indexes (PostgreSQL)
- Example 47: Creating Views
- Example 48: Creating Materialized Views
- Example 49: Trigger Functions
- Example 50: Stored Procedures
- Example 51: Conditional Migration Logic
- Example 52: Batch Data Migration Pattern
- Example 53: Migration Testing with xUnit
- Example 54: Test Database Setup with Testcontainers
- Example 55: JSON/JSONB Columns
- Example 56: Array Columns (PostgreSQL)
- Example 57: GIN Index for JSONB
- Example 58: Table Partitioning
- Example 59: Generated Columns
- Example 60: Migration Error Handling and Recovery
Advanced (Examples 61–85)
- Example 61: Custom IScriptProvider
- Example 62: Custom IScriptExecutor
- Example 63: Custom IJournal Implementation
- Example 64: Zero-Downtime Column Addition
- Example 65: Zero-Downtime Column Removal (3-Phase)
- Example 66: Large Table Migration with Batched Updates
- Example 67: Online Index Creation (CONCURRENTLY)
- Example 68: Data Backfill Pattern
- Example 69: DbUp in CI/CD Pipeline
- Example 70: Dry-Run with LogScriptOutput
- Example 71: Multi-Database Migrations
- Example 72: Schema Comparison Pattern
- Example 73: Migration Rollback Testing
- Example 74: Blue-Green Deployment Migrations
- Example 75: Feature Flag Migration Pattern
- Example 76: DbUp with EF Core Hybrid
- Example 77: Multi-Tenant Schema Migration
- Example 78: Migration with pgcrypto
- Example 79: Audit Trail Table
- Example 80: Soft Delete Schema Pattern
- Example 81: Custom Preprocessor
- Example 82: Migration Performance Benchmarking
- Example 83: Schema Drift Detection
- Example 84: Production Migration Checklist
- Example 85: Migration Monitoring and Alerting
Last updated March 26, 2026