sequent 2.1.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequent +65 -0
  3. data/db/sequent_schema.rb +9 -8
  4. data/lib/sequent.rb +3 -0
  5. data/lib/sequent/configuration.rb +67 -0
  6. data/lib/sequent/core/aggregate_repository.rb +1 -1
  7. data/lib/sequent/core/core.rb +2 -2
  8. data/lib/sequent/core/event_store.rb +2 -2
  9. data/lib/sequent/core/helpers/attribute_support.rb +3 -0
  10. data/lib/sequent/core/helpers/self_applier.rb +1 -1
  11. data/lib/sequent/core/{record_sessions/active_record_session.rb → persistors/active_record_persistor.rb} +21 -19
  12. data/lib/sequent/core/persistors/persistor.rb +84 -0
  13. data/lib/sequent/core/persistors/persistors.rb +3 -0
  14. data/lib/sequent/core/{record_sessions/replay_events_session.rb → persistors/replay_optimized_postgres_persistor.rb} +16 -7
  15. data/lib/sequent/core/projector.rb +96 -0
  16. data/lib/sequent/generator.rb +2 -0
  17. data/lib/sequent/generator/aggregate.rb +71 -0
  18. data/lib/sequent/generator/project.rb +61 -0
  19. data/lib/sequent/generator/template_aggregate/template_aggregate.rb +4 -0
  20. data/lib/sequent/generator/template_aggregate/template_aggregate/commands.rb +2 -0
  21. data/lib/sequent/generator/template_aggregate/template_aggregate/events.rb +2 -0
  22. data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate.rb +9 -0
  23. data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate_command_handler.rb +5 -0
  24. data/lib/sequent/generator/template_project/Gemfile +11 -0
  25. data/lib/sequent/generator/template_project/Gemfile.lock +72 -0
  26. data/lib/sequent/generator/template_project/Rakefile +12 -0
  27. data/lib/sequent/generator/template_project/app/projectors/post_projector.rb +22 -0
  28. data/lib/sequent/generator/template_project/app/records/post_record.rb +2 -0
  29. data/lib/sequent/generator/template_project/config/initializers/sequent.rb +13 -0
  30. data/lib/sequent/generator/template_project/db/database.yml +17 -0
  31. data/lib/sequent/generator/template_project/db/migrations.rb +17 -0
  32. data/lib/sequent/generator/template_project/db/sequent_schema.rb +51 -0
  33. data/lib/sequent/generator/template_project/db/tables/post_records.sql +10 -0
  34. data/lib/sequent/generator/template_project/lib/post.rb +4 -0
  35. data/lib/sequent/generator/template_project/lib/post/commands.rb +4 -0
  36. data/lib/sequent/generator/template_project/lib/post/events.rb +14 -0
  37. data/lib/sequent/generator/template_project/lib/post/post.rb +24 -0
  38. data/lib/sequent/generator/template_project/lib/post/post_command_handler.rb +5 -0
  39. data/lib/sequent/generator/template_project/my_app.rb +11 -0
  40. data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +32 -0
  41. data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +20 -0
  42. data/lib/sequent/generator/template_project/spec/spec_helper.rb +29 -0
  43. data/lib/sequent/migrations/migrate_events.rb +1 -0
  44. data/lib/sequent/migrations/migrations.rb +2 -0
  45. data/lib/sequent/migrations/projectors.rb +18 -0
  46. data/lib/sequent/migrations/view_schema.rb +364 -0
  47. data/lib/sequent/rake/migration_tasks.rb +109 -0
  48. data/lib/sequent/rake/tasks.rb +16 -0
  49. data/lib/sequent/sequent.rb +53 -13
  50. data/lib/sequent/support/database.rb +53 -8
  51. data/lib/sequent/util/printer.rb +16 -0
  52. data/lib/sequent/util/timer.rb +14 -0
  53. data/lib/sequent/util/util.rb +2 -0
  54. data/lib/version.rb +1 -1
  55. metadata +67 -14
  56. data/lib/sequent/core/base_event_handler.rb +0 -54
  57. data/lib/sequent/core/record_sessions/record_sessions.rb +0 -2
