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.
- 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
|
|