sequent 6.0.1 → 7.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/db/sequent_schema.rb +5 -0
  3. data/lib/sequent/configuration.rb +64 -13
  4. data/lib/sequent/core/aggregate_repository.rb +2 -2
  5. data/lib/sequent/core/aggregate_snapshotter.rb +4 -0
  6. data/lib/sequent/core/base_command_handler.rb +5 -0
  7. data/lib/sequent/core/core.rb +1 -1
  8. data/lib/sequent/core/event.rb +2 -2
  9. data/lib/sequent/core/event_record.rb +1 -0
  10. data/lib/sequent/core/event_store.rb +20 -16
  11. data/lib/sequent/core/helpers/attribute_support.rb +7 -7
  12. data/lib/sequent/core/helpers/message_handler.rb +10 -11
  13. data/lib/sequent/core/helpers/message_router.rb +13 -7
  14. data/lib/sequent/core/persistors/active_record_persistor.rb +4 -0
  15. data/lib/sequent/core/persistors/persistor.rb +5 -0
  16. data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +140 -133
  17. data/lib/sequent/core/projector.rb +4 -0
  18. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +2 -1
  19. data/lib/sequent/core/workflow.rb +4 -0
  20. data/lib/sequent/dry_run/dry_run.rb +4 -0
  21. data/lib/sequent/dry_run/read_only_replay_optimized_postgres_persistor.rb +26 -0
  22. data/lib/sequent/dry_run/view_schema.rb +36 -0
  23. data/lib/sequent/generator/template_project/db/sequent_schema.rb +1 -0
  24. data/lib/sequent/migrations/errors.rb +12 -0
  25. data/lib/sequent/migrations/migrations.rb +0 -1
  26. data/lib/sequent/migrations/planner.rb +11 -7
  27. data/lib/sequent/migrations/versions.rb +82 -0
  28. data/lib/sequent/migrations/view_schema.rb +101 -58
  29. data/lib/sequent/rake/migration_tasks.rb +89 -6
  30. data/lib/sequent/sequent.rb +4 -11
  31. data/lib/sequent/support/database.rb +3 -11
  32. data/lib/sequent/support.rb +0 -2
  33. data/lib/sequent/util/util.rb +1 -0
  34. data/lib/sequent/util/web/clear_cache.rb +19 -0
  35. data/lib/sequent.rb +1 -0
  36. data/lib/version.rb +1 -1
  37. metadata +20 -30
  38. data/lib/sequent/core/helpers/message_dispatcher.rb +0 -20
  39. data/lib/sequent/migrations/migrate_events.rb +0 -67
  40. data/lib/sequent/support/view_projection.rb +0 -61
  41. data/lib/sequent/support/view_schema.rb +0 -24
@@ -41,13 +41,17 @@ module Sequent
41
41
  # end
42
42
  #
43
43
  # In this case it is wise to create an index on InvoiceRecord on the aggregate_id and recipient_id
44
- # like you would in the database.
44
+ # attributes like you would in the database. Note that previous versions of this class supported
45
+ # multi-column indexes. These are now split into multiple single-column indexes and the results of
46
+ # each index is combined using set-intersection. This reduces the amount of memory used and makes
47
+ # it possible to use an index in more cases (whenever an indexed attribute is present in the where
48
+ # clause the index will be used, so not all attributes need to be present).
45
49
  #
46
50
  # Example:
47
51
  #
48
52
  # ReplayOptimizedPostgresPersistor.new(
49
53
  # 50,
50
- # {InvoiceRecord => [[:aggregate_id, :recipient_id]]}
54
+ # {InvoiceRecord => [:aggregate_id, :recipient_id]}
51
55
  # )
52
56
  class ReplayOptimizedPostgresPersistor
53
57
  include Persistor
@@ -56,108 +60,106 @@ module Sequent
56
60
  attr_reader :record_store
57
61
  attr_accessor :insert_with_csv_size
58
62
 
