Skip to content

Commit 166fa0b

Browse files
committed
feat: add rollback CLI
1 parent 4c4b320 commit 166fa0b

3 files changed

Lines changed: 157 additions & 0 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ Execute scripts as one-off container jobs.
2727
docker-compose run --no-deps -it init task db:init host=$PG_HOST port=$PG_PORT db=$PG_DB user=$PG_USER password=$PG_PASSWORD
2828
```
2929

30+
```bash
31+
# Roll back the last migration
32+
docker-compose run --no-deps -it init task db:down host=$PG_HOST port=$PG_PORT db=$PG_DB user=$PG_USER password=$PG_PASSWORD
33+
```
34+
35+
```bash
36+
# Roll back to a specific migration version (rolls down until current <= target)
37+
docker-compose run --no-deps -it init task db:down target=20240101000000 host=$PG_HOST port=$PG_PORT db=$PG_DB user=$PG_USER password=$PG_PASSWORD
38+
```
39+
40+
```bash
41+
# Inspect migration status / current version
42+
docker-compose run --no-deps -it init task db:status host=$PG_HOST port=$PG_PORT db=$PG_DB user=$PG_USER password=$PG_PASSWORD
43+
docker-compose run --no-deps -it init task db:version host=$PG_HOST port=$PG_PORT db=$PG_DB user=$PG_USER password=$PG_PASSWORD
44+
```
45+
3046
```bash
3147
# Dump PostgreSQL database to local filesystem
3248
docker-compose run --no-deps -it init task db:dump host=$PG_HOST port=$PG_PORT db=$PG_DB user=$PG_USER password=$PG_PASSWORD
@@ -238,6 +254,20 @@ By default, the backup will take place at midnight every day.
238254
* `user`: Defaults to `PG_USER` || `"postgres"`
239255
* `password`: Defaults to `PG_PASS` || `""`
240256

257+
- `db:down`: Roll back the last migration, or down to a specific version.
258+
* `db`: Defaults `PG_DB` || `"postgres"`
259+
* `host`: Defaults to `PG_HOST` || `"localhost"`
260+
* `port`: Defaults to `PG_PORT` || `5432`
261+
* `user`: Defaults to `PG_USER` || `"postgres"`
262+
* `password`: Defaults to `PG_PASS` || `""`
263+
* `target`: Optional version (e.g. `20240101000000`). When supplied, rolls down repeatedly until the current version is `<= target`. Use `target=0` to revert all migrations.
264+
265+
- `db:redo`: Re-run the last migration (rolls back, then re-applies).
266+
267+
- `db:status`: Show migration status for each migration.
268+
269+
- `db:version`: Print the current database migration version.
270+
241271
- `db:clean`: Clean PostgreSQL Database by deleting old records.
242272
* `db`: Defaults `PG_DB` || `"postgres"`
243273
* `host`: Defaults to `PG_HOST` || `"localhost"`

src/sam.cr

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,59 @@ namespace "db" do
4949
puts "PostgreSQL database '#{args["db"]}' restored successfully from dump file '#{args["path"]}'" if ret
5050
end
5151

