a database layer insipred by caqti and ecto
1# Migrations 2 3Migrations version your database schema, allowing you to evolve it over time in a controlled way. 4 5## Defining Migrations 6 7Each migration has a version number, name, and up/down operations: 8 9```ocaml 10open Repodb 11 12let migration_001 = Migration.migration 13 ~version:1L 14 ~name:"create_users" 15 ~up:[ 16 Migration.create_table "users" [ 17 Migration.column "id" "BIGSERIAL" ~primary_key:true ~nullable:false; 18 Migration.column "name" "VARCHAR(255)" ~nullable:false; 19 Migration.column "email" "VARCHAR(255)" ~nullable:false ~unique:true; 20 Migration.column "inserted_at" "TIMESTAMPTZ" ~nullable:false ~default:"NOW()"; 21 Migration.column "updated_at" "TIMESTAMPTZ" ~nullable:false ~default:"NOW()"; 22 ]; 23 ] 24 ~down:[ 25 Migration.drop_table "users"; 26 ] 27``` 28 29## Migration Operations 30 31### Create Table 32 33```ocaml 34Migration.create_table "posts" [ 35 Migration.column "id" "BIGSERIAL" ~primary_key:true ~nullable:false; 36 Migration.column "author_id" "BIGINT" ~nullable:false 37 ~references:("users", "id"); 38 Migration.column "title" "VARCHAR(255)" ~nullable:false; 39 Migration.column "body" "TEXT"; 40 Migration.column "published" "BOOLEAN" ~default:"FALSE"; 41] 42``` 43 44### Drop Table 45 46```ocaml 47Migration.drop_table "posts" 48``` 49 50### Alter Table 51 52```ocaml 53Migration.alter_table "users" [ 54 Migration.add_column ( 55 Migration.column "bio" "TEXT" 56 ); 57 Migration.drop_column "legacy_field"; 58 Migration.rename_column ~from:"name" ~to_:"full_name"; 59] 60``` 61 62### Create Index 63 64```ocaml 65(* Simple index *) 66Migration.create_index "posts" ["author_id"] 67 68(* Named index *) 69Migration.create_index "posts" ["author_id"] ~name:"idx_posts_author" 70 71(* Unique index *) 72Migration.create_index "users" ["email"] ~unique:true 73``` 74 75### Drop Index 76 77```ocaml 78Migration.drop_index "idx_posts_author" 79``` 80 81### Raw SQL 82 83For operations not covered by the DSL: 84 85```ocaml 86Migration.execute "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"" 87``` 88 89## Column Options 90 91```ocaml 92Migration.column "field_name" "SQL_TYPE" 93 ~nullable:false (* NOT NULL constraint *) 94 ~primary_key:true (* PRIMARY KEY constraint *) 95 ~unique:true (* UNIQUE constraint *) 96 ~default:"expression" (* DEFAULT value/expression *) 97 ~references:("table", "column") (* FOREIGN KEY reference *) 98``` 99 100## Common Patterns 101 102### Timestamps Helper 103 104```ocaml 105let migration_001 = Migration.migration 106 ~version:1L 107 ~name:"create_users" 108 ~up:[ 109 Migration.create_table "users" ( 110 [ 111 Migration.column "id" "BIGSERIAL" ~primary_key:true ~nullable:false; 112 Migration.column "name" "VARCHAR(255)" ~nullable:false; 113 ] 114 @ Migration.timestamps () (* adds inserted_at and updated_at *) 115 ); 116 ] 117 ~down:[Migration.drop_table "users"] 118``` 119 120### Foreign Keys 121 122```ocaml 123Migration.column "author_id" "BIGINT" ~nullable:false 124 ~references:("users", "id") 125``` 126 127For more control over foreign key behavior, use raw SQL: 128 129```ocaml 130Migration.execute 131 "ALTER TABLE posts ADD CONSTRAINT fk_posts_author 132 FOREIGN KEY (author_id) REFERENCES users(id) 133 ON DELETE CASCADE ON UPDATE CASCADE" 134``` 135 136## Generating SQL 137 138Convert operations to SQL strings: 139 140```ocaml 141let sql_statements = Migration.generate_up_sql migration_001 142(* [ 143 "CREATE TABLE users ( 144 id BIGSERIAL PRIMARY KEY NOT NULL, 145 name VARCHAR(255) NOT NULL, 146 email VARCHAR(255) NOT NULL UNIQUE, 147 ... 148 )"; 149 ] *) 150 151let rollback_sql = Migration.generate_down_sql migration_001 152(* ["DROP TABLE users"] *) 153``` 154 155## Schema Migrations Table 156 157repodb tracks applied migrations in a `schema_migrations` table: 158 159```ocaml 160(* SQL to create the tracking table *) 161let create_table_sql = Migration.create_schema_migrations_sql 162 163(* The table has: version (BIGINT PK), name (VARCHAR), inserted_at (TIMESTAMPTZ) *) 164``` 165 166## Planning Migrations 167 168### Get Pending Migrations 169 170```ocaml 171let all_migrations = [migration_001; migration_002; migration_003] 172let applied_versions = [1L; 2L] (* from schema_migrations table *) 173 174let pending = Migration.pending_migrations ~applied_versions all_migrations 175(* [migration_003] *) 176``` 177 178### Plan Migration Run 179 180```ocaml 181(* Plan to run all pending *) 182let actions = Migration.plan_migrate ~applied_versions ~target:None all_migrations 183 184(* Plan to run up to specific version *) 185let actions = Migration.plan_migrate ~applied_versions ~target:(Some 5L) all_migrations 186``` 187 188### Plan Rollback 189 190```ocaml 191(* Rollback last migration *) 192let actions = Migration.plan_rollback ~applied_versions ~step:None all_migrations 193 194(* Rollback last 3 migrations *) 195let actions = Migration.plan_rollback ~applied_versions ~step:(Some 3) all_migrations 196``` 197 198### Execute Plan 199 200```ocaml 201let sql_list = Migration.actions_to_sql actions 202(* Execute each SQL statement in order *) 203``` 204 205## Migration Status 206 207```ocaml 208let status = Migration.format_status ~applied_versions ~migrations:all_migrations 209(* 210 Applied migrations: 211 [✓] 1: create_users 212 [✓] 2: create_posts 213 214 Pending migrations: 215 [ ] 3: add_comments 216 [ ] 4: add_tags 217*) 218``` 219 220## Complete Migration Runner Example 221 222```ocaml 223let run_migrations conn migrations = 224 (* Ensure schema_migrations table exists *) 225 let create_table_sql = Migration.create_schema_migrations_sql in 226 match Repo.exec conn create_table_sql ~params:[||] with 227 | Error _ -> () (* Table might already exist *) 228 | Ok () -> (); 229 230 (* Get applied versions *) 231 match Repo.query conn Migration.get_applied_versions_sql ~params:[||] with 232 | Error e -> Error e 233 | Ok rows -> 234 let applied_versions = List.map (fun row -> 235 Driver.row_int64 row 0 236 ) rows in 237 238 (* Plan and run *) 239 let actions = Migration.plan_migrate 240 ~applied_versions 241 ~target:None 242 migrations 243 in 244 245 Repo.transaction conn (fun conn -> 246 let rec run_actions = function 247 | [] -> Ok () 248 | action :: rest -> 249 let sqls = Migration.action_to_sql action in 250 let rec run_sqls = function 251 | [] -> run_actions rest 252 | sql :: more -> 253 match Repo.exec conn sql ~params:[||] with 254 | Error e -> Error e 255 | Ok () -> run_sqls more 256 in 257 run_sqls sqls 258 in 259 run_actions actions 260 ) 261 262let rollback_last conn migrations = 263 match Repo.query conn Migration.get_applied_versions_sql ~params:[||] with 264 | Error e -> Error e 265 | Ok rows -> 266 let applied_versions = List.map (fun row -> 267 Driver.row_int64 row 0 268 ) rows in 269 270 let actions = Migration.plan_rollback 271 ~applied_versions 272 ~step:(Some 1) 273 migrations 274 in 275 276 Repo.transaction conn (fun conn -> 277 let sqls = Migration.actions_to_sql actions in 278 let rec run = function 279 | [] -> Ok () 280 | sql :: rest -> 281 match Repo.exec conn sql ~params:[||] with 282 | Error e -> Error e 283 | Ok () -> run rest 284 in 285 run sqls 286 ) 287``` 288 289## Best Practices 290 2911. **Version numbers**: Use timestamps or sequential integers. Timestamps prevent conflicts in teams. 292 2932. **Always provide down**: Every migration should be reversible when possible. 294 2953. **Keep migrations small**: One logical change per migration. 296 2974. **Don't modify old migrations**: Once applied to production, create new migrations for changes. 298 2995. **Test rollbacks**: Verify `down` migrations work correctly. 300 3016. **Data migrations**: For data changes, use `Migration.execute` with INSERT/UPDATE statements. 302 303## Next Steps 304 305- [Schemas](schemas.md) - Defining schemas in code 306- [Repo](repo.md) - Database operations 307- [Getting Started](getting-started.md) - Basic setup