sequent 6.0.1 → 7.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/db/sequent_schema.rb +5 -0
- data/lib/sequent/configuration.rb +64 -13
- data/lib/sequent/core/aggregate_repository.rb +2 -2
- data/lib/sequent/core/aggregate_snapshotter.rb +4 -0
- data/lib/sequent/core/base_command_handler.rb +5 -0
- data/lib/sequent/core/core.rb +1 -1
- data/lib/sequent/core/event.rb +2 -2
- data/lib/sequent/core/event_record.rb +1 -0
- data/lib/sequent/core/event_store.rb +20 -16
- data/lib/sequent/core/helpers/attribute_support.rb +7 -7
- data/lib/sequent/core/helpers/message_handler.rb +10 -11
- data/lib/sequent/core/helpers/message_router.rb +13 -7
- data/lib/sequent/core/persistors/active_record_persistor.rb +4 -0
- data/lib/sequent/core/persistors/persistor.rb +5 -0
- data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +140 -133
- data/lib/sequent/core/projector.rb +4 -0
- data/lib/sequent/core/transactions/active_record_transaction_provider.rb +2 -1
- data/lib/sequent/core/workflow.rb +4 -0
- data/lib/sequent/dry_run/dry_run.rb +4 -0
- data/lib/sequent/dry_run/read_only_replay_optimized_postgres_persistor.rb +26 -0
- data/lib/sequent/dry_run/view_schema.rb +36 -0
- data/lib/sequent/generator/template_project/db/sequent_schema.rb +1 -0
- data/lib/sequent/migrations/errors.rb +12 -0
- data/lib/sequent/migrations/migrations.rb +0 -1
- data/lib/sequent/migrations/planner.rb +11 -7
- data/lib/sequent/migrations/versions.rb +82 -0
- data/lib/sequent/migrations/view_schema.rb +101 -58
- data/lib/sequent/rake/migration_tasks.rb +89 -6
- data/lib/sequent/sequent.rb +4 -11
- data/lib/sequent/support/database.rb +3 -11
- data/lib/sequent/support.rb +0 -2
- data/lib/sequent/util/util.rb +1 -0
- data/lib/sequent/util/web/clear_cache.rb +19 -0
- data/lib/sequent.rb +1 -0
- data/lib/version.rb +1 -1
- metadata +20 -30
- data/lib/sequent/core/helpers/message_dispatcher.rb +0 -20
- data/lib/sequent/migrations/migrate_events.rb +0 -67
- data/lib/sequent/support/view_projection.rb +0 -61
- 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 => [
|
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
|
-
|
60
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
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 =
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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(
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
@
|
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(
|
105
|
-
|
106
|
-
|
107
|
-
|
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(
|
118
|
-
remove(
|
119
|
-
add(
|
122
|
+
def update(record)
|
123
|
+
remove(record)
|
124
|
+
add(record)
|
120
125
|
end
|
121
126
|
|
122
|
-
def find(
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
@
|
133
|
-
|
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?(
|
137
|
-
|
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
|
143
|
-
@indexed_columns.
|
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 =
|
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(
|
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
|
-
|
191
|
-
|
192
|
-
|
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 =
|
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
|
-
@
|
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(
|
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(
|
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
|
257
|
+
record[k] = v
|
270
258
|
end
|
271
|
-
@record_index.update(
|
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(
|
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(
|
274
|
+
@record_index[record_class].update(record)
|
287
275
|
end
|
288
276
|
|
289
277
|
def find_records(record_class, where_clause)
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
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
|
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
|
@@ -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
|
@@ -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
|
@@ -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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
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
|
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
|
|