Claude code assisted port of dbmate for Odin apps.
Currently supports PostgreSQL.
- Odin compiler
- PostgreSQL client library (
libpq)- macOS:
brew install libpq - Debian/Ubuntu:
apt install libpq-dev
- macOS:
pg_dump(included with libpq) for schema dump
make build
# or directly
odin build cli -out:bin/omgFor an optimized release build:
make build-releaseSet your database URL via environment variable:
export DATABASE_URL="postgres://user:password@localhost:5432/myapp?sslmode=disable"Or pass it as a flag:
omg --url "postgres://user:password@localhost:5432/myapp?sslmode=disable" <command>omg new create_users
# Created: db/migrations/20260215120000_create_users.sqlEdit the generated file:
-- migrate:up
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- migrate:down
DROP TABLE IF EXISTS users;omg create
# Created database "myapp".omg migrate
# Applying: 20260215120000_create_users.sql
# Applied 1 migration(s).omg up
# Created database "myapp".
# Applying: 20260215120000_create_users.sql
# Applied 1 migration(s).omg status
# [x] 20260215120000_create_users.sql
# [ ] 20260215130000_add_posts.sqlomg rollback
# Rolling back: 20260215120000_create_users.sql
# Rolled back 1 migration.omg down
# Rolling back: 20260215130000_add_posts.sql
# Rolling back: 20260215120000_create_users.sql
# Rolled back 2 migration(s).omg dump
# Wrote: ./db/schema.sqlomg load
# Schema loaded successfully.omg drop
# Dropped database "myapp".omg wait
# Waiting for database ok| Flag | Short | Description | Default |
|---|---|---|---|
--url |
-u |
Database URL (or set DATABASE_URL env) |
|
--migrations-dir |
-d |
Migrations directory | ./db/migrations |
--schema-file |
-s |
Schema file path | ./db/schema.sql |
--migrations-table |
Migrations table name | schema_migrations |
|
--wait-timeout |
Wait timeout in seconds | 60 |
|
--no-dump-schema |
Skip schema dump after migrate/rollback | false |
Migration files use plain SQL with marker comments:
-- migrate:up
<SQL to apply>
-- migrate:down
<SQL to revert>Files are named with a timestamp prefix: YYYYMMDDHHMMSS_description.sql
Some PostgreSQL operations cannot run inside a transaction (e.g., CREATE INDEX CONCURRENTLY,
ALTER TYPE ... ADD VALUE). Add transaction:false to the marker line:
-- migrate:up transaction:false
ALTER TYPE status ADD VALUE 'archived';
-- migrate:down transaction:false
-- PostgreSQL does not support removing enum valuesThe core omg package can be imported directly into your Odin project:
import omg "path/to/odin-migrate"
import "path/to/odin-migrate/pg"
main :: proc() {
config := omg.default_config()
config.database_url = "postgres://user:pass@localhost:5432/myapp?sslmode=disable"
driver := pg.make_driver()
db, err := omg.make_db(config, driver)
if err != nil { /* handle error */ }
defer omg.db_close(&db)
// Create database + run migrations
created, result, up_err := omg.db_up(&db)
if up_err != nil { /* handle error */ }
// Check status
entries, status_err := omg.db_status(&db)
// entries is [dynamic]Status_Entry with .version, .name, .applied
}All library functions return data instead of printing, so the caller controls output.
Use Odin's #load_directory to embed all migration files at compile time,
then use load_directory_migrations to parse them. The compiled binary
contains all migrations — no external files needed at runtime:
import omg "path/to/odin-migrate"
import "path/to/odin-migrate/pg"
// Embed entire migrations directory at compile time
MIGRATIONS :: #load_directory("db/migrations")
main :: proc() {
config := omg.default_config()
config.database_url = "postgres://user:pass@localhost:5432/myapp?sslmode=disable"
config.no_dump_schema = true // no filesystem access at runtime
driver := pg.make_driver()
db, err := omg.make_db(config, driver)
if err != nil { /* handle */ }
defer omg.db_close(&db)
// Parse embedded migrations (filters .sql, sorts by version)
migrations, parse_err := omg.load_directory_migrations(MIGRATIONS)
if parse_err != nil { /* handle */ }
// Create DB + run embedded migrations
created, result, up_err := omg.db_up_with(&db, migrations[:])
if up_err != nil { /* handle */ }
}