59
- def self.struct_cache
60
- @struct_cache ||= {}
63
+ # We create a struct on the fly to represent an in-memory record.
64
+ #
65
+ # Since the replay happens in memory we implement the ==, eql? and hash methods
66
+ # to point to the same object. A record is the same if and only if they point to
67
+ # the same object. These methods are necessary since we use Set instead of [].
68
+ #
69
+ # Also basing equality on object identity is more consistent with ActiveRecord,
70
+ # which is the implementation used during normal (non-optimized) replay.
71
+ module InMemoryStruct
72
+ def ==(other)
73
+ equal?(other)
74
+ end
75
+ def eql?(other)
76
+ equal?(other)
77
+ end
78
+ def hash
79
+ object_id.hash
80
+ end
61
81
  end
62
82
 
63
- module InitStruct
64
- def set_values(values)
65
- values.each do |k, v|
66
- self[k] = v
83
+ def struct_cache
84
+ @struct_cache ||= Hash.new do |hash, record_class|
85
+ struct_class = Struct.new(*record_class.column_names.map(&:to_sym), keyword_init: true) do
86
+ include InMemoryStruct
67
87
  end
68
- self
88
+ hash[record_class] = struct_class
69
89
  end
70
90
  end
71
91
 
72
92
  class Index
93
+ attr_reader :indexed_columns
94
+
73
95
  def initialize(indexed_columns)
74
- @indexed_columns = Hash.new do |hash, record_class|
75
- hash[record_class] = if record_class.column_names.include? 'aggregate_id'
76
- ['aggregate_id']
77
- else
78
- []
79
- end
96
+ @indexed_columns = indexed_columns.to_set
97
+ @indexes = @indexed_columns.to_h do |field|
98
+ [field, {}]
99
+ end
100
+ @reverse_indexes = @indexed_columns.to_h do |field|
101
+ [field, {}.compare_by_identity]
80
102
  end
81
-
82
- @indexed_columns = @indexed_columns.merge(
83
- indexed_columns.reduce({}) do |memo, (key, ics)|
84
- memo.merge({key => ics.map { |c| c.map(&:to_s) }})
85
- end,
86
- )
87
-
88
- @index = {}
89
- @reverse_index = {}
90
103
  end
91
104
 
92
- def add(record_class, record)
93
- return unless indexed?(record_class)
94
-
95
- get_keys(record_class, record).each do |key|
96
- @index[key.hash] = [] unless @index.key? key.hash
97
- @index[key.hash] << record
98
-
99
- @reverse_index[record.object_id.hash] = [] unless @reverse_index.key? record.object_id.hash
100
- @reverse_index[record.object_id.hash] << key.hash
105
+ def add(record)
106
+ @indexes.map do |field, index|
107
+ key = Persistors.normalize_symbols(record[field]).freeze
108
+ records = index[key] || (index[key] = Set.new.compare_by_identity)
109
+ records << record
110
+ @reverse_indexes[field][record] = key
101
111
  end
102
112
  end
103
113
 
104
- def remove(record_class, record)
105
- return unless indexed?(record_class)
106
-
107
- keys = @reverse_index.delete(record.object_id.hash) { [] }
108
-
109
- return unless keys.any?
110
-
111
- keys.each do |key|
112
- @index[key].delete(record)
113
- @index.delete(key) if @index[key].count == 0
114
+ def remove(record)
115
+ @indexes.map do |field, index|
116
+ key = @reverse_indexes[field].delete(record)
117
+ remaining = index[key]&.delete(record)
118
+ index.delete(key) if remaining&.empty?
114
119
  end
115
120
  end
116
121
 
117
- def update(record_class, record)
118
- remove(record_class, record)
119
- add(record_class, record)
122
+ def update(record)
123
+ remove(record)
124
+ add(record)
120
125
  end
121
126
 
122
- def find(record_class, where_clause)
123
- key = [record_class.name]
124
- get_index(record_class, where_clause).each do |field|
125
- key << field
126
- key << where_clause.stringify_keys[field]
127
+ def find(normalized_where_clause)
128
+ record_sets = normalized_where_clause.map do |(field, expected_value)|
129
+ if expected_value.is_a?(Array)
130
+ expected_value.reduce(Set.new.compare_by_identity) do |memo, value|
131
+ key = Persistors.normalize_symbols(value)
132
+ memo.merge(@indexes[field][key] || [])
133
+ end
134
+ else
135
+ key = Persistors.normalize_symbols(expected_value)
136
+ @indexes[field][key] || Set.new.compare_by_identity
137
+ end
138
+ end
139
+
140
+ smallest, *rest = record_sets.sort_by(&:size)
141
+ return smallest.to_a if smallest.empty? || rest.empty?
142
+
143
+ smallest.select do |record|
144
+ rest.all? { |x| x.include? record }
127
145
  end
