sequent 8.1.1 → 8.2.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/db/migrate/20250101000000_sequent_initial_schema.rb +166 -0
  3. data/db/migrate/20250101000001_sequent_stored_procedures.rb +48 -0
  4. data/db/migrate/20250312105100_sequent_store_events_v02.rb +29 -0
  5. data/db/migrate/sequent/aggregate_event_type_v01.sql +7 -0
  6. data/db/migrate/sequent/aggregates_that_need_snapshots_v01.sql +12 -0
  7. data/db/migrate/sequent/command_records_v01.sql +11 -0
  8. data/db/migrate/sequent/delete_all_snapshots_v01.sql +9 -0
  9. data/db/migrate/sequent/delete_snapshots_before_v01.sql +14 -0
  10. data/db/migrate/sequent/enrich_command_json_v01.sql +14 -0
  11. data/db/migrate/sequent/enrich_event_json_v01.sql +11 -0
  12. data/db/migrate/sequent/event_records_v01.sql +13 -0
  13. data/db/migrate/sequent/load_event_v01.sql +19 -0
  14. data/db/migrate/sequent/load_events_v01.sql +40 -0
  15. data/db/migrate/sequent/load_latest_snapshots_v01.sql +12 -0
  16. data/db/migrate/sequent/permanently_delete_commands_without_events_v01.sql +13 -0
  17. data/db/migrate/sequent/permanently_delete_event_streams_v01.sql +13 -0
  18. data/db/migrate/sequent/save_events_trigger_v01.sql +53 -0
  19. data/db/migrate/sequent/select_aggregates_for_snapshotting_v01.sql +19 -0
  20. data/db/migrate/sequent/store_aggregates_v01.sql +39 -0
  21. data/db/migrate/sequent/store_command_v01.sql +21 -0
  22. data/db/migrate/sequent/store_events_v01.sql +37 -0
  23. data/db/migrate/sequent/store_events_v02.sql +37 -0
  24. data/db/migrate/sequent/store_snapshots_v01.sql +30 -0
  25. data/db/migrate/sequent/stream_records_v01.sql +7 -0
  26. data/db/migrate/sequent/update_types_v01.sql +32 -0
  27. data/db/migrate/sequent/update_unique_keys_v01.sql +33 -0
  28. data/db/sequent_pgsql.sql +22 -440
  29. data/db/structure.sql +1358 -0
  30. data/lib/sequent/core/command_record.rb +0 -1
  31. data/lib/sequent/core/event_record.rb +0 -1
  32. data/lib/sequent/core/event_store.rb +64 -6
  33. data/lib/sequent/core/helpers/message_router.rb +9 -6
  34. data/lib/sequent/core/persistors/active_record_persistor.rb +1 -22
  35. data/lib/sequent/generator/template_project/db/database.yml +1 -3
  36. data/lib/sequent/generator/template_project/spec/spec_helper.rb +0 -1
  37. data/lib/sequent/migrations/sequent_schema.rb +2 -11
  38. data/lib/sequent/migrations/view_schema.rb +1 -1
  39. data/lib/sequent/rake/migration_files.rb +69 -0
  40. data/lib/sequent/rake/migration_tasks.rb +71 -9
  41. data/lib/sequent/support/database.rb +12 -31
  42. data/lib/sequent/test/command_handler_helpers.rb +14 -1
  43. data/lib/sequent/util/dry_run.rb +8 -0
  44. data/lib/version.rb +1 -1
  45. metadata +29 -2
  46. data/db/migrate/20250108162754_aggregate_unique_keys.rb +0 -31
@@ -20,7 +20,6 @@ module Sequent
20
20
 
21
21
  # optional attributes (here for historic reasons)
22
22
  # this should be moved to a configurable CommandSerializer
23
- self.organization_id = command.organization_id if serialize_attribute?(command, :organization_id)
24
23
  self.event_aggregate_id = command.event_aggregate_id if serialize_attribute?(command, :event_aggregate_id)