52+
desc "Roll back the last migration, or down to `target=<version>`"
53+
task "down" do |_, args|
54+
arguments = {
55+
pg_host: (args["host"]? || PlaceOS::PG_HOST).to_s,
56+
pg_port: (args["port"]? || PlaceOS::PG_PORT).to_i,
57+
pg_db: (args["db"]? || PlaceOS::PG_DB).to_s,
58+
pg_user: (args["user"]? || PlaceOS::PG_USER).try &.to_s,
59+
pg_password: (args["password"]? || PlaceOS::PG_PASS).to_s,
60+
target: args["target"]?.try(&.to_s.to_i64),
61+
}
62+
63+
PlaceOS::Tasks.pg_migrate_down(**arguments)
64+
end
65+
66+
desc "Re-run the last migration"
67+
task "redo" do |_, args|
68+
arguments = {
69+
pg_host: (args["host"]? || PlaceOS::PG_HOST).to_s,
70+
pg_port: (args["port"]? || PlaceOS::PG_PORT).to_i,
71+
pg_db: (args["db"]? || PlaceOS::PG_DB).to_s,
72+
pg_user: (args["user"]? || PlaceOS::PG_USER).try &.to_s,
73+
pg_password: (args["password"]? || PlaceOS::PG_PASS).to_s,
74+
}
75+
76+
PlaceOS::Tasks.pg_migrate_redo(**arguments)
77+
end
78+
79+
desc "Show migration status for the PostgreSQL DB"
80+
task "status" do |_, args|
81+
arguments = {
82+
pg_host: (args["host"]? || PlaceOS::PG_HOST).to_s,
83+
pg_port: (args["port"]? || PlaceOS::PG_PORT).to_i,
84+
pg_db: (args["db"]? || PlaceOS::PG_DB).to_s,
85+
pg_user: (args["user"]? || PlaceOS::PG_USER).try &.to_s,
86+
pg_password: (args["password"]? || PlaceOS::PG_PASS).to_s,
87+
}
88+
89+
PlaceOS::Tasks.pg_migration_status(**arguments)
90+
end
91+
92+
desc "Print the current PostgreSQL DB migration version"
93+
task "version" do |_, args|
94+
arguments = {
95+
pg_host: (args["host"]? || PlaceOS::PG_HOST).to_s,
96+
pg_port: (args["port"]? || PlaceOS::PG_PORT).to_i,
97+
pg_db: (args["db"]? || PlaceOS::PG_DB).to_s,
98+
pg_user: (args["user"]? || PlaceOS::PG_USER).try &.to_s,
99+
pg_password: (args["password"]? || PlaceOS::PG_PASS).to_s,
100+
}
101+
102+
PlaceOS::Tasks.pg_database_version(**arguments)
103+
end
104+
52105
desc "Clean PostgreSQL Database by deleting old records"
53106
task "clean" do |_, args|
54107
arguments = {

src/tasks/database.cr

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,80 @@ module PlaceOS::Tasks::Database
2626
end
2727
end
2828

29+
private def configure_micrate_connection(pg_db, pg_host, pg_port, pg_user, pg_password)
30+
pg_user = "postgres" if pg_user.nil?
31+
pg_password = "" if pg_password.nil?
32+
Micrate::DB.connection_url = "postgresql://#{pg_user}:#{pg_password}@#{pg_host}:#{pg_port}/#{pg_db}"
33+
end
34+
35+
def pg_migrate_down(
36+
pg_db : String,
37+
pg_host : String,
38+
pg_port : Int32,
39+
pg_user : String? = nil,
40+
pg_password : String? = nil,
41+
target : Int64? = nil,
42+
)
43+
configure_micrate_connection(pg_db, pg_host, pg_port, pg_user, pg_password)
44+
Micrate::DB.connect do |db|
45+
if target.nil?
46+
Micrate.down(db)
47+
else
48+
# Public API only exposes one-step `down`; loop until at or below target.
49+
loop do
50+
current = Micrate.dbversion(db)
51+
break if current <= target
52+
Micrate.down(db)
53+
break if Micrate.dbversion(db) == current
54+
end
55+
end
56+
end
57+
end
58+
59+
def pg_migrate_redo(
60+
pg_db : String,
61+
pg_host : String,
62+
pg_port : Int32,
63+
pg_user : String? = nil,
64+
pg_password : String? = nil,
65+
)
66+
configure_micrate_connection(pg_db, pg_host, pg_port, pg_user, pg_password)
67+
Micrate::DB.connect do |db|
68+
Micrate.redo(db)
69+
end
70+
end
71+
72+
def pg_migration_status(
73+
pg_db : String,
74+
pg_host : String,
75+
pg_port : Int32,
76+
pg_user : String? = nil,
77+
pg_password : String? = nil,
78+
)
79+
configure_micrate_connection(pg_db, pg_host, pg_port, pg_user, pg_password)
80+
Micrate::DB.connect do |db|
81+
puts "Applied At Migration"
82+
puts "======================================="
83+
Micrate.migration_status(db).each do |migration, migrated_at|
84+
ts = migrated_at.nil? ? "Pending" : migrated_at.to_s
85+
puts "%-24s -- %s" % [ts, migration.name]
86+
end
87+
end
88+
end
89+
90+
def pg_database_version(
91+
pg_db : String,
92+
pg_host : String,
93+
pg_port : Int32,
94+
pg_user : String? = nil,
95+
pg_password : String? = nil,
96+
)
97+
configure_micrate_connection(pg_db, pg_host, pg_port, pg_user, pg_password)
98+
Micrate::DB.connect do |db|
99+
puts Micrate.dbversion(db)
100+
end
101+
end
102+
29103
def drop_pg_tables(
30104
pg_db : String,
31105
pg_host : String,

0 commit comments

Comments
 (0)