128
- @index[key.hash] || []
129
146
  end
130
147
 
131
148
  def clear
132
- @index = {}
133
- @reverse_index = {}
149
+ @indexed_columns.each do |field|
150
+ @indexes[field].clear
151
+ @reverse_indexes[field].clear
152
+ end
134
153
  end
135
154
 
136
- def use_index?(record_class, where_clause)
137
- @indexed_columns.key?(record_class) && get_index(record_class, where_clause).present?
155
+ def use_index?(normalized_where_clause)
156
+ get_indexes(normalized_where_clause).present?
138
157
  end
139
158
 
140
159
  private
141
160
 
142
- def indexed?(record_class)
143
- @indexed_columns.key?(record_class)
144
- end
145
-
146
- def get_keys(record_class, record)
147
- @indexed_columns[record_class].map do |index|
148
- arr = [record_class.name]
149
- index.each do |key|
150
- arr << key
151
- arr << record[key]
152
- end
153
- arr
154
- end
155
- end
156
-
157
- def get_index(record_class, where_clause)
158
- @indexed_columns[record_class].find do |indexed_where|
159
- where_clause.keys.size == indexed_where.size && (where_clause.keys.map(&:to_s) - indexed_where).empty?
160
- end
161
+ def get_indexes(normalized_where_clause)
162
+ @indexed_columns & normalized_where_clause.keys
161
163
  end
162
164
  end
163
165
 
@@ -167,17 +169,28 @@ module Sequent
167
169
  # Key corresponds to the name of the 'Record'
168
170
  # Values contains list of lists on which columns to index.
169
171
  # E.g. [[:first_index_column], [:another_index, :with_to_columns]]
170
- def initialize(insert_with_csv_size = 50, indices = {})
172
+ def initialize(insert_with_csv_size = 50, indices = {}, default_indexed_columns = [:aggregate_id])
171
173
  @insert_with_csv_size = insert_with_csv_size
172
- @record_store = Hash.new { |h, k| h[k] = Set.new }
173
- @record_index = Index.new(indices)
174
+ @record_store = Hash.new { |h, k| h[k] = Set.new.compare_by_identity }
175
+ @record_index = Hash.new do |h, k|
176
+ h[k] = Index.new(default_indexed_columns.to_set & k.column_names.map(&:to_sym))
177
+ end
178
+
179
+ indices.each do |record_class, indexed_columns|
180
+ columns = indexed_columns.flatten(1).map(&:to_sym).to_set + default_indexed_columns
181
+ @record_index[record_class] = Index.new(columns & record_class.column_names.map(&:to_sym))
182
+ end
183
+
184
+ @record_defaults = Hash.new do |h, record_class|
185
+ h[record_class] = record_class.column_defaults.symbolize_keys
186
+ end
174
187
  end
175
188
 
176
189
  def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {})
177
190
  record = get_record!(record_class, where_clause)
178
- record.updated_at = event.created_at if record.respond_to?(:updated_at)
191
+ record.updated_at = event.created_at if record.respond_to?(:updated_at=)
179
192
  yield record if block_given?
180
- @record_index.update(record_class, record)
193
+ @record_index[record_class].update(record)
181
194
  update_sequence_number = if options.key?(:update_sequence_number)
182
195
  options[:update_sequence_number]
183
196
  else
@@ -187,41 +200,16 @@ module Sequent
187
200
  end
188
201
 
189
202
  def create_record(record_class, values)