25
24
  self.event_sequence_number = command.event_sequence_number if serialize_attribute?(
26
25
  command,
@@ -53,7 +53,6 @@ module Sequent
53
53
  def event=(event)
54
54
  self.aggregate_id = event.aggregate_id
55
55
  self.sequence_number = event.sequence_number
56
- self.organization_id = event.organization_id if event.respond_to?(:organization_id)
57
56
  self.event_type = event.class.name
58
57
  self.created_at = event.created_at
59
58
  self.event_json = serialize_json? ? self.class.serialize_to_json(event) : event.attributes
@@ -10,6 +10,25 @@ require_relative 'snapshot_store'
10
10
  module Sequent
11
11
  module Core
12
12
  class AggregateKeyNotUniqueError < RuntimeError
13
+ attr_reader :aggregate_type, :aggregate_id
14
+
15
+ def self.unique_key_error_message?(message)
16
+ message =~ /duplicate unique key value for aggregate/
17
+ end
18
+
19
+ def initialize(message)
20
+ super
21
+
22
+ match = message.match(
23
+ # rubocop:disable Layout/LineLength
24
+ /aggregate (\p{Upper}\p{Alnum}*(?:::\p{Upper}\p{Alnum}*)*) (\p{XDigit}{8}-\p{XDigit}{4}-\p{XDigit}{4}-\p{XDigit}{4}-\p{XDigit}{12})/,
25
+ # rubocop:enable Layout/LineLength
26
+ )
27
+ if match
28
+ @aggregate_type = match[1]
29
+ @aggregate_id = match[2]
30
+ end
31
+ end
13
32
  end
14
33
 
15
34
  class EventStore
@@ -201,6 +220,47 @@ module Sequent
201
220
  ]
202
221
  end
203
222
 
223
+ # Returns an enumerator that yields aggregate ids in blocks of `group_size` arrays. Optionally the
224
+ # aggregate root type can be specified (as a string) to only yield aggregate ids of the indicated type.
225
+ def event_streams_enumerator(aggregate_type: nil, group_size: 100)
226
+ Enumerator.new do |yielder|
227
+ last_aggregate_id = nil
228
+ loop do
229
+ aggregate_rows = ActiveRecord::Base.connection.exec_query(
230
+ 'SELECT aggregate_id
231
+ FROM aggregates
232
+ WHERE ($1::text IS NULL OR aggregate_type_id = (SELECT id FROM aggregate_types WHERE type = $1))
233
+ AND ($3::uuid IS NULL OR aggregate_id > $3)
234
+ ORDER BY 1
235
+ LIMIT $2',
236
+ 'aggregates_to_update',
237
+ [
238
+ aggregate_type,
239
+ group_size,
240
+ last_aggregate_id,
241
+ ],
242
+ ).to_a
243
+ break if aggregate_rows.empty?
244
+
245
+ last_aggregate_id = aggregate_rows.last['aggregate_id']
246
+
247
+ yielder << aggregate_rows.map { |x| x['aggregate_id'] }
248
+ end
249
+ end
250
+ end
251
+
252
+ def update_unique_keys(event_streams)
253
+ fail ArgumentError, 'array of stream records expected' unless event_streams.all? { |x| x.is_a?(EventStream) }
254
+
255
+ call_procedure(connection, 'update_unique_keys', [event_streams.to_json])
256
+ rescue ActiveRecord::RecordNotUnique => e
257
+ if AggregateKeyNotUniqueError.unique_key_error_message?(e.message)
258
+ raise AggregateKeyNotUniqueError, e.message
259
+ else
260
+ raise OptimisticLockingError
261
+ end
262
+ end
263
+
204
264
  def permanently_delete_event_stream(aggregate_id)
205
265
  permanently_delete_event_streams([aggregate_id])
206
266
  end
@@ -209,12 +269,10 @@ module Sequent
209
269
  call_procedure(connection, 'permanently_delete_event_streams', [aggregate_ids.to_json])
210
270
  end
211
271
 
212
- def permanently_delete_commands_without_events(aggregate_id: nil, organization_id: nil)
213
- unless aggregate_id || organization_id
214
- fail ArgumentError, 'aggregate_id and/or organization_id must be specified'
215
- end
272
+ def permanently_delete_commands_without_events(aggregate_id:)
273
+ fail ArgumentError, 'aggregate_id must be specified' unless aggregate_id
216
274
 
