sequent 2.1.0 → 3.0.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 (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