190
- column_names = record_class.column_names
191
- values = record_class.column_defaults.with_indifferent_access.merge(values)
192
- values.merge!(updated_at: values[:created_at]) if column_names.include?('updated_at')
193
- struct_class_name = "#{record_class}Struct"
194
- if self.class.struct_cache.key?(struct_class_name)
195
- struct_class = self.class.struct_cache[struct_class_name]
196
- else
197
- # We create a struct on the fly.
198
- # Since the replay happens in memory we implement the ==, eql? and hash methods
199
- # to point to the same object. A record is the same if and only if they point to
200
- # the same object. These methods are necessary since we use Set instead of [].
201
- class_def = <<-EOD
202
- #{struct_class_name} = Struct.new(*#{column_names.map(&:to_sym)})
203
- class #{struct_class_name}
204
- include InitStruct
205
- def ==(other)
206
- self.equal?(other)
207
- end
208
- def hash
209
- self.object_id.hash
210
- end
211
- end
212
- EOD
213
- # rubocop:disable Security/Eval
214
- eval(class_def.to_s)
215
- # rubocop:enable Security/Eval
216
- struct_class = ReplayOptimizedPostgresPersistor.const_get(struct_class_name)
217
- self.class.struct_cache[struct_class_name] = struct_class
203
+ record = struct_cache[record_class].new(**values)
204
+ @record_defaults[record_class].each do |column, default|
205
+ record[column] = default unless values.include? column
218
206
  end
219
- record = struct_class.new.set_values(values)
207
+ record.updated_at = values[:created_at] if record.respond_to?(:updated_at)
220
208
 
221
209
  yield record if block_given?
222
- @record_store[record_class] << record
223
210
 
224
- @record_index.add(record_class, record)
211
+ @record_store[record_class] << record
212
+ @record_index[record_class].add(record)
225
213
 
226
214
  record
227
215
  end
@@ -234,7 +222,7 @@ module Sequent
234
222
  record = get_record(record_class, values)
235
223
  record ||= create_record(record_class, values.merge(created_at: created_at))
236
224
  yield record if block_given?
237
- @record_index.update(record_class, record)
225
+ @record_index[record_class].update(record)
238
226
  record
239
227
  end
240
228
 
@@ -260,15 +248,15 @@ module Sequent
260
248
 
261
249
  def delete_record(record_class, record)
262
250
  @record_store[record_class].delete(record)
263
- @record_index.remove(record_class, record)
251
+ @record_index[record_class].remove(record)
264
252
  end
265
253
 
266
254
  def update_all_records(record_class, where_clause, updates)
267
255
  find_records(record_class, where_clause).each do |record|
268
256
  updates.each_pair do |k, v|
269
- record[k.to_sym] = v
257
+ record[k] = v
270
258
  end
271
- @record_index.update(record_class, record)
259
+ @record_index[record_class].update(record)
272
260
  end
273
261
  end
274
262
 
@@ -276,33 +264,41 @@ module Sequent
276
264
  records = find_records(record_class, where_clause)
277
265
  records.each do |record|
278
266
  yield record
279
- @record_index.update(record_class, record)
267
+ @record_index[record_class].update(record)
280
268
  end
281
269
  end
282
270
 
283
271
  def do_with_record(record_class, where_clause)
284
272
  record = get_record!(record_class, where_clause)
285
273
  yield record
286
- @record_index.update(record_class, record)
274
+ @record_index[record_class].update(record)
287
275
  end
288
276
 
289
277
  def find_records(record_class, where_clause)
290
- if @record_index.use_index?(record_class, where_clause)
291
- @record_index.find(record_class, where_clause)
292
- else
293
- @record_store[record_class].select do |record|
294
- where_clause.all? do |k, v|
295
- expected_value = v.is_a?(Symbol) ? v.to_s : v
296
- actual_value = record[k.to_sym]
297
- actual_value = actual_value.to_s if actual_value.is_a? Symbol
298
- if expected_value.is_a?(Array)
299
- expected_value.include?(actual_value)
300
- else
301
- actual_value == expected_value
302
- end
278
+ where_clause = where_clause.symbolize_keys
279
+
280
+ indexed_columns = @record_index[record_class].indexed_columns
281
+ indexed_fields, non_indexed_fields = where_clause.partition { |field, _| indexed_columns.include? field }
282
+
283
+ candidate_records = if indexed_fields.present?
284
+ @record_index[record_class].find(indexed_fields)
285
+ else
286
+ @record_store[record_class]
287
+ end
288
+
289
+ return candidate_records.to_a if non_indexed_fields.empty?
290
+
291
+ candidate_records.select do |record|
292
+ non_indexed_fields.all? do |k, v|
293
+ expected_value = Persistors.normalize_symbols(v)
294
+ actual_value = Persistors.normalize_symbols(record[k])
295
+ if expected_value.is_a?(Array)
296
+ expected_value.include?(actual_value)
297
+ else
298
+ actual_value == expected_value
303
299
  end