217
- call_procedure(connection, 'permanently_delete_commands_without_events', [aggregate_id, organization_id])
275
+ call_procedure(connection, 'permanently_delete_commands_without_events', [aggregate_id])
218
276
  end
219
277
 
220
278
  private
@@ -277,7 +335,7 @@ module Sequent
277
335
  ],
278
336
  )
279
337
  rescue ActiveRecord::RecordNotUnique => e
280
- if e.message =~ /duplicate unique key value for aggregate/
338
+ if AggregateKeyNotUniqueError.unique_key_error_message?(e.message)
281
339
  raise AggregateKeyNotUniqueError, e.message
282
340
  else
283
341
  raise OptimisticLockingError
@@ -20,11 +20,13 @@ module Sequent
20
20
  # or a falsey value otherwise.
21
21
  #
22
22
  def register_matchers(*matchers, handler)
23
+ fail ArgumentError, 'handler is required' if handler.nil?
24
+
23
25
  matchers.each do |matcher|
24
26
  if matcher.is_a?(MessageMatchers::InstanceOf)
25
- @instanceof_routes[matcher.expected_class] << handler
27
+ (@instanceof_routes[matcher.expected_class] ||= Set.new) << handler
26
28
  else
27
- @routes[matcher] << handler
29
+ (@routes[matcher] ||= Set.new) << handler
28
30
  end
29
31
  end
30
32
  end
@@ -34,7 +36,7 @@ module Sequent
34
36
  #
35
37
  def match_message(message)
36
38
  result = Set.new
37
- result.merge(@instanceof_routes[message.class])
39
+ result.merge(@instanceof_routes[message.class]) if @instanceof_routes.include?(message.class)
38
40
  @routes.each do |matcher, handlers|
39
41
  result.merge(handlers) if matcher.matches_message?(message)
40
42
  end
@@ -45,15 +47,16 @@ module Sequent
45
47
  # Returns true when there is at least one handler for the given message, or false otherwise.
46
48
  #
47
49
  def matches_message?(message)
48
- match_message(message).any?
50
+ @instanceof_routes.include?(message.class) ||
51
+ @routes.keys.any? { |matcher| matcher.matches_message?(message) }
49
52
  end
50
53
 
51
54
  ##
52
55
  # Removes all routes from the router.
53
56
  #
54
57
  def clear_routes
55
- @instanceof_routes = Hash.new { |h, k| h[k] = Set.new }
56
- @routes = Hash.new { |h, k| h[k] = Set.new }
58
+ @instanceof_routes = {}
59
+ @routes = {}
57
60
  end
58
61
  end
59
62
  end
@@ -45,20 +45,7 @@ module Sequent
45
45
  end
46
46
 
47
47
  def create_records(record_class, array_of_value_hashes)
48
- table = record_class.arel_table
49
-
50
- query = array_of_value_hashes.map do |values|
51
- insert_manager = new_insert_manager
52
- insert_manager.into(table)
53
- insert_manager.insert(
54
- values.map do |key, value|
55
- convert_to_values(key, table, value)
56
- end,
57
- )
58
- insert_manager.to_sql
59
- end.join(';')
60
-
61
- execute_sql(query)
48
+ record_class.unscoped.insert_all!(array_of_value_hashes, returning: false)
62
49
  end
63
50
 
64
51
  def create_or_update_record(record_class, values, created_at = Time.now)
@@ -130,14 +117,6 @@ module Sequent
130
117
  def new_record(record_class, values)
131
118
  record_class.unscoped.new(values)
132
119
  end
133
-
134
- def new_insert_manager
135
- Arel::InsertManager.new
136
- end
137
-
138
- def convert_to_values(key, table, value)
139
- [table[key], table.type_cast_for_database(key, value)]
140
- end
141
120
  end
142
121
  end
143
122
  end
@@ -1,9 +1,8 @@
1
1
  database: &database
