sequent 7.1.1 → 8.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.
- checksums.yaml +4 -4
- data/bin/sequent +6 -107
- data/db/sequent_8_migration.sql +120 -0
- data/db/sequent_pgsql.sql +416 -0
- data/db/sequent_schema.rb +11 -57
- data/db/sequent_schema_indexes.sql +37 -0
- data/db/sequent_schema_partitions.sql +34 -0
- data/db/sequent_schema_tables.sql +74 -0
- data/lib/sequent/cli/app.rb +132 -0
- data/lib/sequent/cli/sequent_8_migration.rb +180 -0
- data/lib/sequent/configuration.rb +11 -8
- data/lib/sequent/core/aggregate_repository.rb +2 -2
- data/lib/sequent/core/aggregate_root.rb +32 -9
- data/lib/sequent/core/aggregate_snapshotter.rb +8 -6
- data/lib/sequent/core/command_record.rb +27 -18
- data/lib/sequent/core/command_service.rb +2 -2
- data/lib/sequent/core/event_publisher.rb +1 -1
- data/lib/sequent/core/event_record.rb +37 -17
- data/lib/sequent/core/event_store.rb +101 -119
- data/lib/sequent/core/helpers/array_with_type.rb +1 -1
- data/lib/sequent/core/helpers/association_validator.rb +2 -2
- data/lib/sequent/core/helpers/attribute_support.rb +8 -8
- data/lib/sequent/core/helpers/equal_support.rb +3 -3
- data/lib/sequent/core/helpers/message_matchers/has_attrs.rb +2 -0
- data/lib/sequent/core/helpers/message_router.rb +2 -2
- data/lib/sequent/core/helpers/param_support.rb +1 -3
- data/lib/sequent/core/helpers/pgsql_helpers.rb +32 -0
- data/lib/sequent/core/helpers/string_support.rb +1 -1
- data/lib/sequent/core/helpers/string_to_value_parsers.rb +1 -1
- data/lib/sequent/core/persistors/active_record_persistor.rb +1 -1
- data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +3 -4
- data/lib/sequent/core/projector.rb +1 -1
- data/lib/sequent/core/snapshot_record.rb +44 -0
- data/lib/sequent/core/snapshot_store.rb +105 -0
- data/lib/sequent/core/stream_record.rb +10 -15
- data/lib/sequent/dry_run/read_only_replay_optimized_postgres_persistor.rb +1 -1
- data/lib/sequent/dry_run/view_schema.rb +2 -3
- data/lib/sequent/generator/project.rb +5 -7
- data/lib/sequent/generator/template_aggregate/template_aggregate/commands.rb +2 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate/events.rb +2 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate.rb +2 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate_command_handler.rb +2 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate.rb +2 -0
- data/lib/sequent/generator/template_project/Gemfile +7 -5
- data/lib/sequent/generator/template_project/Rakefile +4 -2
- data/lib/sequent/generator/template_project/app/projectors/post_projector.rb +2 -0
- data/lib/sequent/generator/template_project/app/records/post_record.rb +2 -0
- data/lib/sequent/generator/template_project/config/initializers/sequent.rb +3 -8
- data/lib/sequent/generator/template_project/db/migrations.rb +3 -3
- data/lib/sequent/generator/template_project/lib/post/commands.rb +2 -0
- data/lib/sequent/generator/template_project/lib/post/events.rb +2 -0
- data/lib/sequent/generator/template_project/lib/post/post.rb +2 -0
- data/lib/sequent/generator/template_project/lib/post/post_command_handler.rb +2 -0
- data/lib/sequent/generator/template_project/lib/post.rb +2 -0
- data/lib/sequent/generator/template_project/my_app.rb +2 -1
- data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +2 -0
- data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +9 -2
- data/lib/sequent/generator/template_project/spec/spec_helper.rb +4 -7
- data/lib/sequent/generator.rb +1 -1
- data/lib/sequent/internal/aggregate_type.rb +12 -0
- data/lib/sequent/internal/command_type.rb +12 -0
- data/lib/sequent/internal/event_type.rb +12 -0
- data/lib/sequent/internal/internal.rb +14 -0
- data/lib/sequent/internal/partitioned_aggregate.rb +26 -0
- data/lib/sequent/internal/partitioned_command.rb +16 -0
- data/lib/sequent/internal/partitioned_event.rb +29 -0
- data/lib/sequent/migrations/grouper.rb +90 -0
- data/lib/sequent/migrations/sequent_schema.rb +2 -1
- data/lib/sequent/migrations/view_schema.rb +76 -77
- data/lib/sequent/rake/migration_tasks.rb +49 -24
- data/lib/sequent/sequent.rb +1 -0
- data/lib/sequent/support/database.rb +20 -16
- data/lib/sequent/test/time_comparison.rb +1 -1
- data/lib/sequent/util/timer.rb +1 -1
- data/lib/version.rb +1 -1
- metadata +102 -21
- data/lib/sequent/generator/template_project/db/sequent_schema.rb +0 -52
- data/lib/sequent/generator/template_project/ruby-version +0 -1
@@ -12,8 +12,7 @@ module Sequent
|
|
12
12
|
module ClassMethods
|
13
13
|
##
|
14
14
|
# Enable snapshots for this aggregate. The aggregate instance
|
15
|
-
# must define the *
|
16
|
-
# methods.
|
15
|
+
# must define the *take_snapshot* methods.
|
17
16
|
#
|
18
17
|
def enable_snapshots(default_threshold: 20)
|
19
18
|
@snapshot_default_threshold = default_threshold
|
@@ -41,7 +40,8 @@ module Sequent
|
|
41
40
|
include SnapshotConfiguration
|
42
41
|
extend ActiveSupport::DescendantsTracker
|
43
42
|
|
44
|
-
attr_reader :id, :uncommitted_events, :sequence_number
|
43
|
+
attr_reader :id, :uncommitted_events, :sequence_number
|
44
|
+
attr_accessor :latest_snapshot_sequence_number
|
45
45
|
|
46
46
|
def self.load_from_history(stream, events)
|
47
47
|
first, *rest = events
|
@@ -49,6 +49,7 @@ module Sequent
|
|
49
49
|
# rubocop:disable Security/MarshalLoad
|
50
50
|
aggregate_root = Marshal.load(Base64.decode64(first.data))
|
51
51
|
# rubocop:enable Security/MarshalLoad
|
52
|
+
aggregate_root.latest_snapshot_sequence_number = first.sequence_number
|
52
53
|
rest.each { |x| aggregate_root.apply_event(x) }
|
53
54
|
else
|
54
55
|
aggregate_root = allocate # allocate without calling new
|
@@ -61,9 +62,6 @@ module Sequent
|
|
61
62
|
@id = id
|
62
63
|
@uncommitted_events = []
|
63
64
|
@sequence_number = 1
|
64
|
-
@event_stream = EventStream.new aggregate_type: self.class.name,
|
65
|
-
aggregate_id: id,
|
66
|
-
snapshot_threshold: self.class.snapshot_default_threshold
|
67
65
|
end
|
68
66
|
|
69
67
|
def load_from_history(stream, events)
|
@@ -100,13 +98,38 @@ module Sequent
|
|
100
98
|
"#{self.class.name}: #{@id}"
|
101
99
|
end
|
102
100
|
|
101
|
+
def event_stream
|
102
|
+
EventStream.new(
|
103
|
+
aggregate_type: self.class.name,
|
104
|
+
aggregate_id: id,
|
105
|
+
events_partition_key: events_partition_key,
|
106
|
+
snapshot_outdated_at: snapshot_outdated? ? Time.now : nil,
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Provide the partitioning key for storing events. This value
|
111
|
+
# must be a string and will be used by PostgreSQL to store the
|
112
|
+
# events in the right partition.
|
113
|
+
#
|
114
|
+
# The value may change over the lifetime of the aggregate, old
|
115
|
+
# events will be moved to the correct partition after a
|
116
|
+
# change. This can be an expensive database operation.
|
117
|
+
def events_partition_key
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
103
121
|
def clear_events
|
104
122
|
@uncommitted_events = []
|
105
123
|
end
|
106
124
|
|
107
|
-
def
|
108
|
-
|
109
|
-
@
|
125
|
+
def snapshot_outdated?
|
126
|
+
snapshot_threshold = self.class.snapshot_default_threshold
|
127
|
+
events_since_latest_snapshot = @sequence_number - (latest_snapshot_sequence_number || 1)
|
128
|
+
snapshot_threshold.present? && events_since_latest_snapshot >= snapshot_threshold
|
129
|
+
end
|
130
|
+
|
131
|
+
def take_snapshot
|
132
|
+
build_event SnapshotEvent, data: Base64.encode64(Marshal.dump(self))
|
110
133
|
end
|
111
134
|
|
112
135
|
def apply_event(event)
|
@@ -24,23 +24,25 @@ module Sequent
|
|
24
24
|
@last_aggregate_id,
|
25
25
|
command.limit,
|
26
26
|
)
|
27
|
-
aggregate_ids.
|
28
|
-
|
29
|
-
|
27
|
+
snapshots = aggregate_ids.filter_map { |aggregate_id| take_snapshot(aggregate_id) }
|
28
|
+
Sequent.configuration.event_store.store_snapshots(snapshots)
|
29
|
+
|
30
30
|
@last_aggregate_id = aggregate_ids.last
|
31
31
|
throw :done if @last_aggregate_id.nil?
|
32
32
|
end
|
33
33
|
|
34
34
|
on TakeSnapshot do |command|
|
35
|
-
take_snapshot
|
35
|
+
snapshot = take_snapshot(command.aggregate_id)
|
36
|
+
Sequent.configuration.event_store.store_snapshots([snapshot]) if snapshot
|
36
37
|
end
|
37
38
|
|
38
|
-
def take_snapshot
|
39
|
+
def take_snapshot(aggregate_id)
|
39
40
|
aggregate = repository.load_aggregate(aggregate_id)
|
40
41
|
Sequent.logger.info "Taking snapshot for aggregate #{aggregate}"
|
41
|
-
aggregate.take_snapshot
|
42
|
+
aggregate.take_snapshot
|
42
43
|
rescue StandardError => e
|
43
44
|
Sequent.logger.error("Failed to take snapshot for aggregate #{aggregate_id}: #{e}, #{e.inspect}")
|
45
|
+
nil
|
44
46
|
end
|
45
47
|
end
|
46
48
|
end
|
@@ -7,7 +7,7 @@ module Sequent
|
|
7
7
|
module Core
|
8
8
|
module SerializesCommand
|
9
9
|
def command
|
10
|
-
args = Sequent::Core::Oj.strict_load(command_json)
|
10
|
+
args = serialize_json? ? Sequent::Core::Oj.strict_load(command_json) : command_json
|
11
11
|
Class.const_get(command_type).deserialize_from_json(args)
|
12
12
|
end
|
13
13
|
|
@@ -16,7 +16,7 @@ module Sequent
|
|
16
16
|
self.aggregate_id = command.aggregate_id if command.respond_to? :aggregate_id
|
17
17
|
self.user_id = command.user_id if command.respond_to? :user_id
|
18
18
|
self.command_type = command.class.name
|
19
|
-
self.command_json = Sequent::Core::Oj.dump(command.attributes)
|
19
|
+
self.command_json = serialize_json? ? Sequent::Core::Oj.dump(command.attributes) : command.attributes
|
20
20
|
|
21
21
|
# optional attributes (here for historic reasons)
|
22
22
|
# this should be moved to a configurable CommandSerializer
|
@@ -30,6 +30,13 @@ module Sequent
|
|
30
30
|
|
31
31
|
private
|
32
32
|
|
33
|
+
def serialize_json?
|
34
|
+
return true unless self.class.respond_to? :columns_hash
|
35
|
+
|
36
|
+
json_column_type = self.class.columns_hash['command_json'].sql_type_metadata.type
|
37
|
+
%i[json jsonb].exclude? json_column_type
|
38
|
+
end
|
39
|
+
|
33
40
|
def serialize_attribute?(command, attribute)
|
34
41
|
[self, command].all? { |obj| obj.respond_to?(attribute) }
|
35
42
|
end
|
@@ -39,32 +46,34 @@ module Sequent
|
|
39
46
|
class CommandRecord < Sequent::ApplicationRecord
|
40
47
|
include SerializesCommand
|
41
48
|
|
49
|
+
self.primary_key = :id
|
42
50
|
self.table_name = 'command_records'
|
43
51
|
|
44
|
-
has_many :
|
52
|
+
has_many :child_events,
|
53
|
+
inverse_of: :parent_command,
|
54
|
+
class_name: :EventRecord,
|
55
|
+
foreign_key: :command_record_id
|
45
56
|
|
46
57
|
validates_presence_of :command_type, :command_json
|
47
58
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
.first
|
53
|
-
end
|
59
|
+
# A `belongs_to` association fails in weird ways with ActiveRecord 7.1, probably due to the use of composite
|
60
|
+
# primary keys so use an explicit query here and cache the result.
|
61
|
+
def parent_event
|
62
|
+
return nil unless event_aggregate_id && event_sequence_number
|
54
63
|
|
55
|
-
|
56
|
-
event_records
|
64
|
+
@parent_event ||= EventRecord.find_by(aggregate_id: event_aggregate_id, sequence_number: event_sequence_number)
|
57
65
|
end
|
58
66
|
|
59
|
-
def
|
60
|
-
|
67
|
+
def origin_command
|
68
|
+
parent_event&.parent_command&.origin_command || self
|
61
69
|
end
|
62
70
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
71
|
+
# @deprecated
|
72
|
+
alias parent parent_event
|
73
|
+
# @deprecated
|
74
|
+
alias children child_events
|
75
|
+
# @deprecated
|
76
|
+
alias origin origin_command
|
68
77
|
end
|
69
78
|
end
|
70
79
|
end
|
@@ -67,7 +67,7 @@ module Sequent
|
|
67
67
|
def process_command(command)
|
68
68
|
fail ArgumentError, 'command is required' if command.nil?
|
69
69
|
|
70
|
-
Sequent.logger.debug("[CommandService] Processing command #{command.class}")
|
70
|
+
Sequent.logger.debug("[CommandService] Processing command #{command.class}") if Sequent.logger.debug?
|
71
71
|
|
72
72
|
filters.each { |filter| filter.execute(command) }
|
73
73
|
|
@@ -118,7 +118,7 @@ module Sequent
|
|
118
118
|
def initialize(command)
|
119
119
|
@command = command
|
120
120
|
msg = @command.respond_to?(:aggregate_id) ? " #{@command.aggregate_id}" : ''
|
121
|
-
super
|
121
|
+
super("Invalid command #{@command.class}#{msg}, errors: #{@command.validation_errors}")
|
122
122
|
end
|
123
123
|
|
124
124
|
def errors(prefix = nil)
|
@@ -51,7 +51,7 @@ module Sequent
|
|
51
51
|
def process_event(event)
|
52
52
|
fail ArgumentError, 'event is required' if event.nil?
|
53
53
|
|
54
|
-
Sequent.logger.debug("[EventPublisher] Publishing event #{event.class}")
|
54
|
+
Sequent.logger.debug("[EventPublisher] Publishing event #{event.class}") if Sequent.logger.debug?
|
55
55
|
|
56
56
|
configuration.event_handlers.each do |handler|
|
57
57
|
handler.handle_message event
|
@@ -46,7 +46,7 @@ module Sequent
|
|
46
46
|
|
47
47
|
module SerializesEvent
|
48
48
|
def event
|
49
|
-
payload = Sequent::Core::Oj.strict_load(event_json)
|
49
|
+
payload = serialize_json? ? Sequent::Core::Oj.strict_load(event_json) : event_json
|
50
50
|
Class.const_get(event_type).deserialize_from_json(payload)
|
51
51
|
end
|
52
52
|
|
@@ -56,7 +56,7 @@ module Sequent
|
|
56
56
|
self.organization_id = event.organization_id if event.respond_to?(:organization_id)
|
57
57
|
self.event_type = event.class.name
|
58
58
|
self.created_at = event.created_at
|
59
|
-
self.event_json = self.class.serialize_to_json(event)
|
59
|
+
self.event_json = serialize_json? ? self.class.serialize_to_json(event) : event.attributes
|
60
60
|
|
61
61
|
Sequent.configuration.event_record_hooks_class.after_serialization(self, event)
|
62
62
|
end
|
@@ -65,42 +65,62 @@ module Sequent
|
|
65
65
|
def serialize_to_json(event)
|
66
66
|
Sequent::Core::Oj.dump(event)
|
67
67
|
end
|
68
|
+
|
69
|
+
def serialize_json?
|
70
|
+
return true unless respond_to? :columns_hash
|
71
|
+
|
72
|
+
json_column_type = columns_hash['event_json'].sql_type_metadata.type
|
73
|
+
%i[json jsonb].exclude? json_column_type
|
74
|
+
end
|
68
75
|
end
|
69
76
|
|
70
77
|
def self.included(host_class)
|
71
78
|
host_class.extend(ClassMethods)
|
72
79
|
end
|
80
|
+
|
81
|
+
def serialize_json?
|
82
|
+
self.class.serialize_json?
|
83
|
+
end
|
73
84
|
end
|
74
85
|
|
75
86
|
class EventRecord < Sequent::ApplicationRecord
|
76
87
|
include SerializesEvent
|
77
88
|
|
89
|
+
self.primary_key = %i[aggregate_id sequence_number]
|
78
90
|
self.table_name = 'event_records'
|
79
91
|
self.ignored_columns = %w[xact_id]
|
80
92
|
|
81
|
-
belongs_to :stream_record
|
82
|
-
belongs_to :command_record
|
93
|
+
belongs_to :stream_record, foreign_key: :aggregate_id, primary_key: :aggregate_id
|
83
94
|
|
84
|
-
|
85
|
-
validates_numericality_of :sequence_number, only_integer: true, greater_than: 0
|
95
|
+
belongs_to :parent_command, class_name: :CommandRecord, foreign_key: :command_record_id
|
86
96
|
|
87
|
-
|
88
|
-
|
97
|
+
if Gem.loaded_specs['activerecord'].version < Gem::Version.create('7.2')
|
98
|
+
has_many :child_commands,
|
99
|
+
class_name: :CommandRecord,
|
100
|
+
primary_key: %i[aggregate_id sequence_number],
|
101
|
+
query_constraints: %i[event_aggregate_id event_sequence_number]
|
102
|
+
else
|
103
|
+
has_many :child_commands,
|
104
|
+
class_name: :CommandRecord,
|
105
|
+
primary_key: %i[aggregate_id sequence_number],
|
106
|
+
foreign_key: %i[event_aggregate_id event_sequence_number]
|
89
107
|
end
|
90
108
|
|
91
|
-
|
92
|
-
|
93
|
-
end
|
109
|
+
validates_presence_of :aggregate_id, :sequence_number, :event_type, :event_json, :stream_record, :parent_command
|
110
|
+
validates_numericality_of :sequence_number, only_integer: true, greater_than: 0
|
94
111
|
|
95
|
-
def
|
96
|
-
|
112
|
+
def self.find_by_event(event)
|
113
|
+
find_by(aggregate_id: event.aggregate_id, sequence_number: event.sequence_number)
|
97
114
|
end
|
98
115
|
|
99
|
-
def
|
100
|
-
|
101
|
-
|
102
|
-
record
|
116
|
+
def origin_command
|
117
|
+
parent_command&.origin_command
|
103
118
|
end
|
119
|
+
|
120
|
+
# @deprecated
|
121
|
+
alias parent parent_command
|
122
|
+
alias children child_commands
|
123
|
+
alias origin origin_command
|
104
124
|
end
|
105
125
|
end
|
106
126
|
end
|
@@ -2,11 +2,16 @@
|
|
2
2
|
|
3
3
|
require 'forwardable'
|
4
4
|
require_relative 'event_record'
|
5
|
+
require_relative 'helpers/pgsql_helpers'
|
5
6
|
require_relative 'sequent_oj'
|
7
|
+
require_relative 'snapshot_record'
|
8
|
+
require_relative 'snapshot_store'
|
6
9
|
|
7
10
|
module Sequent
|
8
11
|
module Core
|
9
12
|
class EventStore
|
13
|
+
include Helpers::PgsqlHelpers
|
14
|
+
include SnapshotStore
|
10
15
|
include ActiveRecord::ConnectionAdapters::Quoting
|
11
16
|
extend Forwardable
|
12
17
|
|
@@ -26,15 +31,6 @@ module Sequent
|
|
26
31
|
end
|
27
32
|
end
|
28
33
|
|
29
|
-
##
|
30
|
-
# Disables event type caching (ie. for in development).
|
31
|
-
#
|
32
|
-
class NoEventTypesCache
|
33
|
-
def fetch_or_store(event_type)
|
34
|
-
yield(event_type)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
34
|
##
|
39
35
|
# Stores the events in the EventStore and publishes the events
|
40
36
|
# to the registered event_handlers.
|
@@ -56,7 +52,7 @@ module Sequent
|
|
56
52
|
end
|
57
53
|
|
58
54
|
##
|
59
|
-
# Returns all events for the AggregateRoot ordered by sequence_number, disregarding
|
55
|
+
# Returns all events for the AggregateRoot ordered by sequence_number, disregarding snapshots.
|
60
56
|
#
|
61
57
|
# This streaming is done in batches to prevent loading many events in memory all at once. A usecase for ignoring
|
62
58
|
# the snapshots is when events of a nested AggregateRoot need to be loaded up until a certain moment in time.
|
@@ -68,16 +64,20 @@ module Sequent
|
|
68
64
|
stream = find_event_stream(aggregate_id)
|
69
65
|
fail ArgumentError, 'no stream found for this aggregate' if stream.blank?
|
70
66
|
|
71
|
-
q = Sequent
|
72
|
-
.configuration
|
73
|
-
.event_record_class
|
74
|
-
.where(aggregate_id: aggregate_id)
|
75
|
-
.where.not(event_type: Sequent.configuration.snapshot_event_class.name)
|
76
|
-
.order(:sequence_number)
|
77
|
-
q = q.where('created_at < ?', load_until) if load_until.present?
|
78
67
|
has_events = false
|
79
68
|
|
80
|
-
|
69
|
+
# PostgreSQLCursor::Cursor does not support bind parameters, so bind parameters manually instead.
|
70
|
+
sql = ActiveRecord::Base.sanitize_sql_array(
|
71
|
+
[
|
72
|
+
'SELECT * FROM load_events(:aggregate_ids, FALSE, :load_until)',
|
73
|
+
{
|
74
|
+
aggregate_ids: [aggregate_id].to_json,
|
75
|
+
load_until: load_until,
|
76
|
+
},
|
77
|
+
],
|
78
|
+
)
|
79
|
+
|
80
|
+
PostgreSQLCursor::Cursor.new(sql, {connection: connection}).each_row do |event_hash|
|
81
81
|
has_events = true
|
82
82
|
event = deserialize_event(event_hash)
|
83
83
|
block.call([stream, event])
|
@@ -85,6 +85,11 @@ module Sequent
|
|
85
85
|
fail ArgumentError, 'no events for this aggregate' unless has_events
|
86
86
|
end
|
87
87
|
|
88
|
+
def load_event(aggregate_id, sequence_number)
|
89
|
+
event_hash = query_function(connection, 'load_event', [aggregate_id, sequence_number]).first
|
90
|
+
deserialize_event(event_hash) if event_hash
|
91
|
+
end
|
92
|
+
|
88
93
|
##
|
89
94
|
# Returns all events for the aggregate ordered by sequence_number, loading them from the latest snapshot
|
90
95
|
# event onwards, if a snapshot is present
|
@@ -96,40 +101,21 @@ module Sequent
|
|
96
101
|
def load_events_for_aggregates(aggregate_ids)
|
97
102
|
return [] if aggregate_ids.none?
|
98
103
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
deserialize_event(event_hash)
|
104
|
-
end
|
105
|
-
|
106
|
-
events
|
107
|
-
.group_by(&:aggregate_id)
|
108
|
-
.map do |aggregate_id, es|
|
104
|
+
query_events(aggregate_ids)
|
105
|
+
.group_by { |row| row['aggregate_id'] }
|
106
|
+
.values
|
107
|
+
.map do |rows|
|
109
108
|
[
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
109
|
+
EventStream.new(
|
110
|
+
aggregate_type: rows.first['aggregate_type'],
|
111
|
+
aggregate_id: rows.first['aggregate_id'],
|
112
|
+
events_partition_key: rows.first['events_partition_key'],
|
113
|
+
),
|
114
|
+
rows.map { |row| deserialize_event(row) },
|
114
115
|
]
|
115
116
|
end
|
116
117
|
end
|
117
118
|
|
118
|
-
def aggregate_query(aggregate_id)
|
119
|
-
<<~SQL.chomp
|
120
|
-
(
|
121
|
-
SELECT event_type, event_json
|
122
|
-
FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS o
|
123
|
-
WHERE aggregate_id = #{quote(aggregate_id)}
|
124
|
-
AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
|
125
|
-
FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS i
|
126
|
-
WHERE event_type = #{quote Sequent.configuration.snapshot_event_class.name}
|
127
|
-
AND i.aggregate_id = #{quote(aggregate_id)}), 0)
|
128
|
-
ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuration.snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
|
129
|
-
)
|
130
|
-
SQL
|
131
|
-
end
|
132
|
-
|
133
119
|
def stream_exists?(aggregate_id)
|
134
120
|
Sequent.configuration.stream_record_class.exists?(aggregate_id: aggregate_id)
|
135
121
|
end
|
@@ -137,6 +123,7 @@ module Sequent
|
|
137
123
|
def events_exists?(aggregate_id)
|
138
124
|
Sequent.configuration.event_record_class.exists?(aggregate_id: aggregate_id)
|
139
125
|
end
|
126
|
+
|
140
127
|
##
|
141
128
|
# Replays all events in the event store to the registered event_handlers.
|
142
129
|
#
|
@@ -164,7 +151,7 @@ module Sequent
|
|
164
151
|
event = deserialize_event(record)
|
165
152
|
publish_events([event])
|
166
153
|
progress += 1
|
167
|
-
ids_replayed << record['
|
154
|
+
ids_replayed << record['aggregate_id']
|
168
155
|
if progress % block_size == 0
|
169
156
|
on_progress[progress, false, ids_replayed]
|
170
157
|
ids_replayed.clear
|
@@ -174,110 +161,105 @@ module Sequent
|
|
174
161
|
end
|
175
162
|
|
176
163
|
PRINT_PROGRESS = ->(progress, done, _) do
|
164
|
+
next unless Sequent.logger.debug?
|
165
|
+
|
177
166
|
if done
|
178
|
-
Sequent.logger.debug
|
167
|
+
Sequent.logger.debug("Done replaying #{progress} events")
|
179
168
|
else
|
180
|
-
Sequent.logger.debug
|
169
|
+
Sequent.logger.debug("Replayed #{progress} events")
|
181
170
|
end
|
182
171
|
end
|
183
172
|
|
184
|
-
##
|
185
|
-
# Returns the ids of aggregates that need a new snapshot.
|
186
|
-
#
|
187
|
-
def aggregates_that_need_snapshots(last_aggregate_id, limit = 10)
|
188
|
-
stream_table = quote_table_name Sequent.configuration.stream_record_class.table_name
|
189
|
-
event_table = quote_table_name Sequent.configuration.event_record_class.table_name
|
190
|
-
query = <<~SQL.chomp
|
191
|
-
SELECT aggregate_id
|
192
|
-
FROM #{stream_table} stream
|
193
|
-
WHERE aggregate_id::varchar > COALESCE(#{quote last_aggregate_id}, '')
|
194
|
-
AND snapshot_threshold IS NOT NULL
|
195
|
-
AND snapshot_threshold <= (
|
196
|
-
(SELECT MAX(events.sequence_number) FROM #{event_table} events WHERE events.event_type <> #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = events.aggregate_id) -
|
197
|
-
COALESCE((SELECT MAX(snapshots.sequence_number) FROM #{event_table} snapshots WHERE snapshots.event_type = #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = snapshots.aggregate_id), 0))
|
198
|
-
ORDER BY aggregate_id
|
199
|
-
LIMIT #{quote limit}
|
200
|
-
FOR UPDATE
|
201
|
-
SQL
|
202
|
-
Sequent.configuration.event_record_class.connection.select_all(query).map { |x| x['aggregate_id'] }
|
203
|
-
end
|
204
|
-
|
205
173
|
def find_event_stream(aggregate_id)
|
206
174
|
record = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_id).first
|
207
175
|
record&.event_stream
|
208
176
|
end
|
209
177
|
|
210
|
-
|
178
|
+
def permanently_delete_event_stream(aggregate_id)
|
179
|
+
permanently_delete_event_streams([aggregate_id])
|
180
|
+
end
|
211
181
|
|
212
|
-
def
|
213
|
-
|
182
|
+
def permanently_delete_event_streams(aggregate_ids)
|
183
|
+
call_procedure(connection, 'permanently_delete_event_streams', [aggregate_ids.to_json])
|
214
184
|
end
|
215
185
|
|
216
|
-
def
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
186
|
+
def permanently_delete_commands_without_events(aggregate_id: nil, organization_id: nil)
|
187
|
+
unless aggregate_id || organization_id
|
188
|
+
fail ArgumentError, 'aggregate_id and/or organization_id must be specified'
|
189
|
+
end
|
190
|
+
|
191
|
+
call_procedure(connection, 'permanently_delete_commands_without_events', [aggregate_id, organization_id])
|
222
192
|
end
|
223
193
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
.column_names
|
229
|
-
.reject { |c| c == primary_key_event_records }
|
194
|
+
private
|
195
|
+
|
196
|
+
def connection
|
197
|
+
Sequent.configuration.event_record_class.connection
|
230
198
|
end
|
231
199
|
|
232
|
-
def
|
233
|
-
|
200
|
+
def query_events(aggregate_ids, use_snapshots = true, load_until = nil)
|
201
|
+
query_function(connection, 'load_events', [aggregate_ids.to_json, use_snapshots, load_until])
|
234
202
|
end
|
235
203
|
|
236
204
|
def deserialize_event(event_hash)
|
237
|
-
|
238
|
-
|
239
|
-
|
205
|
+
should_serialize_json = Sequent.configuration.event_record_class.serialize_json?
|
206
|
+
record = Sequent.configuration.event_record_class.new
|
207
|
+
record.event_type = event_hash.fetch('event_type')
|
208
|
+
record.event_json =
|
209
|
+
if should_serialize_json
|
210
|
+
event_hash.fetch('event_json')
|
211
|
+
else
|
212
|
+
# When the column type is JSON or JSONB the event record
|
213
|
+
# class expects the JSON to be deserialized into a hash
|
214
|
+
# already.
|
215
|
+
Sequent::Core::Oj.strict_load(event_hash.fetch('event_json'))
|
216
|
+
end
|
217
|
+
record.event
|
240
218
|
rescue StandardError
|
241
219
|
raise DeserializeEventError, event_hash
|
242
220
|
end
|
243
221
|
|
244
|
-
def resolve_event_type(event_type)
|
245
|
-
event_types.fetch_or_store(event_type) { |k| Class.const_get(k) }
|
246
|
-
end
|
247
|
-
|
248
222
|
def publish_events(events)
|
249
223
|
Sequent.configuration.event_publisher.publish_events(events)
|
250
224
|
end
|
251
225
|
|
252
226
|
def store_events(command, streams_with_events = [])
|
253
|
-
command_record =
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
227
|
+
command_record = {
|
228
|
+
created_at: convert_timestamp(command.created_at&.to_time || Time.now),
|
229
|
+
command_type: command.class.name,
|
230
|
+
command_json: command,
|
231
|
+
}
|
232
|
+
|
233
|
+
events = streams_with_events.map do |stream, uncommitted_events|
|
234
|
+
[
|
235
|
+
Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(stream)),
|
236
|
+
uncommitted_events.map do |event|
|
237
|
+
{
|
238
|
+
created_at: convert_timestamp(event.created_at.to_time),
|
239
|
+
event_type: event.class.name,
|
240
|
+
event_json: event,
|
241
|
+
}
|
242
|
+
end,
|
243
|
+
]
|
268
244
|
end
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
Sequent.configuration.event_record_class.connection.insert(sql, nil, primary_key_event_records)
|
245
|
+
call_procedure(
|
246
|
+
connection,
|
247
|
+
'store_events',
|
248
|
+
[
|
249
|
+
Sequent::Core::Oj.dump(command_record),
|
250
|
+
Sequent::Core::Oj.dump(events),
|
251
|
+
],
|
252
|
+
)
|
278
253
|
rescue ActiveRecord::RecordNotUnique
|
279
254
|
raise OptimisticLockingError
|
280
255
|
end
|
256
|
+
|
257
|
+
def convert_timestamp(timestamp)
|
258
|
+
# Since ActiveRecord uses `TIMESTAMP WITHOUT TIME ZONE`
|
259
|
+
# we need to manually convert database timestamps to the
|
260
|
+
# ActiveRecord default time zone on serialization.
|
261
|
+
ActiveRecord.default_timezone == :utc ? timestamp.getutc : timestamp.getlocal
|
262
|
+
end
|
281
263
|
end
|
282
264
|
end
|
283
265
|
end
|
@@ -35,7 +35,7 @@ module Sequent
|
|
35
35
|
value = record.instance_variable_get("@#{association}")
|
36
36
|
if value && incorrect_type?(value, record, association)
|
37
37
|
record.errors.add(association, "is not of type #{describe_type(record.class.types[association])}")
|
38
|
-
elsif value
|
38
|
+
elsif value.is_a?(Array)
|
39
39
|
item_type = record.class.types.fetch(association).item_type
|
40
40
|
record.errors.add(association, 'is invalid') unless validate_all(value, item_type).all?
|
41
41
|
elsif value&.invalid?
|
@@ -47,7 +47,7 @@ module Sequent
|
|
47
47
|
private
|
48
48
|
|
49
49
|
def incorrect_type?(value, record, association)
|
50
|
-
return unless record.class.respond_to?(:types)
|
50
|
+
return false unless record.class.respond_to?(:types)
|
51
51
|
|
52
52
|
type = record.class.types[association]
|
53
53
|
if type.respond_to?(:candidate?)
|