304
300
  end
305
- end.dup
301
+ end
306
302
  end
307
303
 
308
304
  def last_record(record_class, where_clause)
@@ -310,6 +306,10 @@ module Sequent
310
306
  results.empty? ? nil : results.last
311
307
  end
312
308
 
309
+ def prepare
310
+ # noop
311
+ end
312
+
313
313
  def commit
314
314
  @record_store.each do |clazz, records|
315
315
  @column_cache ||= {}
@@ -358,7 +358,7 @@ module Sequent
358
358
 
359
359
  def clear
360
360
  @record_store.clear
361
- @record_index.clear
361
+ @record_index.values.each(&:clear)
362
362
  end
363
363
 
364
364
  private
@@ -366,12 +366,19 @@ module Sequent
366
366
  def cast_value_to_column_type(clazz, column_name, record)
367
367
  uncasted_value = ActiveModel::Attribute.from_database(
368
368
  column_name,
369
- record[column_name.to_sym],
369
+ record[column_name],
370
370
  Sequent::ApplicationRecord.connection.lookup_cast_type_from_column(@column_cache[clazz.name][column_name]),
371
371
  ).value_for_database
372
372
  Sequent::ApplicationRecord.connection.type_cast(uncasted_value)
373
373
  end
374
374
  end
375
+
376
+ # Normalizes symbol values to strings (by using its name) while
377
+ # preserving all other values. This allows symbol/string
378
+ # indifferent comparisons.
379
+ def self.normalize_symbols(value)
380
+ value.is_a?(Symbol) ? value.name : value
381
+ end
375
382
  end
376
383
  end
377
384
  end
@@ -88,6 +88,10 @@ module Sequent
88
88
  include Migratable
89
89
  extend ActiveSupport::DescendantsTracker
90
90
 
91
+ class << self
92
+ attr_accessor :abstract_class, :skip_autoregister
93
+ end
94
+
91
95
  def initialize(persistor = Sequent::Core::Persistors::ActiveRecordPersistor.new)
92
96
  ensure_valid!
93
97
  @persistor = persistor
@@ -30,8 +30,9 @@ module Sequent
30
30
  #
31
31
  class ActiveRecordTransactionProvider
32
32
  def transactional(&block)
33
- Sequent::ApplicationRecord.transaction(requires_new: true, &block)
33
+ result = Sequent::ApplicationRecord.transaction(requires_new: true, &block)
34
34
  after_commit_queue.pop.call until after_commit_queue.empty?
35
+ result
35
36
  ensure
36
37
  clear_after_commit_queue
37
38
  end
@@ -9,6 +9,10 @@ module Sequent
9
9
  include Helpers::MessageHandler
10
10
  extend ActiveSupport::DescendantsTracker
11
11
 
12
+ class << self
13
+ attr_accessor :abstract_class, :skip_autoregister
14
+ end
15
+
12
16
  def self.on(*args, **opts, &block)
13
17
  decorated_block = ->(event) do
14
18
  begin
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'read_only_replay_optimized_postgres_persistor'
4
+ require_relative 'view_schema'
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../core/persistors/replay_optimized_postgres_persistor'
4
+ module Sequent
5
+ module DryRun
6
+ # Subclass of ReplayOptimizedPostgresPersistor
7
+ # This persistor does not persist anything. Mainly usefull for
8
+ # performance testing migrations.
9
+ class ReadOnlyReplayOptimizedPostgresPersistor < Core::Persistors::ReplayOptimizedPostgresPersistor
10
+ def prepare
11
+ @starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
12
+ end
13
+
14
+ def commit
15
+ # Running in dryrun mode, not committing anything.
16
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
17
+ elapsed = ending - @starting
18
+ count = @record_store.values.map(&:size).sum
19
+ Sequent.logger.info(
20
+ "dryrun: processed #{count} records in #{elapsed.round(2)} s (#{(count / elapsed).round(2)} records/s)",
21
+ )
22
+ clear
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../migrations/view_schema'
4
+ require_relative 'read_only_replay_optimized_postgres_persistor'
5
+
6
+ module Sequent
7
+ module DryRun
8
+ # Subclass of Migrations::ViewSchema to dry run a migration.
9
+ # This migration does not insert anything into the database, mainly usefull
10
+ # for performance testing migrations.
11
+ class ViewSchema < Migrations::ViewSchema
12
+ def migrate_dryrun(regex:, group_exponent: 3, limit: nil, offset: nil)
13
+ persistor = DryRun::ReadOnlyReplayOptimizedPostgresPersistor.new
14
+
15
+ projectors = Sequent::Core::Migratable.all.select { |p| p.replay_persistor.nil? && p.name.match(regex || /.*/) }
16
+ if projectors.present?
17
+ Sequent.logger.info "Dry run using the following projectors: #{projectors.map(&:name).join(', ')}"
18
+
19
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
20
+ groups = groups(group_exponent: group_exponent, limit: limit, offset: offset)
21
+ replay!(persistor, projectors: projectors, groups: groups)
22
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
+
24
+ Sequent.logger.info("Done migrate_dryrun for version #{Sequent.new_version} in #{ending - starting} s")
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # override so no ids are inserted.
31
+ def insert_ids
32
+ ->(progress, done, ids) {}
33
+ end
34
+ end
35
+ end
36
+ end
@@ -8,6 +8,7 @@ ActiveRecord::Schema.define do
8
8
  t.text "event_json", :null => false