2
2
  adapter: postgresql
3
3
  host: localhost
4
- port: 5432
5
4
  timeout: 5000
6
- schema_search_path: "sequent_schema, view_schema"
5
+ schema_search_path: "public, sequent_schema, view_schema"
7
6
 
8
7
  development:
9
8
  <<: *database
@@ -14,4 +13,3 @@ test:
14
13
  <<: *database
15
14
  pool: 5
16
15
  database: my_app_test
17
-
@@ -23,7 +23,6 @@ RSpec.configure do |config|
23
23
  config.before :suite do
24
24
  Sequent::Support::Database.connect!('test')
25
25
  Sequent::Support::Database.drop_schema!(Sequent.configuration.view_schema_name)
26
- Sequent::Support::Database.drop_schema!(Sequent.configuration.event_store_schema_name)
27
26
 
28
27
  Sequent::Test::DatabaseHelpers.maintain_test_database_schema(env: 'test')
29
28
  end
@@ -14,8 +14,7 @@ module Sequent
14
14
  def create_sequent_schema_if_not_exists(env:, fail_if_exists: true)
15
15
  fail ArgumentError, 'env is required' if env.blank?
16
16
 
17
- db_config = Sequent::Support::Database.read_config(env)
18
-
17
+ db_config = Sequent::Support::Database.read_database_config(env)
19
18
  Sequent::Support::Database.establish_connection(db_config)
20
19
 
21
20
  event_store_schema = Sequent.configuration.event_store_schema_name
@@ -25,15 +24,7 @@ module Sequent
25
24
  FAIL_IF_EXISTS.call(event_store_schema) if schema_exists && fail_if_exists
26
25
  return if schema_exists
27
26
 
28
- sequent_schema = File.join(Sequent.configuration.database_schema_directory, "#{event_store_schema}.rb")
29
- unless File.exist?(sequent_schema)
30
- fail "File '#{sequent_schema}' does not exist. Check your Sequent configuration."
31
- end
32
-
33
- Sequent::Support::Database.create_schema(event_store_schema)
34
- Sequent::Support::Database.with_schema_search_path(event_store_schema, db_config, env) do
35
- load(sequent_schema)
36
- end
27
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config, :sql)
37
28
  end
38
29
  end
39
30
  end
@@ -383,7 +383,7 @@ module Sequent
383
383
  end
384
384
 
385
385
  def in_view_schema(&block)
386
- Sequent::Support::Database.with_schema_search_path(view_schema, db_config, &block)
386
+ Sequent::Support::Database.with_search_path(view_schema, &block)
387
387
  end
388
388
 
389
389
  def drop_old_tables(new_version)
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Rake
5
+ class MigrationFiles
6
+ MIGRATION_DIRECTORY = File.realpath(File.join(__dir__, '../../../db/migrate'))
7
+
8
+ def copy(to)
9
+ FileUtils.mkdir_p(to)
10
+ now = Time.current.strftime('%Y%m%d%H%M%S')
11
+ current_entries = current_migration_files(to)
12
+
13
+ Dir
14
+ .entries(MIGRATION_DIRECTORY)
15
+ .reject { |dir| dir.start_with?('.') }
16
+ .sort
17
+ .each_with_index do |file, index|
18
+ full_file_name = File.join(MIGRATION_DIRECTORY, file)
19
+
20
+ if File.directory?(full_file_name)
21
+ copy_directory(file, MIGRATION_DIRECTORY, to)
22
+ else
23
+ _timestamp, *file_parts = file.split('_')
24
+ next if current_entries.include?(file_parts.join('_'))
25
+
26
+ file_name = [(now.to_i + index).to_s, *file_parts].join('_')
27
+ destination_file_name = File.join(to, file_name)
28
+ FileUtils.cp(full_file_name, destination_file_name, preserve: true, verbose: true)
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def current_migration_files(to)
36
+ Dir
37
+ .entries(to)
38
+ .reject { |f| f.start_with?('.') }
39
+ .map do |f|
40
+ _timestamp, *file_parts = f.split('_')
41
+ file_parts.join('_')
42
+ end
43
+ end
44
+
45
+ def copy_directory(directory_name, from, to)
46
+ source = File.join(from, directory_name)
47
+ dest = File.join(to, directory_name)
48
+ FileUtils.mkdir_p(dest)
49
+
50
+ existing = Dir.entries(dest)
51
+
52
+ Dir
53
+ .entries(source)
54
+ .reject { |file| file.start_with?('.') }
55
+ .sort
56
+ .each do |file|
57
+ full_file_name = File.join(source, file)
58
+ if File.directory?(full_file_name)
59
+ copy_directory(file, source, dest)
60
+ else
61
+ next if existing.include?(file)
62
+
63
+ FileUtils.cp(File.join(source, file), File.join(dest, file), preserve: true, verbose: true)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -7,14 +7,35 @@ require 'rake/tasklib'
7
7
  require 'sequent/support'
