sequent 6.0.1 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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