From 6e75db75af2a023a3648fb943566b91a4057ae02 Mon Sep 17 00:00:00 2001 From: Vlad Faust Date: Mon, 15 Oct 2018 16:52:51 +0300 Subject: [PATCH] feat: add error command Closes #17 --- README.md | 16 +++++++++ spec/migrate/migrator_with_errors_spec.cr | 43 +++++++++++++++++++++++ spec/migrations_with_errors/1.sql | 11 ++++++ spec/migrations_with_errors/2.sql | 1 + src/migrate/migration.cr | 14 +++++++- src/migrate/migration/error.cr | 7 ++++ src/migrate/migrator.cr | 26 +++++++++----- 7 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 spec/migrate/migrator_with_errors_spec.cr create mode 100644 spec/migrations_with_errors/1.sql create mode 100644 spec/migrations_with_errors/2.sql create mode 100644 src/migrate/migration/error.cr diff --git a/README.md b/README.md index 70e0c21..4877b4c 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,22 @@ migrator.to_latest migrator.current_version # => 10 ``` +### Errors + +A special command `+migrate error` is available. It raises `Migrate::Migration::Error` when a specific migration file is run. A error can be either top-level or direction-specific. This is useful to point out irreversible migrations: + +```sql +-- +migrate up +CREATE TABLE foo; + +-- +migrate down +-- +migrate error Could not migrate down from this point +``` + +```sql +-- +migrate error Could not run this migration file at all +``` + ### [Cakefile](https://github.com/axvm/cake) Note that `Cakefile` doesn't support task arguments (that means that `Migrator#to` will not be available). Also see [cake-bake](https://github.com/vladfaust/cake-bake.cr) for baking Cakefiles (this could be helpful in Docker deployments). diff --git a/spec/migrate/migrator_with_errors_spec.cr b/spec/migrate/migrator_with_errors_spec.cr new file mode 100644 index 0000000..584daf4 --- /dev/null +++ b/spec/migrate/migrator_with_errors_spec.cr @@ -0,0 +1,43 @@ +require "../spec_helper" + +db = DB.open(ENV["DATABASE_URL"]) + +{% for table in %w(foo bar baz) %} + def {{table.id}}_exists?(db) + db.scalar("SELECT COUNT(*) FROM {{table.id}}").as(Int64) + rescue + false + end +{% end %} + +describe "Migrate::Migrator with errors" do + drop_db + + migrator = Migrate::Migrator.new( + db, + nil, + File.join("spec", "migrations_with_errors") + ) + + describe "direction-specific errors" do + it do + migrator.up + + expect_raises Migrate::Migration::Error do + migrator.down + end + + migrator.current_version.should eq 1 + end + end + + describe "top-level errors" do + it do + expect_raises Migrate::Migration::Error do + migrator.up + end + + migrator.current_version.should eq 1 + end + end +end diff --git a/spec/migrations_with_errors/1.sql b/spec/migrations_with_errors/1.sql new file mode 100644 index 0000000..f92c3d7 --- /dev/null +++ b/spec/migrations_with_errors/1.sql @@ -0,0 +1,11 @@ +-- +migrate up +CREATE TABLE foo ( + id SERIAL PRIMARY KEY, + content TEXT NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX foo_content_index ON foo (content); + +-- +migrate down +-- +migrate error Could not migrate down from version 1 diff --git a/spec/migrations_with_errors/2.sql b/spec/migrations_with_errors/2.sql new file mode 100644 index 0000000..e2fb523 --- /dev/null +++ b/spec/migrations_with_errors/2.sql @@ -0,0 +1 @@ +-- +migrate error Nope diff --git a/src/migrate/migration.cr b/src/migrate/migration.cr index 6efd056..840df19 100644 --- a/src/migrate/migration.cr +++ b/src/migrate/migration.cr @@ -1,9 +1,12 @@ +require "./migration/*" + module Migrate # :nodoc: struct Migration CMD_PREFIX = "-- +migrate" getter queries_up = Array(String).new, queries_down = Array(String).new + getter error : Exception | Nil, error_up : Exception | Nil, error_down : Exception | Nil def initialize(lines : Iterator) direction : Direction? = nil @@ -12,12 +15,21 @@ module Migrate lines.each do |line| if line.starts_with?(CMD_PREFIX) - cmd = line[CMD_PREFIX.size..-1].strip.downcase + cmd = /^#{Regex.escape(CMD_PREFIX)} (?\w+)(?:\s.+)?/.match(line).not_nil!["cmd"].strip.downcase + case cmd when "up" direction = Direction::Up when "down" direction = Direction::Down + when "error" + message = /^#{Regex.escape(CMD_PREFIX)} error (?.+)$/.match(line).try &.["message"] + + case direction + when Direction::Up then @error_up ||= Error.new(message) + when Direction::Down then @error_down ||= Error.new(message) + else @error ||= Error.new(message) + end else raise "Unknown command #{cmd}!" end diff --git a/src/migrate/migration/error.cr b/src/migrate/migration/error.cr new file mode 100644 index 0000000..df89b70 --- /dev/null +++ b/src/migrate/migration/error.cr @@ -0,0 +1,7 @@ +module Migrate + struct Migration + # Could be raised after `error` command. See README for examples. + class Error < Exception + end + end +end diff --git a/src/migrate/migrator.cr b/src/migrate/migrator.cr index dca2390..7808086 100644 --- a/src/migrate/migrator.cr +++ b/src/migrate/migrator.cr @@ -129,20 +129,30 @@ module Migrate applied_files.reverse! if direction == Direction::Down - migrations = applied_files.map do |file_path| - migration = Migration.new(File.join(@dir, file_path)) + migrations = applied_files.map { |path| Migration.new(File.join(@dir, path)) } + + migrations.each do |migration| + if error = migration.error + raise error + end case direction when Direction::Up - migration.queries_up + if error = migration.error_up + raise error + end + + queries = migration.queries_up when Direction::Down - migration.queries_down - end.not_nil! - end + if error = migration.error_down + raise error + end + + queries = migration.queries_down + end - migrations.each do |migration| @db.transaction do |tx| - migration.each do |query| + queries.not_nil!.each do |query| @logger.try &.debug(query) tx.connection.exec(query) end