8
8
  require 'sequent/migrations/view_schema'
9
9
  require 'sequent/migrations/sequent_schema'
10
+ require_relative 'migration_files'
10
11
 
11
12
  module Sequent
12
13
  module Rake
13
14
  class MigrationTasks < ::Rake::TaskLib
14
15
  include ::Rake::DSL
16
+ include ActiveRecord::Tasks
17
+
18
+ def db_config
19
+ DatabaseTasks.db_dir = Sequent.configuration.database_schema_directory unless defined?(Rails)
20
+ Sequent::Support::Database.read_database_config(@env)
21
+ end
22
+
23
+ def connection
24
+ ensure_sequent_env_set!
25
+ Sequent::Support::Database.establish_connection(db_config)
26
+ end
15
27
 
16
28
  def register_tasks!
17
29
  namespace :sequent do
30
+ namespace :install do
31
+ desc <<~EOS
32
+ Copy (new) Sequent database migration files to your projects migrations directory
33
+ EOS
34
+ task :migrations do
35
+ MigrationFiles.new.copy('./db/migrate')
36
+ end
37
+ end
38
+
18
39
  desc <<~EOS
19
40
  Set the SEQUENT_ENV to RAILS_ENV or RACK_ENV if not already set
20
41
  EOS
@@ -41,11 +62,36 @@ module Sequent
41
62
  desc 'Creates the database and initializes the event_store schema for the current env'
42
63
  task create: ['sequent:init'] do
43
64
  ensure_sequent_env_set!
44
-
45
- db_config = Sequent::Support::Database.read_config(@env)
46
65
  Sequent::Support::Database.create!(db_config)
66
+ end
67
+
68
+ desc 'Apply Sequent event store migrations (NOT view schema projection migrations)'
69
+ task migrate: [:create] do
70
+ ensure_sequent_env_set!
71
+ Sequent::Support::Database.establish_connection(db_config)
72
+ ActiveRecord::MigrationContext.new('db/migrate').migrate
73
+ ::Rake::Task['sequent:db:schema:dump'].invoke
74
+ end
75
+
76
+ namespace :schema do
77
+ desc "Creates the database schema file 'db/structure.sql'"
78
+ task :dump do
79
+ connection
80
+ old_dump_schemas = ActiveRecord.dump_schemas
81
+ begin
82
+ ActiveRecord.dump_schemas = nil
83
+ DatabaseTasks.structure_dump_flags = "--exclude-schema=#{Sequent.configuration.view_schema_name}"
84
+ DatabaseTasks.dump_schema(db_config, :sql)
85
+ ensure
86
+ ActiveRecord.dump_schemas = old_dump_schemas
87
+ end
88
+ end
47
89
 
48
- Sequent::Migrations::SequentSchema.create_sequent_schema_if_not_exists(env: @env, fail_if_exists: true)
90
+ desc "Loads the database schema file 'db/structure.sql'"
91
+ task :load do
92
+ connection
93
+ DatabaseTasks.load_schema(db_config, :sql)
94
+ end
49
95
  end
50
96
 
51
97
  desc 'Drops the database for the current env'
@@ -69,12 +115,6 @@ module Sequent
69
115
  Sequent::Migrations::ViewSchema.create_view_schema_if_not_exists(env: @env)
