V2.2: Migrations
Migrations are Ruby files that provide syntax for generating Data Definition Language for the ROM::SQL
adapter. They are generated by the Hanami command line, and provided by the Sequel library.
Migration filenames are defined in snake case and prefaced by a numeric timestamp of when they were created. This allows for roughly sequential execution without the likelihood of collisions from sequential integers when multiple people are creating them at the same time.
They are located in the config/db/migrate
directory.
$ tree config/db/migrate
config/db/migrate
├── 20240717170227_create_posts.rb
└── 20240717170318_add_published_at_to_posts.rb
Direction
Migration files are bi-directional, they define schema changes forward and backward, or up
and down
in Sequel’s syntax. This is important, in case your migration changes cause a problem you will want to roll them back as quickly as possible, and requiring a fresh migration to do this may take too much time.
ROM::SQL.migration do
change do
create_table :users do
primary_key :id
foreign_key :account_id, :accounts, on_delete: :cascade, null: false
column :given_name, String, null: false
column :family_name, String, null: false
column :email, "citext", null: false
end
end
end
In the example above, we used the change
method to define our migration. This is written in the style of an up migration, and the down version is inferred by Sequel.
Sometimes, this cannot be automatically inferred. You will need to provide explicit up
and down
definitions in this case.
ROM::SQL.migration do
up do
alter_table :users do
add_unique_constraint [:email], name: :users_email_uniq
end
end
down do
alter_table :users do
drop_constraint :users_email_uniq
end
end
end
Transactions
The majority of migrations are run within a transaction, so that DDL errors trigger a rollback and don’t leave you in a partial state. But certain operations don’t support running inside a transaction.
Within a migration block, no_transaction
tells the migrator to run the migration without first starting a transaction.
ROM::SQL.migration do
no_transaction
up do
alter_table :users do
add_index :email, concurrently: true
end
end
down do
alter_table :users do
drop_index :email, concurrently: true
end
end
end
Syntax
Sequel migration syntax provides some flexibility in how you may choose to represent your table columns.
create_table :users do
# column method, explicit SQL type
column :email, "varchar(255)", null: false
# column method, inferred SQL type: varchar(255)
column :email, String, null: false
# helper method, no inference, SQL type: text
text :email, null: false
# Ruby type method, inferred SQL type: varchar(255)
String :email, null: false
end
Sequel also provides Ruby syntax for defining logical pieces, such as constraints
create_table :users do
primary_key :id
column :name, String, null: false
constraint(:name_min_length) { char_length(name) > 2 }
end
Read more at Sequel: Schema modification methods
Ruby syntax is not a requirement, however, and sometimes what you are doing is not easy or possible to express in Sequel’s DSL. In those cases, execute
acts as an escape-hatch into raw SQL. The drawback of execute
is that it cannot infer how to reverse your changes, so you will have to provide explicit up and down migrations.
ROM::SQL.migration do
up do
execute <<~SQL
CREATE TRIGGER posts_tsvector_update()
BEFORE INSERT OR UPDATE ON public.posts
FOR EACH ROW
WHEN (
OLD.title IS DISTINCT FROM NEW.title OR
OLD.content IS DISTINCT FROM NEW.content
)
EXECUTE PROCEDURE tsvector_update_trigger(search_tsvector, 'public.english', title, content)
SQL
end
down do
execute "DROP TRIGGER posts_tsvector_update() ON public.posts"
end
end
In this example, we’re storing vector information for PostgreSQL’s text search in a column, automatically built by a trigger.
Database Structure
When adding migrations to your project, it’s useful to maintain a record of what the current state of your database structure was at the time.
This serves three purposes:
- Spot-check your schema changes to ensure you’re doing what you intended
- Provide a simple way to connect the structure of your database to a specific code change
- A blank-slate for setting up a development or test database without running all migrations sequentially
Hanami provides this in the form of config/db/structure.sql
. The choice of using plain SQL to reflect your DB structure gives you maximum flexibility in using the most powerful features of your database.