@@ -0,0 +1,51 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ create_table "event_records", :force => true do |t|
4
+ t.string "aggregate_id", :null => false
5
+ t.integer "sequence_number", :null => false
6
+ t.datetime "created_at", :null => false
7
+ t.string "event_type", :null => false
8
+ t.text "event_json", :null => false
9
+ t.integer "command_record_id", :null => false
10
+ t.integer "stream_record_id", :null => false
11
+ end
12
+
13
+ execute %Q{
14
+ CREATE UNIQUE INDEX unique_event_per_aggregate ON event_records (
15
+ aggregate_id,
16
+ sequence_number,
17
+ (CASE event_type WHEN 'Sequent::Core::SnapshotEvent' THEN 0 ELSE 1 END)
18
+ )
19
+ }
20
+ execute %Q{
21
+ CREATE INDEX snapshot_events ON event_records (aggregate_id, sequence_number DESC) WHERE event_type = 'Sequent::Core::SnapshotEvent'
22
+ }
23
+
24
+ add_index "event_records", ["command_record_id"], :name => "index_event_records_on_command_record_id"
25
+ add_index "event_records", ["event_type"], :name => "index_event_records_on_event_type"
26
+ add_index "event_records", ["created_at"], :name => "index_event_records_on_created_at"
27
+
28
+ create_table "command_records", :force => true do |t|
29
+ t.string "user_id"
30
+ t.string "aggregate_id"
31
+ t.string "command_type", :null => false
32
+ t.text "command_json", :null => false
33
+ t.datetime "created_at", :null => false
34
+ end
35
+
36
+ create_table "stream_records", :force => true do |t|
37
+ t.datetime "created_at", :null => false
38
+ t.string "aggregate_type", :null => false
39
+ t.string "aggregate_id", :null => false
40
+ t.integer "snapshot_threshold"
41
+ end
42
+
43
+ add_index "stream_records", ["aggregate_id"], :name => "index_stream_records_on_aggregate_id", :unique => true
44
+ execute %q{
45
+ ALTER TABLE event_records ADD CONSTRAINT command_fkey FOREIGN KEY (command_record_id) REFERENCES command_records (id)
46
+ }
47
+ execute %q{
48
+ ALTER TABLE event_records ADD CONSTRAINT stream_fkey FOREIGN KEY (stream_record_id) REFERENCES stream_records (id)
49
+ }
50
+
51
+ end
@@ -0,0 +1,10 @@
1
+ CREATE TABLE post_records%SUFFIX% (
2
+ id serial NOT NULL,
3
+ aggregate_id uuid NOT NULL,
4
+ author character varying,
5
+ title character varying,
6
+ content character varying,
7
+ CONSTRAINT post_records_pkey%SUFFIX% PRIMARY KEY (id)
8
+ );
9
+
10
+ CREATE UNIQUE INDEX post_records_keys%SUFFIX% ON post_records%SUFFIX% USING btree (aggregate_id);
@@ -0,0 +1,4 @@
1
+ require_relative 'post/commands'
2
+ require_relative 'post/events'
3
+ require_relative 'post/post'
4
+ require_relative 'post/post_command_handler'
@@ -0,0 +1,4 @@
1
+ class AddPost < Sequent::Command
2
+ attrs author: String, title: String, content: String
3
+ validates_presence_of :author, :title, :content
4
+ end
@@ -0,0 +1,14 @@
1
+ class PostAdded < Sequent::Event
2
+ end
3
+
4
+ class PostAuthorChanged < Sequent::Event
5
+ attrs author: String
6
+ end
7
+
8
+ class PostTitleChanged < Sequent::Event
9
+ attrs title: String
10
+ end
11
+
12
+ class PostContentChanged < Sequent::Event
13
+ attrs content: String
14
+ end
@@ -0,0 +1,24 @@
1
+ class Post < Sequent::AggregateRoot
2
+ def initialize(command)
3
+ super(command.aggregate_id)
4
+ apply PostAdded
5
+ apply PostAuthorChanged, author: command.author
6
+ apply PostTitleChanged, title: command.title
7
+ apply PostContentChanged, content: command.content
8
+ end
9
+
10
+ on PostAdded do
11
+ end
12
+
13
+ on PostAuthorChanged do |event|
14
+ @author = event.author
15
+ end
16
+
17
+ on PostTitleChanged do |event|
18
+ @title = event.title
19
+ end
20
+
21
+ on PostContentChanged do |event|
22
+ @content = event.content
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ class PostCommandHandler < Sequent::CommandHandler
2
+ on AddPost do |command|
3
+ repository.add_aggregate Post.new(command)
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ require 'sequent'
2
+ require 'sequent/support'
3
+ require 'erb'
4
+ require_relative 'lib/post'
5
+ require_relative 'app/projectors/post_projector'
6
+
7
+ require_relative 'config/initializers/sequent'
8
+
9
+ module MyApp
10
+
11
+ end
@@ -0,0 +1,32 @@
1
+ require_relative '../../spec_helper'
2
+ require_relative '../../../app/projectors/post_projector'
3
+
4
+ describe PostProjector do
5
+ let(:aggregate_id) { Sequent.new_uuid }
6
+ let(:post_projector) { PostProjector.new }
7
+ let(:post_added) { PostAdded.new(aggregate_id: aggregate_id, sequence_number: 1) }
8
+
9
+ context PostAdded do
10
+ it 'creates a projection' do
11
+ post_projector.handle_message(post_added)
12
+ expect(PostRecord.count).to eq(1)
13
+ record = PostRecord.first
14
+ expect(record.aggregate_id).to eq(aggregate_id)
15
+ end
16
+ end
17
+
18
+ context PostTitleChanged do
19
+ let(:post_title_changed) do
20
+ PostTitleChanged.new(aggregate_id: aggregate_id, title: 'ben en kim', sequence_number: 2)
21
+ end
22
+
23
+ before { post_projector.handle_message(post_added) }
24
+
25
+ it 'updates a projection' do
26
+ post_projector.handle_message(post_title_changed)
27
+ expect(PostRecord.count).to eq(1)
28
+ record = PostRecord.first
29
+ expect(record.title).to eq('ben en kim')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ require_relative '../../spec_helper'
2
+ require_relative '../../../lib/post'
3
+
4
+ describe PostCommandHandler do
5
+ let(:aggregate_id) { Sequent.new_uuid }
6
+
7
+ before :each do
8
+ Sequent.configuration.command_handlers = [PostCommandHandler.new]
9
+ end
10
+
11
+ it 'creates a post' do
12
+ when_command AddPost.new(aggregate_id: aggregate_id, author: 'ben', title: 'My first blogpost', content: 'Hello World!')
13
+ then_events(
14
+ PostAdded.new(aggregate_id: aggregate_id, sequence_number: 1),
15
+ PostAuthorChanged.new(aggregate_id: aggregate_id, sequence_number: 2, author: 'ben'),
16
+ PostTitleChanged,
17
+ PostContentChanged
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ ENV['RACK_ENV'] ||= 'test'
5
+
6
+ require 'sequent/test'
7
+ require 'database_cleaner'
8
+
9
+ require_relative '../my_app'
10
+
11
+ db_config = Sequent::Support::Database.read_config('test')
12
+ Sequent::Support::Database.establish_connection(db_config)
13
+
14
+ Sequent::Support::Database.drop_schema!(Sequent.configuration.view_schema_name)
15
+
16
+ Sequent::Migrations::ViewSchema.new(db_config: db_config).create_view_tables
17
+ Sequent.configuration.event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
18
+
19
+ RSpec.configure do |config|
20
+ config.include Sequent::Test::CommandHandlerHelpers
21
+
22
+ config.around do |example|
23
+ Sequent.configuration.aggregate_repository.clear
24
+ DatabaseCleaner.strategy = :truncation
25
+ DatabaseCleaner.cleaning do
26
+ example.run
27
+ end
28
+ end
29
+ end
@@ -24,6 +24,7 @@ module Sequent
24
24
  ##
25
25
  # @param env The string representing the current environment. E.g. "development", "production"
26
26
  def initialize(env)
27
+ warn '[DEPRECATED] Use of MigrateEvents is deprecated and will be removed from future version. Please use Sequent::Migrations::ViewSchema instead. See the changelog on how to update.'
27
28
  @env = env
28
29
  end
29
30
 
@@ -5,3 +5,5 @@ module Sequent
5
5
  end
6
6
 
7
7
  require_relative 'migrate_events'
8
+ require_relative 'projectors'
9
+ require_relative 'view_schema'
@@ -0,0 +1,18 @@
1
+ module Sequent
2
+ module Migrations
3
+ class Projectors
4
+ def self.versions
5
+ fail "Define your own Sequent::Migrations::Projectors class that extends this class and implements this method"
6
+ end
7
+
8
+ def self.version
9
+ fail "Define your own Sequent::Migrations::Projectors class that extends this class and implements this method"
10
+ end
11
+
12
+ def self.projectors_between(old, new)
13
+ versions.values_at(*Range.new(old + 1, new).to_a.map(&:to_s)).compact.flatten.uniq
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,364 @@
1
+ require 'parallel'
2
+ require 'postgresql_cursor'
3
+
4
+ require_relative '../support/database'
5
+ require_relative '../sequent'
6
+ require_relative '../util/timer'
7
+ require_relative '../util/printer'
8
+ require_relative './projectors'
9
+
10
+ module Sequent
11
+ module Migrations
12
+ class MigrationError < RuntimeError; end
13
+
14
+
15
+ ##
16
+ # Responsible for migration of Projectors between view schema versions.
17
+ #
18
+ # A Projector needs migration when for instance:
19
+ #
20
+ # - New columns are added
21
+ # - Structure is changed
22
+ #
23
+ # To maintain your migrations you need to:
24
+ # 1. Create a class that extends `Sequent::Migrations::Projectors` and specify in `Sequent.configuration.migrations_class_name`
25
+ # 2. Define per version which Projectors you want to migrate
26
+ # See the definition of `Sequent::Migrations::Projectors.versions` and `Sequent::Migrations::Projectors.version`
27
+ # 3. Specify in Sequent where your sql files reside (Sequent.configuration.migration_sql_files_directory)
28
+ # 4. Ensure that you add %SUFFIX% to each name that needs to be unique in postgres (like TABLE names, INDEX names, PRIMARY KEYS)
29
+ # E.g. `create table foo%SUFFIX% (id serial NOT NULL, CONSTRAINT foo_pkey%SUFFIX% PRIMARY KEY (id))`
30
+ #
31
+ class ViewSchema
32
+
33
+ # Corresponds with the index on aggregate_id column in the event_records table
34
+ #
35
+ # Since we replay in batches of the first 3 chars of the uuid we created an index on
36
+ # these 3 characters. Hence the name ;-)
37
+ #
38
+ # This also means that the online replay is divided up into 16**3 groups
39
+ # This might seem a lot for starting event store, but when you will get more
40
+ # events, you will see that this is pretty good partitioned.
41
+ LENGTH_OF_SUBSTRING_INDEX_ON_AGGREGATE_ID_IN_EVENT_STORE = 3
42
+
43
+ include Sequent::Util::Timer
44
+ include Sequent::Util::Printer
45
+
46
+ class Versions < ActiveRecord::Base; end
47
+ class ReplayedIds < ActiveRecord::Base; end
48
+
49
+ attr_reader :view_schema, :db_config, :logger
50
+
51
+ def initialize(db_config:)
52
+ @db_config = db_config
53
+ @view_schema = Sequent.configuration.view_schema_name
54
+ @logger = Sequent.logger
55
+ end
56
+
57
+ ##
58
+ # Returns the current version from the database
59
+ def current_version
60
+ Versions.order('version desc').limit(1).first&.version || 0
61
+ end
62
+
63
+ ##
64
+ # Utility method that creates all tables in the view schema
65
+ #
66
+ # This method is mainly useful in test scenario to just create
67
+ # the entire view schema without replaying the events
68
+ def create_view_tables
69
+ create_view_schema_if_not_exists
70
+ in_view_schema do
71
+ Sequent::Core::Migratable.all.flat_map(&:managed_tables).each do |table|
72
+ statements = sql_file_to_statements("#{Sequent.configuration.migration_sql_files_directory}/#{table.table_name}.sql") { |raw_sql| raw_sql.remove('%SUFFIX%') }
73
+ statements.each { |statement| exec_sql(statement) }
74
+ end
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Utility method that replays events for all managed_tables from all Sequent::Core::Projector's
80
+ #
81
+ # This method is mainly useful in test scenario's or development tasks
82
+ def replay_all!
83
+ replay!(Sequent::Core::Migratable.all, Sequent.configuration.online_replay_persistor_class.new)
84
+ end
85
+
86
+ ##
87
+ # Utility method that creates the view_schema and the meta data tables
88
+ #
89
+ # This method is mainly useful during an initial setup of the view schema
90
+ def create_view_schema_if_not_exists
91
+ exec_sql(%Q{CREATE SCHEMA IF NOT EXISTS #{view_schema}})
92
+ in_view_schema do
93
+ exec_sql(%Q{CREATE TABLE IF NOT EXISTS #{Versions.table_name} (version integer NOT NULL, CONSTRAINT version_pk PRIMARY KEY(version))})
94
+ exec_sql(%Q{CREATE TABLE IF NOT EXISTS #{ReplayedIds.table_name} (event_id bigint NOT NULL, CONSTRAINT event_id_pk PRIMARY KEY(event_id))})
95
+ end
96
+ end
97
+
98
+ ##
99
+ # First part of a view schema migration
100
+ #
101
+ # Call this method while your application is running.
102
+ # The online part consists of:
103
+ #
104
+ # 1. Ensure any previous migrations are cleaned up
105
+ # 2. Create new tables for the Projectors which need to be migrated to the new version
106
+ # These tables will be called `table_name_VERSION`.
107
+ # 3. Replay all events to populate the tables
108
+ # It keeps track of all events that are already replayed.
109
+ #
110
+ # If anything fails an exception is raised and everything is rolled back
111
+ #
112
+ def migrate_online
113
+ ensure_version_correct!
114
+
115
+ in_view_schema do
116
+ truncate_replay_ids_table!
117
+
118
+ drop_old_tables(Sequent.new_version)
119
+ for_each_table_to_migrate do |table|
120
+ statements = sql_file_to_statements("#{Sequent.configuration.migration_sql_files_directory}/#{table.table_name}.sql") { |raw_sql| raw_sql.gsub('%SUFFIX%', "_#{Sequent.new_version}") }
121
+ statements.each { |statement| exec_sql(statement) }
122
+ table.table_name = "#{table.table_name}_#{Sequent.new_version}"
123
+ table.reset_column_information
124
+ end
125
+ end
126
+ replay!(projectors_to_migrate, Sequent.configuration.online_replay_persistor_class.new)
127
+ rescue Exception => e
128
+ rollback_migration
129
+ raise e
130
+ end
131
+
132
+ ##
133
+ # Last part of a view schema migration
134
+ #
135
+ # +You have to ensure no events are being added to the event store while this method is running.+
136
+ # For instance put your application in maintenance mode.
137
+ #
138
+ # The offline part consists of:
139
+ #
140
+ # 1. Replay all events not yet replayed since #migration_online
141
+ # 2. Within a single transaction do:
142
+ # 2.1 Rename current tables with the +current version+ as SUFFIX
143
+ # 2.2 Rename the new tables and remove the +new version+ suffix
144
+ # 2.3 Add the new version in the +Versions+ table
145
+ # 3. Performs cleanup of replayed event ids
146
+ #
147
+ # If anything fails an exception is raised and everything is rolled back
148
+ #
149
+ # When this method succeeds you can safely start the application from Sequent's point of view.
150
+ #
151
+ def migrate_offline
152
+ return if Sequent.new_version == current_version
153
+
154
+ ensure_version_correct!
155
+
156
+ set_table_names_to_new_version
157
+
158
+ # 1 replay events not yet replayed
159
+ replay!(projectors_to_migrate, Sequent.configuration.offline_replay_persistor_class.new, exclude_ids: true, group_exponent: 1)
160
+
161
+ in_view_schema do
162
+ ActiveRecord::Base.transaction do
163
+ for_each_table_to_migrate do |table|
164
+ current_table_name = table.table_name.gsub("_#{Sequent.new_version}", "")
165
+ # 2 Rename old table
166
+ exec_sql("ALTER TABLE IF EXISTS #{current_table_name} RENAME TO #{current_table_name}_#{current_version}")
167
+ # 3 Rename new table
168
+ exec_sql("ALTER TABLE #{table.table_name} RENAME TO #{current_table_name}")
169
+ # Use new table from now on
170
+ table.table_name = current_table_name
171
+ table.reset_column_information
172
+ end
173
+ # 4. Create migration record
174
+ Versions.create!(version: Sequent.new_version)
175
+ end
176
+
177
+ # 5. Truncate replayed ids
178
+ truncate_replay_ids_table!
179
+ end
180
+ logger.info "Migrated to version #{Sequent.new_version}"
181
+ rescue Exception => e
182
+ rollback_migration
183
+ raise e
184
+ end
185
+
186
+ private
187
+ def set_table_names_to_new_version
188
+ for_each_table_to_migrate do |table|
189
+ unless table.table_name.end_with?("_#{Sequent.new_version}")
190
+ table.table_name = "#{table.table_name}_#{Sequent.new_version}"
191
+ table.reset_column_information
192
+ fail MigrationError.new("Table #{table.table_name} does not exist. Did you run migrate_online first?") unless table.table_exists?
193
+ end
194
+ end
195
+ end
196
+
197
+ def reset_table_names
198
+ for_each_table_to_migrate do |table|
199
+ table.table_name = table.table_name.gsub("_#{Sequent.new_version}", "")
200
+ table.reset_column_information
201
+ end
202
+ end
203
+
204
+ def ensure_version_correct!
205
+ create_view_schema_if_not_exists
206
+ new_version = Sequent.new_version
207
+
208
+ fail ArgumentError.new("new_version [#{new_version}] must be greater or equal to current_version [#{current_version}]") if new_version < current_version
209
+ end
210
+
211
+ def replay!(projectors, replay_persistor, exclude_ids: false, group_exponent: 3)
212
+ logger.info "group_exponent: #{group_exponent.inspect}"
213
+
214
+ with_sequent_config(replay_persistor, projectors) do
215
+ logger.info "Start replaying events"
216
+
217
+ time("#{16**group_exponent} groups replayed") do
218
+ event_types = projectors.flat_map { |projector| projector.message_mapping.keys }.uniq.map(&:name)
219
+ disconnect!
220
+
221
+ number_of_groups = 16**group_exponent
222
+ groups = groups_of_aggregate_id_prefixes(number_of_groups)
223
+
224
+ @connected = false
225
+ # using `map_with_index` because https://github.com/grosser/parallel/issues/175
226
+ result = Parallel.map_with_index(groups, in_processes: Sequent.configuration.number_of_replay_processes) do |aggregate_prefixes, index|
227
+ begin
228
+ @connected ||= establish_connection
229
+ time("Group (#{aggregate_prefixes.first}-#{aggregate_prefixes.last}) #{index + 1}/#{number_of_groups} replayed") do
230
+ replay_events(aggregate_prefixes, event_types, exclude_ids, replay_persistor, &insert_ids)
231
+ end
232
+ nil
233
+ rescue => e
234
+ logger.error "Replaying failed for ids: ^#{aggregate_prefixes.first} - #{aggregate_prefixes.last}"
235
+ logger.error "+++++++++++++++ ERROR +++++++++++++++"
236
+ recursively_print(e)
237
+ raise Parallel::Kill # immediately kill all sub-processes
238
+ end
239
+ end
240
+ establish_connection
241
+ fail if result.nil?
242
+ end
243
+ end
244
+ end
245
+
246
+ def replay_events(aggregate_prefixes, event_types, exclude_already_replayed, replay_persistor, &on_progress)
247
+ Sequent.configuration.event_store.replay_events_from_cursor(
248
+ block_size: 1000,
249
+ get_events: -> { event_stream(aggregate_prefixes, event_types, exclude_already_replayed) },
250
+ on_progress: on_progress
251
+ )
252
+
253
+ replay_persistor.commit
254
+
255
+ # Also commit all specific declared replay persistors on projectors.
256
+ Sequent.configuration.event_handlers.select { |e| e.class.replay_persistor }.each(&:commit)
257
+ end
258
+
259
+ def rollback_migration
260
+ disconnect!
261
+ establish_connection
262
+ drop_old_tables(Sequent.new_version)
263
+
264
+ truncate_replay_ids_table!
265
+ reset_table_names
266
+ end
267
+
268
+ def truncate_replay_ids_table!
269
+ exec_sql("truncate table #{ReplayedIds.table_name}")
270
+ end
271
+
272
+ def groups_of_aggregate_id_prefixes(number_of_groups)
273
+ all_prefixes = (0...16**LENGTH_OF_SUBSTRING_INDEX_ON_AGGREGATE_ID_IN_EVENT_STORE).to_a.map { |i| i.to_s(16) } # first x digits of hex
274
+ all_prefixes = all_prefixes.map { |s| s.length == 3 ? s : "#{"0" * (3 - s.length)}#{s}" }
275
+
276
+ logger.info "Number of groups #{number_of_groups}"
277
+
278
+ logger.debug "Prefixes: #{all_prefixes.length}"
279
+ fail "Can not have more groups #{number_of_groups} than number of prefixes #{all_prefixes.length}" if number_of_groups > all_prefixes.length
280
+
281
+ all_prefixes.each_slice(all_prefixes.length/number_of_groups).to_a
282
+ end
283
+
284
+ def for_each_table_to_migrate
285
+ projectors_to_migrate.flat_map(&:managed_tables).each do |managed_table|
286
+ yield(managed_table)
287
+ end
288
+ end
289
+
290
+ def projectors_to_migrate
291
+ Sequent.migration_class.projectors_between(current_version, Sequent.new_version)
292
+ end
293
+
294
+ def in_view_schema
295
+ Sequent::Support::Database.with_schema_search_path(view_schema, db_config) do
296
+ yield
297
+ end
298
+ end
299
+
300
+ def sql_file_to_statements(file_location)
301
+ raw_sql_string = File.read(file_location, encoding: 'bom|utf-8')
302
+ sql_string = yield(raw_sql_string)
303
+ sql_string.split(/;$/).reject { |statement| statement.remove("\n").blank? }
304
+ end
305
+
306
+ def drop_old_tables(new_version)
307
+ versions_to_check = (current_version - 10)..new_version
308
+ old_tables = versions_to_check.flat_map do |old_version|
309
+ exec_sql(
310
+ "select table_name from information_schema.tables where table_schema = '#{Sequent.configuration.view_schema_name}' and table_name LIKE '%_#{old_version}'"
311
+ ).flat_map { |row| row.values }
312
+ end
313
+ old_tables.each do |old_table|
314
+ exec_sql("DROP TABLE #{Sequent.configuration.view_schema_name}.#{old_table} CASCADE")
315
+ end
316
+ end
317
+
318
+ def insert_ids
319
+ ->(progress, done, ids) do
320
+ exec_sql("insert into #{ReplayedIds.table_name} (event_id) values #{ids.map { |id| "(#{id})" }.join(',')}") unless ids.empty?
321
+ Sequent::Core::EventStore::PRINT_PROGRESS[progress, done, ids] if progress > 0
322
+ end
323
+ end
324
+
325
+ def with_sequent_config(replay_persistor, projectors, &block)
326
+ old_config = Sequent.configuration
327
+
328
+ config = Sequent.configuration.dup
329
+
330
+ replay_projectors = projectors.map { |projector_class| projector_class.new(projector_class.replay_persistor || replay_persistor) }
331
+ config.transaction_provider = Sequent::Core::Transactions::NoTransactions.new
332
+ config.event_handlers = replay_projectors
333
+
334
+ Sequent::Configuration.restore(config)
335
+
336
+ block.call
337
+ ensure
338
+ Sequent::Configuration.restore(old_config)
339
+ end
340
+
341
+ def event_stream(aggregate_prefixes, event_types, exclude_already_replayed)
342
+ fail ArgumentError.new("aggregate_prefixes is mandatory") unless aggregate_prefixes.present?
343
+
344
+ event_stream = Sequent.configuration.event_record_class.where(event_type: event_types)
345
+ event_stream = event_stream.where("substring(aggregate_id::varchar from 1 for #{LENGTH_OF_SUBSTRING_INDEX_ON_AGGREGATE_ID_IN_EVENT_STORE}) in (?)", aggregate_prefixes)
346
+ event_stream = event_stream.where("NOT EXISTS (SELECT 1 FROM #{ReplayedIds.table_name} WHERE event_id = event_records.id)") if exclude_already_replayed
347
+ event_stream.order('sequence_number ASC').select('id, event_type, event_json, sequence_number')
348
+ end
349
+
350
+ ## shortcut methods
351
+ def disconnect!
352
+ Sequent::Support::Database.disconnect!
353
+ end
354
+
355
+ def establish_connection
356
+ Sequent::Support::Database.establish_connection(db_config)
357
+ end
358
+
359
+ def exec_sql(sql)
360
+ ActiveRecord::Base.connection.execute(sql)
361
+ end
362
+ end
363
+ end
364
+ end