70
116
  end
71
117
 
72
- desc 'Creates the event_store schema for the current env'
73
- task create_event_store: ['sequent:init'] do
74
- ensure_sequent_env_set!
75
- Sequent::Migrations::SequentSchema.create_sequent_schema_if_not_exists(env: @env, fail_if_exists: true)
76
- end
77
-
78
118
  desc 'Utility tasks that can be used to guard against unsafe usage of rails db:migrate directly'
79
119
  task :dont_use_db_migrate_directly do
80
120
  fail <<~EOS unless ENV['SEQUENT_MIGRATION_SCHEMAS'].present?
@@ -207,6 +247,28 @@ module Sequent
207
247
 
208
248
  view_schema.migrate_dryrun(regex: args[:regex])
209
249
  end
250
+
251
+ desc <<~EOS
252
+ Loads all aggregates of the specified type (if any) and updates the aggregate's unique keys in the database.
253
+
254
+ Use this after adding new unique key constraints to an aggregate to ensure every aggregate's unique keys
255
+ are present in the database.
256
+ EOS
257
+ task :unique_keys, %i[aggregate_type group_size] => ['sequent:init', :init] do |_task, args|
258
+ count = 0
259
+ Sequent.configuration.event_store.event_streams_enumerator(
260
+ aggregate_type: args[:aggregate_type],
261
+ group_size: args[:group_size] || 100,
262
+ ).each do |aggregate_ids|
263
+ Sequent.configuration.transaction_provider.transactional do
264
+ aggregates = Sequent.configuration.aggregate_repository.load_aggregates(aggregate_ids)
265
+ Sequent.configuration.event_store.update_unique_keys(aggregates.map(&:event_stream))
266
+ count += aggregates.size
267
+ printf("\rUpdated unique keys for #{count} aggregates.")
268
+ end
269
+ end
270
+ puts("\nDone.")
271
+ end
210
272
  end
211
273
 
212
274
  namespace :snapshots do
@@ -11,6 +11,8 @@ module Sequent
11
11
  # take in a database configuration). Instance methods assume that a database
12
12
  # connection yet is established.
13
13
  class Database
14
+ include ActiveRecord::Tasks
15
+
14
16
  attr_reader :db_config
15
17
 
16
18
  def self.connect!(env)
@@ -18,43 +20,34 @@ module Sequent
18
20
  establish_connection(db_config)
19
21
  end
20
22
 
21
- def self.read_config(env)
23
+ def self.read_database_config(env)
22
24
  fail ArgumentError, 'env is mandatory' unless env
23
25
 
26
+ DatabaseTasks.db_dir = Sequent.configuration.database_schema_directory unless defined?(Rails)
24
27
  database_yml = File.join(Sequent.configuration.database_config_directory, 'database.yml')
25
28
  config = YAML.safe_load(ERB.new(File.read(database_yml)).result, aliases: true)[env]
26
- ActiveRecord::Base.configurations.resolve(config).configuration_hash.with_indifferent_access
29
+ ActiveRecord::Base.configurations.resolve(config)
27
30
  end
28
31
 
29
- def self.create!(db_config)
30
- establish_connection(db_config, {database: 'postgres'})
31
- ActiveRecord::Base.connection.create_database(get_db_config_attribute(db_config, :database))
32
+ def self.read_config(env)
33
+ read_database_config(env).configuration_hash.with_indifferent_access
32
34
  end
33
35
 
34
- def self.drop!(db_config)
35
- establish_connection(db_config, {database: 'postgres'})
36
- ActiveRecord::Base.connection.drop_database(get_db_config_attribute(db_config, :database))
36
+ def self.create!(db_config)
37
+ DatabaseTasks.create(db_config)
37
38
  end
38
39
 
39
- def self.get_db_config_attribute(db_config, attribute)
40
- if Sequent.configuration.can_use_multiple_databases?
41
- db_config[Sequent.configuration.primary_database_key][attribute]
42
- else
43
- db_config[attribute]
44
- end
40
+ def self.drop!(db_config)
41
+ DatabaseTasks.drop(db_config)
45
42
  end