9
9
  t.integer "command_record_id", :null => false
10
10
  t.integer "stream_record_id", :null => false
11
+ t.bigint "xact_id"
11
12
  end
12
13
 
13
14
  execute %Q{
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Migrations
5
+ class MigrationError < RuntimeError; end
6
+ class MigrationNotStarted < MigrationError; end
7
+ class MigrationDone < MigrationError; end
8
+ class ConcurrentMigration < MigrationError; end
9
+
10
+ class InvalidMigrationDefinition < MigrationError; end
11
+ end
12
+ end
@@ -5,7 +5,6 @@ module Sequent
5
5
  end
6
6
  end
7
7
 
8
- require_relative 'migrate_events'
9
8
  require_relative 'projectors'
10
9
  require_relative 'view_schema'
11
10
  require_relative 'functions'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'errors'
4
+
3
5
  module Sequent
4
6
  module Migrations
5
7
  class Planner
@@ -69,10 +71,10 @@ module Sequent
69
71
  .each_with_index
70
72
  .select { |migration, _index| migration.instance_of?(AlterTable) }
71
73
  .select do |migration, index|
72
- migrations
73
- .slice((index + 1)..-1)
74
- .find { |m| m.instance_of?(ReplayTable) && m.record_class == migration.record_class }
75
- end.map(&:first)
74
+ migrations
75
+ .slice((index + 1)..-1)
76
+ .find { |m| m.instance_of?(ReplayTable) && m.record_class == migration.record_class }
77
+ end.map(&:first)
76
78
  end
77
79
 
78
80
  def remove_redundancy(grouped_migrations)
@@ -100,7 +102,8 @@ module Sequent
100
102
  def map_to_migrations(migrations)
101
103
  migrations.reduce({}) do |memo, (version, ms)|
102
104
  unless ms.is_a?(Array)
103
- fail "Declared migrations for version #{version} must be an Array. For example: {'3' => [FooProjector]}"
105
+ fail InvalidMigrationDefinition,
106
+ "Declared migrations for version #{version} must be an Array. For example: {'3' => [FooProjector]}"
104
107
  end
105
108
 
106
109
  memo[version] = ms.flat_map do |migration|
@@ -109,14 +112,15 @@ module Sequent
109
112
  #{Sequent.configuration.migration_sql_files_directory}/#{migration.table_name}_#{version}.sql
110
113
  EOS
111
114
  unless File.exist?(alter_table_sql_file_name)
112
- fail "Missing file #{alter_table_sql_file_name} to apply for version #{version}"
115
+ fail InvalidMigrationDefinition,
116
+ "Missing file #{alter_table_sql_file_name} to apply for version #{version}"
113
117
  end
114
118
 
115
119
  migration.copy(version)
116
120
  elsif migration < Sequent::Projector
117
121
  migration.managed_tables.map { |table| ReplayTable.create(table, version) }
118
122
  else
119
- fail "Unknown Migration #{migration}"
123
+ fail InvalidMigrationDefinition, "Unknown Migration #{migration}"
120
124
  end
121
125
  end
122
126