Hummingbird is a way to write SQL migrations in SQL, and provide some minimal tooling around running these migrations against a DB.
DSLs that abstract away the differences in databases can be nice to work with, but they often cater to something resembling the lowest common denominator amongst those databases.
Hummingbird will look for a hummingbird.yml
in the directory passed
to Hummingbird::Configuration.new
. Hummingbird will also look for a
user-specific .hummingbird.yml
in that directory. The user-specific
configuration file will take precidence over the configuration in the
hummingbird.yml
. Both the configuration file name and path, and the
user-specific configuration name and path can be overridden by passing
:config_file => config_name
or :user_config_file => user_config_name
to Hummingbird::Configuration.new
.
For example:
config = Hummingbird::Configuration.new(
Dir.getwd,
config_file: 'path/to/config.yml',
user_config_file: '/absolute/path/to/user_config.yml'
)
The above example will cause Hummingbird to look for the configuration
file in File.join(Dir.getwd, 'path/to/config.yml')
, and to look for
the user's configuration file in /absolute/path/to/user_config.yml
.
Both of these configuration files are YAML files with the following format:
---
basedir: 'sql'
planfile: 'application.plan'
migrations_dir: 'migrations'
migrations_table: 'application_migrations'
connection_string: 'sqlite://db/database.db'
basedir
: Theplanfile
, andmigrations_dir
settings are relative to this directory, which is relative to the current working directory.planfile
: Described more below. This determines the order in which migration files are run.migrations_dir
: This is the name of the directory that will contain all of the migrations. All files in this directory are considered to be migration files, and Hummingbird will recurse into any and all subdirectories starting here.connection_string
: This is a Sequel compatible connection string for connecting to the database.
The above example configures Hummingbird to look in the file
sql/application.plan
for the list of migrations to run, in the
directory sql/migrations
for all of the migration files, use the
table named application_migrations
within the database, and to
connect to the database using the Sequel connection string 'sqlite://db/database.db'
.
Hummingbird is capable of bootstrapping itself into the database to be
managed as long as the first migration creates the table named by the
migrations_table
configuration option. This table will need to have
a migration_name
column of type TEXT
(or similar data type to
handle the maximum file path relative to migrations_dir
), and a
run_on
column of type INTEGER
.
The following is an example for defining the migrations_table
for
SQLite3, and PostgreSQL:
CREATE TABLE hummingbird_migrations (
migration_name TEXT PRIMARY KEY,
run_on INTEGER
);
The plan file contains the names of the migration files to be run, one per line, in the order that they should be run.
Given the following plan file and the example configuration from above:
bootstrap.sql
stored_procedures/foo.sql
tables/bar.sql
Hummingbird would attempt to run the files
sql/migrations/bootstrap.sql
,
sql/migrations/stored_procedures/foo.sql
,
sql/migrations/tables/bar.sql
in exactly this order.
Right now, only just enough is written to enable someone to write their own rake task, or other glue code to actually migrate their database using Hummingbird.
This isn't really a great way to go about it, but you can do the
following in your Rakefile
at least until the rest of the glue is
included in Hummingbird itself:
desc 'Migrate the database'
task "migrate" do
require 'hummingbird'
config = Hummingbird::Configuration.new(Dir.getwd)
plan = Hummingbird::Plan.new(config.planfile, config.migrations_dir)
db = Hummingbird::Database.new(config.connection_string, config.migrations_table)
unplanned_files = plan.files_missing_from_plan
fail "Found migration files not listed in #{config.planfile}: #{unplanned_files.join(', ')}" unless unplanned_files.empty?
missing_files = plan.files_missing_from_migration_dir
fail "Found planned migration files not in migrations directory: #{missing_files.join(', ')}" unless missing_files.empty?
migrations_already_run = db.already_run_migrations
migrations_to_run = plan.migrations_to_be_run(migrations_already_run)
puts "#{plan.planned_files.count} migrations planned; #{migrations_already_run.count} already run; #{migrations_to_run.count} to run"
migrations_to_run.each do |migration|
puts "Running migration: #{migration[:migration_name]}"
db.run_migration(migration[:migration_name], migration[:sql])
end
end