46
43
 
47
- def self.establish_connection(db_config, db_config_overrides = {})
44
+ def self.establish_connection(db_config)
48
45
  if Sequent.configuration.can_use_multiple_databases?
49
- db_config = db_config.deep_merge(
50
- Sequent.configuration.primary_database_key => db_config_overrides,
51
- ).stringify_keys
52
46
  ActiveRecord::Base.configurations = db_config.stringify_keys
53
47
  ActiveRecord::Base.connects_to database: {
54
48
  Sequent.configuration.primary_database_role => Sequent.configuration.primary_database_key,
55
49
  }
56
50
  else
57
- db_config = db_config.merge(db_config_overrides)
58
51
  ActiveRecord::Base.establish_connection(db_config)
59
52
  end
60
53
  end
@@ -78,18 +71,6 @@ module Sequent
78
71
  execute_sql "DROP SCHEMA if exists #{schema_name} cascade"
79
72
  end
80
73
 
81
- def self.with_schema_search_path(search_path, db_config, env = ENV['SEQUENT_ENV'])
82
- fail ArgumentError, 'env is required' unless env
83
-
84
- disconnect!
85
-
86
- establish_connection(db_config, {schema_search_path: search_path})
87
- yield
88
- ensure
89
- disconnect!
90
- establish_connection(db_config)
91
- end
92
-
93
74
  def self.with_search_path(search_path)
94
75
  old_search_path = ActiveRecord::Base.connection.select_value("SELECT current_setting('search_path')")
95
76
  begin
@@ -89,7 +89,11 @@ module Sequent
89
89
  stream.unique_keys.to_h { |scope, key| [[scope, key], stream.aggregate_id] }
90
90
  end,
91
91
  ) do |_key, id_1, id_2|
92
- fail Sequent::Core::AggregateKeyNotUniqueError if id_1 != id_2
92
+ if id_1 != id_2
93
+ stream, = streams_with_events.find { |s| s[0].aggregate_id == id_2 }
94
+ fail Sequent::Core::AggregateKeyNotUniqueError,
95
+ "duplicate unique key value for aggregate #{stream.aggregate_type} #{stream.aggregate_id}"
96
+ end
93
97
  end
94
98
 
95
99
  streams_with_events.each do |event_stream, events|
@@ -122,6 +126,15 @@ module Sequent
122
126
  [deserialize_events(@stored_events[mark..]), position_mark]
123
127
  end
124
128
 
129
+ def event_streams_enumerator(aggregate_type: nil, group_size: 100)
130
+ @event_streams
131
+ .values
132
+ .select { |es| aggregate_type.nil? || es.aggregate_type == aggregate_type }
133
+ .sort_by { |es| [es.events_partition_key, es.aggregate_id] }
134
+ .map(&:aggregate_id)
135
+ .each_slice(group_size)
136
+ end
137
+
125
138
  private
126
139
 
127
140
  def serialize_events(events)
@@ -36,6 +36,10 @@ module Sequent
36
36
  :load_events,
37
37
  :stream_exists?,
38
38
  :events_exists?,
39
+ :event_streams_enumerator,
40
+ :find_event_stream,
41
+ :position_mark,
42
+ :load_events_since_marked_position,
39
43
  to: :event_store
40
44
 
41
45
  def initialize(result, event_store)
@@ -50,6 +54,10 @@ module Sequent
50
54
  new_events = streams_with_events.flat_map { |_, events| events }
51
55
  @result.published_command_with_events(command, new_events)
52
56
  end
57
+
58
+ def update_unique_keys(event_streams)
59
+ # no-op
60
+ end
53
61
  end
54
62
 
55
63
  ##
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sequent
4
- VERSION = '8.1.1'
4
+ VERSION = '8.2.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequent
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.1
4
+ version: 8.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lars Vonk
@@ -369,13 +369,39 @@ extensions: []
369
369
  extra_rdoc_files: []
370
370
  files:
371
371
  - bin/sequent
372
- - db/migrate/20250108162754_aggregate_unique_keys.rb
372
+ - db/migrate/20250101000000_sequent_initial_schema.rb
373
+ - db/migrate/20250101000001_sequent_stored_procedures.rb
374
+ - db/migrate/20250312105100_sequent_store_events_v02.rb
375
+ - db/migrate/sequent/aggregate_event_type_v01.sql
376
+ - db/migrate/sequent/aggregates_that_need_snapshots_v01.sql
377
+ - db/migrate/sequent/command_records_v01.sql
378
+ - db/migrate/sequent/delete_all_snapshots_v01.sql
379
+ - db/migrate/sequent/delete_snapshots_before_v01.sql
380
+ - db/migrate/sequent/enrich_command_json_v01.sql
381
+ - db/migrate/sequent/enrich_event_json_v01.sql
382
+ - db/migrate/sequent/event_records_v01.sql
383
+ - db/migrate/sequent/load_event_v01.sql
384
+ - db/migrate/sequent/load_events_v01.sql
385
+ - db/migrate/sequent/load_latest_snapshots_v01.sql
386
+ - db/migrate/sequent/permanently_delete_commands_without_events_v01.sql
387
+ - db/migrate/sequent/permanently_delete_event_streams_v01.sql
388
+ - db/migrate/sequent/save_events_trigger_v01.sql
389
+ - db/migrate/sequent/select_aggregates_for_snapshotting_v01.sql
390
+ - db/migrate/sequent/store_aggregates_v01.sql
391
+ - db/migrate/sequent/store_command_v01.sql
392
+ - db/migrate/sequent/store_events_v01.sql
393
+ - db/migrate/sequent/store_events_v02.sql
394
+ - db/migrate/sequent/store_snapshots_v01.sql
395
+ - db/migrate/sequent/stream_records_v01.sql
396
+ - db/migrate/sequent/update_types_v01.sql
397
+ - db/migrate/sequent/update_unique_keys_v01.sql
373
398
  - db/sequent_8_migration.sql
374
399
  - db/sequent_pgsql.sql
375
400
  - db/sequent_schema.rb
376
401
  - db/sequent_schema_indexes.sql
377
402
  - db/sequent_schema_partitions.sql
378
403
  - db/sequent_schema_tables.sql
404
+ - db/structure.sql
379
405
  - lib/notices.rb
380
406
  - lib/sequent.rb
381
407
  - lib/sequent/application_record.rb
@@ -510,6 +536,7 @@ files:
510
536
  - lib/sequent/migrations/sql.rb
511
537
  - lib/sequent/migrations/versions.rb
512
538
  - lib/sequent/migrations/view_schema.rb
539
+ - lib/sequent/rake/migration_files.rb
513
540
  - lib/sequent/rake/migration_tasks.rb
514
541
  - lib/sequent/sequent.rb
515
542
  - lib/sequent/support.rb
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AggregateUniqueKeys < ActiveRecord::Migration[7.2]
4
- def up
5
- Sequent::Support::Database.with_search_path(Sequent.configuration.event_store_schema_name) do
6
- say 'Creating aggregate_unique_keys table', true
7
- suppress_messages do
8
- execute <<~SQL
9
- CREATE TABLE IF NOT EXISTS aggregate_unique_keys (
10
- aggregate_id uuid NOT NULL,
11
- scope text NOT NULL,
12
- key jsonb NOT NULL,
13
- PRIMARY KEY (aggregate_id, scope),
14
- UNIQUE (scope, key),
15
- FOREIGN KEY (aggregate_id) REFERENCES aggregates (aggregate_id) ON UPDATE CASCADE ON DELETE CASCADE
16
- )
17
- SQL
18
- end
19
-
20
- say 'Creating event store stored procedures and views', true
21
- suppress_messages do
22
- sequent_pgsql_filename = File.join(Sequent.configuration.database_schema_directory, 'sequent_pgsql.sql')
23
- execute File.read(sequent_pgsql_filename)
24
- end
25
- end
26
- end
27
-
28
- def down
29
- fail ActiveRecord::IrreversibleMigration
30
- end
31
- end