ruby_event_store-active_record 2.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +7 -0
- data/lib/rails_event_store_active_record.rb +3 -0
- data/lib/ruby_event_store/active_record/batch_enumerator.rb +38 -0
- data/lib/ruby_event_store/active_record/event.rb +20 -0
- data/lib/ruby_event_store/active_record/event_repository.rb +164 -0
- data/lib/ruby_event_store/active_record/event_repository_reader.rb +205 -0
- data/lib/ruby_event_store/active_record/generators/migration_generator.rb +49 -0
- data/lib/ruby_event_store/active_record/generators/rails_migration_generator.rb +54 -0
- data/lib/ruby_event_store/active_record/generators/templates/create_event_store_events_template.erb +55 -0
- data/lib/ruby_event_store/active_record/index_violation_detector.rb +42 -0
- data/lib/ruby_event_store/active_record/pg_linearized_event_repository.rb +15 -0
- data/lib/ruby_event_store/active_record/railtie.rb +7 -0
- data/lib/ruby_event_store/active_record/rake_task.rb +18 -0
- data/lib/ruby_event_store/active_record/tasks/migration_tasks.rake +12 -0
- data/lib/ruby_event_store/active_record/version.rb +7 -0
- data/lib/ruby_event_store/active_record/with_abstract_base_class.rb +41 -0
- data/lib/ruby_event_store/active_record/with_default_models.rb +11 -0
- data/lib/ruby_event_store/active_record.rb +14 -0
- metadata +99 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: da1ae3857e3ed938eedebac46e1a2ca4e8fc89be6e424f9614e29300430e8278
|
4
|
+
data.tar.gz: 43f039fc3434fc542918682e788acbeb497a219f845da9d149ed041a06d021c0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f1a893454a79f7fb541a96fb12b5fc7d41d9aaa2ac29a8d6d23a6e128dcd349a75c387135774eee641d0e879d5124c092217f6e0e8ff1bffbf4eaf9485ccb5a6
|
7
|
+
data.tar.gz: 5b3a9ad5d24bff351eb8b0ae651b3d68bfc9ab5dd15fe5da2be79ed8c4aabca165cc2807f1bf1b75a6253c17287c342c820ac6d95e94bcbec23add6d2c77fa7e
|
data/README.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# RubyEventStore::ActiveRecord
|
2
|
+
|
3
|
+
Persistent event repository implementation for RubyEventStore based on ActiveRecord. Ships with database schema and migrations suitable for PostgreSQL, MySQL ans SQLite database engines.
|
4
|
+
|
5
|
+
Includes repository implementation with linearized writes to achieve log-like properties of streams on top of SQL database engine.
|
6
|
+
|
7
|
+
Find out more at [https://railseventstore.org](https://railseventstore.org/)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyEventStore
|
4
|
+
module ActiveRecord
|
5
|
+
class BatchEnumerator
|
6
|
+
def initialize(batch_size, total_limit, reader)
|
7
|
+
@batch_size = batch_size
|
8
|
+
@total_limit = total_limit
|
9
|
+
@reader = reader
|
10
|
+
end
|
11
|
+
|
12
|
+
def each
|
13
|
+
return to_enum unless block_given?
|
14
|
+
offset_id = nil
|
15
|
+
|
16
|
+
0.step(total_limit - 1, batch_size) do |batch_offset|
|
17
|
+
batch_limit = [batch_size, total_limit - batch_offset].min
|
18
|
+
results, offset_id = reader.call(offset_id, batch_limit)
|
19
|
+
|
20
|
+
break if results.empty?
|
21
|
+
yield results
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def first
|
26
|
+
each.first
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_a
|
30
|
+
each.to_a
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :batch_size, :total_limit, :reader
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
module RubyEventStore
|
6
|
+
module ActiveRecord
|
7
|
+
class Event < ::ActiveRecord::Base
|
8
|
+
self.primary_key = :id
|
9
|
+
self.table_name = "event_store_events"
|
10
|
+
end
|
11
|
+
private_constant :Event
|
12
|
+
|
13
|
+
class EventInStream < ::ActiveRecord::Base
|
14
|
+
self.primary_key = :id
|
15
|
+
self.table_name = "event_store_events_in_streams"
|
16
|
+
belongs_to :event, primary_key: :event_id
|
17
|
+
end
|
18
|
+
private_constant :EventInStream
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/array"
|
4
|
+
|
5
|
+
module RubyEventStore
|
6
|
+
module ActiveRecord
|
7
|
+
class EventRepository
|
8
|
+
POSITION_SHIFT = 1
|
9
|
+
|
10
|
+
def initialize(model_factory: WithDefaultModels.new, serializer:)
|
11
|
+
@serializer = serializer
|
12
|
+
|
13
|
+
@event_klass, @stream_klass = model_factory.call
|
14
|
+
@repo_reader = EventRepositoryReader.new(@event_klass, @stream_klass, serializer)
|
15
|
+
@index_violation_detector = IndexViolationDetector.new(@event_klass.table_name, @stream_klass.table_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def append_to_stream(records, stream, expected_version)
|
19
|
+
return if records.empty?
|
20
|
+
|
21
|
+
hashes = []
|
22
|
+
event_ids = []
|
23
|
+
records.each do |record|
|
24
|
+
hashes << insert_hash(record, record.serialize(serializer))
|
25
|
+
event_ids << record.event_id
|
26
|
+
end
|
27
|
+
add_to_stream(event_ids, stream, expected_version) { @event_klass.insert_all!(hashes) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def link_to_stream(event_ids, stream, expected_version)
|
31
|
+
return if event_ids.empty?
|
32
|
+
|
33
|
+
(event_ids - @event_klass.where(event_id: event_ids).pluck(:event_id)).each do |id|
|
34
|
+
raise EventNotFound.new(id)
|
35
|
+
end
|
36
|
+
add_to_stream(event_ids, stream, expected_version)
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete_stream(stream)
|
40
|
+
@stream_klass.where(stream: stream.name).delete_all
|
41
|
+
end
|
42
|
+
|
43
|
+
def has_event?(event_id)
|
44
|
+
@repo_reader.has_event?(event_id)
|
45
|
+
end
|
46
|
+
|
47
|
+
def last_stream_event(stream)
|
48
|
+
@repo_reader.last_stream_event(stream)
|
49
|
+
end
|
50
|
+
|
51
|
+
def read(specification)
|
52
|
+
@repo_reader.read(specification)
|
53
|
+
end
|
54
|
+
|
55
|
+
def count(specification)
|
56
|
+
@repo_reader.count(specification)
|
57
|
+
end
|
58
|
+
|
59
|
+
def update_messages(records)
|
60
|
+
hashes = records.map { |record| upsert_hash(record, record.serialize(serializer)) }
|
61
|
+
for_update = records.map(&:event_id)
|
62
|
+
start_transaction do
|
63
|
+
existing =
|
64
|
+
@event_klass
|
65
|
+
.where(event_id: for_update)
|
66
|
+
.pluck(:event_id, :id, :created_at)
|
67
|
+
.reduce({}) { |acc, (event_id, id, created_at)| acc.merge(event_id => [id, created_at]) }
|
68
|
+
(for_update - existing.keys).each { |id| raise EventNotFound.new(id) }
|
69
|
+
hashes.each do |h|
|
70
|
+
h[:id] = existing.fetch(h.fetch(:event_id)).at(0)
|
71
|
+
h[:created_at] = existing.fetch(h.fetch(:event_id)).at(1)
|
72
|
+
end
|
73
|
+
@event_klass.upsert_all(hashes)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def streams_of(event_id)
|
78
|
+
@repo_reader.streams_of(event_id)
|
79
|
+
end
|
80
|
+
|
81
|
+
def position_in_stream(event_id, stream)
|
82
|
+
@repo_reader.position_in_stream(event_id, stream)
|
83
|
+
end
|
84
|
+
|
85
|
+
def global_position(event_id)
|
86
|
+
@repo_reader.global_position(event_id)
|
87
|
+
end
|
88
|
+
|
89
|
+
def event_in_stream?(event_id, stream)
|
90
|
+
@repo_reader.event_in_stream?(event_id, stream)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
attr_reader :serializer
|
96
|
+
|
97
|
+
def add_to_stream(event_ids, stream, expected_version)
|
98
|
+
last_stream_version = ->(stream_) do
|
99
|
+
@stream_klass.where(stream: stream_.name).order("position DESC").first.try(:position)
|
100
|
+
end
|
101
|
+
resolved_version = expected_version.resolve_for(stream, last_stream_version)
|
102
|
+
|
103
|
+
start_transaction do
|
104
|
+
yield if block_given?
|
105
|
+
in_stream =
|
106
|
+
event_ids.map.with_index do |event_id, index|
|
107
|
+
{
|
108
|
+
stream: stream.name,
|
109
|
+
position: compute_position(resolved_version, index),
|
110
|
+
event_id: event_id,
|
111
|
+
created_at: Time.now.utc
|
112
|
+
}
|
113
|
+
end
|
114
|
+
@stream_klass.insert_all!(in_stream) unless stream.global?
|
115
|
+
end
|
116
|
+
self
|
117
|
+
rescue ::ActiveRecord::RecordNotUnique => e
|
118
|
+
raise_error(e)
|
119
|
+
end
|
120
|
+
|
121
|
+
def raise_error(e)
|
122
|
+
raise EventDuplicatedInStream if detect_index_violated(e.message)
|
123
|
+
raise WrongExpectedEventVersion
|
124
|
+
end
|
125
|
+
|
126
|
+
def compute_position(resolved_version, index)
|
127
|
+
resolved_version + index + POSITION_SHIFT unless resolved_version.nil?
|
128
|
+
end
|
129
|
+
|
130
|
+
def detect_index_violated(message)
|
131
|
+
@index_violation_detector.detect(message)
|
132
|
+
end
|
133
|
+
|
134
|
+
def insert_hash(record, serialized_record)
|
135
|
+
{
|
136
|
+
event_id: serialized_record.event_id,
|
137
|
+
data: serialized_record.data,
|
138
|
+
metadata: serialized_record.metadata,
|
139
|
+
event_type: serialized_record.event_type,
|
140
|
+
created_at: record.timestamp,
|
141
|
+
valid_at: optimize_timestamp(record.valid_at, record.timestamp)
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
def upsert_hash(record, serialized_record)
|
146
|
+
{
|
147
|
+
event_id: serialized_record.event_id,
|
148
|
+
data: serialized_record.data,
|
149
|
+
metadata: serialized_record.metadata,
|
150
|
+
event_type: serialized_record.event_type,
|
151
|
+
valid_at: optimize_timestamp(record.valid_at, record.timestamp)
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
def optimize_timestamp(valid_at, created_at)
|
156
|
+
valid_at unless valid_at.eql?(created_at)
|
157
|
+
end
|
158
|
+
|
159
|
+
def start_transaction(&block)
|
160
|
+
@event_klass.transaction(requires_new: true, &block)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyEventStore
|
4
|
+
module ActiveRecord
|
5
|
+
class EventRepositoryReader
|
6
|
+
def initialize(event_klass, stream_klass, serializer)
|
7
|
+
@event_klass = event_klass
|
8
|
+
@stream_klass = stream_klass
|
9
|
+
@serializer = serializer
|
10
|
+
end
|
11
|
+
|
12
|
+
def has_event?(event_id)
|
13
|
+
@event_klass.exists?(event_id: event_id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def last_stream_event(stream)
|
17
|
+
record_ = @stream_klass.where(stream: stream.name).order("position DESC, id DESC").first
|
18
|
+
record(record_) if record_
|
19
|
+
end
|
20
|
+
|
21
|
+
def read(spec)
|
22
|
+
stream = read_scope(spec)
|
23
|
+
if spec.batched?
|
24
|
+
spec.time_sort_by ? offset_limit_batch_reader(spec, stream) : monotonic_id_batch_reader(spec, stream)
|
25
|
+
elsif spec.first?
|
26
|
+
record_ = stream.first
|
27
|
+
record(record_) if record_
|
28
|
+
elsif spec.last?
|
29
|
+
record_ = stream.last
|
30
|
+
record(record_) if record_
|
31
|
+
else
|
32
|
+
stream.map(&method(:record)).each
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def count(spec)
|
37
|
+
read_scope(spec).count
|
38
|
+
end
|
39
|
+
|
40
|
+
def streams_of(event_id)
|
41
|
+
@stream_klass.where(event_id: event_id).pluck(:stream).map { |name| Stream.new(name) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def position_in_stream(event_id, stream)
|
45
|
+
record = @stream_klass.select("position").where(stream: stream.name).find_by(event_id: event_id)
|
46
|
+
raise EventNotFoundInStream if record.nil?
|
47
|
+
record.position
|
48
|
+
end
|
49
|
+
|
50
|
+
def global_position(event_id)
|
51
|
+
record = @event_klass.select("id").find_by(event_id: event_id)
|
52
|
+
raise EventNotFound.new(event_id) if record.nil?
|
53
|
+
record.id - 1
|
54
|
+
end
|
55
|
+
|
56
|
+
def event_in_stream?(event_id, stream)
|
57
|
+
@stream_klass.where(event_id: event_id, stream: stream.name).exists?
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :serializer
|
63
|
+
|
64
|
+
def offset_limit_batch_reader(spec, stream)
|
65
|
+
batch_reader = ->(offset, limit) { stream.offset(offset).limit(limit).map(&method(:record)) }
|
66
|
+
RubyEventStore::BatchEnumerator.new(spec.batch_size, spec.limit, batch_reader).each
|
67
|
+
end
|
68
|
+
|
69
|
+
def monotonic_id_batch_reader(spec, stream)
|
70
|
+
batch_reader = ->(offset_id, limit) do
|
71
|
+
search_in = spec.stream.global? ? @event_klass.table_name : @stream_klass.table_name
|
72
|
+
records =
|
73
|
+
if offset_id.nil?
|
74
|
+
stream.limit(limit)
|
75
|
+
else
|
76
|
+
stream.where(start_offset_condition(spec, offset_id, search_in)).limit(limit)
|
77
|
+
end
|
78
|
+
[records.map(&method(:record)), records.last]
|
79
|
+
end
|
80
|
+
BatchEnumerator.new(spec.batch_size, spec.limit, batch_reader).each
|
81
|
+
end
|
82
|
+
|
83
|
+
def read_scope(spec)
|
84
|
+
if spec.stream.global?
|
85
|
+
stream = @event_klass
|
86
|
+
stream = stream.where(event_id: spec.with_ids) if spec.with_ids?
|
87
|
+
stream = stream.where(event_type: spec.with_types) if spec.with_types?
|
88
|
+
stream = ordered(stream, spec)
|
89
|
+
stream = stream.limit(spec.limit) if spec.limit?
|
90
|
+
stream = stream.where(start_condition_in_global_stream(spec)) if spec.start
|
91
|
+
stream = stream.where(stop_condition_in_global_stream(spec)) if spec.stop
|
92
|
+
stream = stream.where(older_than_condition(spec)) if spec.older_than
|
93
|
+
stream = stream.where(older_than_or_equal_condition(spec)) if spec.older_than_or_equal
|
94
|
+
stream = stream.where(newer_than_condition(spec)) if spec.newer_than
|
95
|
+
stream = stream.where(newer_than_or_equal_condition(spec)) if spec.newer_than_or_equal
|
96
|
+
stream.order(id: order(spec))
|
97
|
+
else
|
98
|
+
stream = @stream_klass.preload(:event).where(stream: spec.stream.name)
|
99
|
+
stream = stream.where(event_id: spec.with_ids) if spec.with_ids?
|
100
|
+
stream = stream.where(@event_klass.table_name => { event_type: spec.with_types }) if spec.with_types?
|
101
|
+
stream = ordered(stream.joins(:event), spec)
|
102
|
+
stream = stream.order(id: order(spec))
|
103
|
+
stream = stream.limit(spec.limit) if spec.limit?
|
104
|
+
stream = stream.where(start_condition(spec)) if spec.start
|
105
|
+
stream = stream.where(stop_condition(spec)) if spec.stop
|
106
|
+
stream = stream.where(older_than_condition(spec)) if spec.older_than
|
107
|
+
stream = stream.where(older_than_or_equal_condition(spec)) if spec.older_than_or_equal
|
108
|
+
stream = stream.where(newer_than_condition(spec)) if spec.newer_than
|
109
|
+
stream = stream.where(newer_than_or_equal_condition(spec)) if spec.newer_than_or_equal
|
110
|
+
stream
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def ordered(stream, spec)
|
115
|
+
case spec.time_sort_by
|
116
|
+
when :as_at
|
117
|
+
stream.order("#{@event_klass.table_name}.created_at #{order(spec)}")
|
118
|
+
when :as_of
|
119
|
+
stream.order("#{@event_klass.table_name}.valid_at #{order(spec)}")
|
120
|
+
else
|
121
|
+
stream
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def start_offset_condition(specification, record_id, search_in)
|
126
|
+
condition = "#{search_in}.id #{specification.forward? ? ">" : "<"} ?"
|
127
|
+
[condition, record_id]
|
128
|
+
end
|
129
|
+
|
130
|
+
def stop_offset_condition(specification, record_id, search_in)
|
131
|
+
condition = "#{search_in}.id #{specification.forward? ? "<" : ">"} ?"
|
132
|
+
[condition, record_id]
|
133
|
+
end
|
134
|
+
|
135
|
+
def start_condition(specification)
|
136
|
+
start_offset_condition(
|
137
|
+
specification,
|
138
|
+
@stream_klass.find_by!(event_id: specification.start, stream: specification.stream.name),
|
139
|
+
@stream_klass.table_name
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
def stop_condition(specification)
|
144
|
+
stop_offset_condition(
|
145
|
+
specification,
|
146
|
+
@stream_klass.find_by!(event_id: specification.stop, stream: specification.stream.name),
|
147
|
+
@stream_klass.table_name
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
def start_condition_in_global_stream(specification)
|
152
|
+
start_offset_condition(
|
153
|
+
specification,
|
154
|
+
@event_klass.find_by!(event_id: specification.start),
|
155
|
+
@event_klass.table_name
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
def stop_condition_in_global_stream(specification)
|
160
|
+
stop_offset_condition(
|
161
|
+
specification,
|
162
|
+
@event_klass.find_by!(event_id: specification.stop),
|
163
|
+
@event_klass.table_name
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
def older_than_condition(specification)
|
168
|
+
["#{@event_klass.table_name}.created_at < ?", specification.older_than]
|
169
|
+
end
|
170
|
+
|
171
|
+
def older_than_or_equal_condition(specification)
|
172
|
+
["#{@event_klass.table_name}.created_at <= ?", specification.older_than_or_equal]
|
173
|
+
end
|
174
|
+
|
175
|
+
def newer_than_condition(specification)
|
176
|
+
["#{@event_klass.table_name}.created_at > ?", specification.newer_than]
|
177
|
+
end
|
178
|
+
|
179
|
+
def newer_than_or_equal_condition(specification)
|
180
|
+
["#{@event_klass.table_name}.created_at >= ?", specification.newer_than_or_equal]
|
181
|
+
end
|
182
|
+
|
183
|
+
def order(spec)
|
184
|
+
spec.forward? ? "ASC" : "DESC"
|
185
|
+
end
|
186
|
+
|
187
|
+
def record(record)
|
188
|
+
record = record.event if @stream_klass === record
|
189
|
+
|
190
|
+
SerializedRecord
|
191
|
+
.new(
|
192
|
+
event_id: record.event_id,
|
193
|
+
metadata: record.metadata,
|
194
|
+
data: record.data,
|
195
|
+
event_type: record.event_type,
|
196
|
+
timestamp: record.created_at.iso8601(TIMESTAMP_PRECISION),
|
197
|
+
valid_at: (record.valid_at || record.created_at).iso8601(TIMESTAMP_PRECISION)
|
198
|
+
)
|
199
|
+
.deserialize(serializer)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
private_constant(:EventRepositoryReader)
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "erb"
|
3
|
+
|
4
|
+
module RubyEventStore
|
5
|
+
module ActiveRecord
|
6
|
+
class MigrationGenerator
|
7
|
+
DATA_TYPES = %w[binary json jsonb].freeze
|
8
|
+
|
9
|
+
def call(data_type, migration_path)
|
10
|
+
raise ArgumentError, "Invalid value for data type. Supported for options are: #{DATA_TYPES.join(", ")}." unless DATA_TYPES.include?(data_type)
|
11
|
+
|
12
|
+
migration_code = migration_code(data_type)
|
13
|
+
path = build_path(migration_path)
|
14
|
+
write_to_file(migration_code, path)
|
15
|
+
path
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def absolute_path(path)
|
21
|
+
File.expand_path(path, __dir__)
|
22
|
+
end
|
23
|
+
|
24
|
+
def migration_code(data_type)
|
25
|
+
migration_template(absolute_path("./templates"), "create_event_store_events").result_with_hash(migration_version: migration_version, data_type: data_type)
|
26
|
+
end
|
27
|
+
|
28
|
+
def migration_template(template_root, name)
|
29
|
+
ERB.new(File.read(File.join(template_root, "#{name}_template.erb")))
|
30
|
+
end
|
31
|
+
|
32
|
+
def migration_version
|
33
|
+
"[4.2]"
|
34
|
+
end
|
35
|
+
|
36
|
+
def timestamp
|
37
|
+
Time.now.strftime("%Y%m%d%H%M%S")
|
38
|
+
end
|
39
|
+
|
40
|
+
def write_to_file(migration_code, path)
|
41
|
+
File.write(path, migration_code)
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_path(migration_path)
|
45
|
+
File.join( "#{migration_path}", "#{timestamp}_create_event_store_events.rb")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "rails/generators"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module RubyEventStore
|
9
|
+
module ActiveRecord
|
10
|
+
class RailsMigrationGenerator < Rails::Generators::Base
|
11
|
+
class Error < Thor::Error
|
12
|
+
end
|
13
|
+
|
14
|
+
DATA_TYPES = %w[binary json jsonb].freeze
|
15
|
+
|
16
|
+
namespace "rails_event_store_active_record:migration"
|
17
|
+
|
18
|
+
source_root File.expand_path(File.join(File.dirname(__FILE__), "../generators/templates"))
|
19
|
+
class_option(
|
20
|
+
:data_type,
|
21
|
+
type: :string,
|
22
|
+
default: "binary",
|
23
|
+
desc:
|
24
|
+
"Configure the data type for `data` and `meta data` fields in Postgres migration (options: #{DATA_TYPES.join("/")})"
|
25
|
+
)
|
26
|
+
|
27
|
+
def initialize(*args)
|
28
|
+
super
|
29
|
+
|
30
|
+
if DATA_TYPES.exclude?(options.fetch(:data_type))
|
31
|
+
raise Error, "Invalid value for --data-type option. Supported for options are: #{DATA_TYPES.join(", ")}."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_migration
|
36
|
+
template "create_event_store_events_template.erb", "db/migrate/#{timestamp}_create_event_store_events.rb"
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def data_type
|
42
|
+
options.fetch("data_type")
|
43
|
+
end
|
44
|
+
|
45
|
+
def migration_version
|
46
|
+
"[4.2]"
|
47
|
+
end
|
48
|
+
|
49
|
+
def timestamp
|
50
|
+
Time.now.strftime("%Y%m%d%H%M%S")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end if defined?(Rails::Generators::Base)
|
data/lib/ruby_event_store/active_record/generators/templates/create_event_store_events_template.erb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateEventStoreEvents < ActiveRecord::Migration<%= migration_version %>
|
4
|
+
def change
|
5
|
+
postgres =
|
6
|
+
ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
7
|
+
if postgres
|
8
|
+
create_table(:event_store_events_in_streams, id: :bigserial, force: false) do |t|
|
9
|
+
t.string :stream, null: false
|
10
|
+
t.integer :position, null: true
|
11
|
+
t.references :event, null: false, type: :uuid
|
12
|
+
t.datetime :created_at, null: false
|
13
|
+
end
|
14
|
+
add_index :event_store_events_in_streams, [:stream, :position], unique: true
|
15
|
+
add_index :event_store_events_in_streams, [:created_at]
|
16
|
+
add_index :event_store_events_in_streams, [:stream, :event_id], unique: true
|
17
|
+
|
18
|
+
create_table(:event_store_events, id: :bigserial, force: false) do |t|
|
19
|
+
t.references :event, null: false, type: :uuid
|
20
|
+
t.string :event_type, null: false
|
21
|
+
t.<%= data_type %> :metadata
|
22
|
+
t.<%= data_type %> :data, null: false
|
23
|
+
t.datetime :created_at, null: false
|
24
|
+
t.datetime :valid_at, null: true
|
25
|
+
end
|
26
|
+
add_index :event_store_events, :event_id, unique: true
|
27
|
+
add_index :event_store_events, :created_at
|
28
|
+
add_index :event_store_events, :valid_at
|
29
|
+
add_index :event_store_events, :event_type
|
30
|
+
else
|
31
|
+
create_table(:event_store_events_in_streams, force: false) do |t|
|
32
|
+
t.string :stream, null: false
|
33
|
+
t.integer :position, null: true
|
34
|
+
t.references :event, null: false, type: :string, limit: 36
|
35
|
+
t.datetime :created_at, null: false, precision: 6
|
36
|
+
end
|
37
|
+
add_index :event_store_events_in_streams, [:stream, :position], unique: true
|
38
|
+
add_index :event_store_events_in_streams, [:created_at]
|
39
|
+
add_index :event_store_events_in_streams, [:stream, :event_id], unique: true
|
40
|
+
|
41
|
+
create_table(:event_store_events, force: false) do |t|
|
42
|
+
t.references :event, null: false, type: :string, limit: 36
|
43
|
+
t.string :event_type, null: false
|
44
|
+
t.binary :metadata
|
45
|
+
t.binary :data, null: false
|
46
|
+
t.datetime :created_at, null: false, precision: 6
|
47
|
+
t.datetime :valid_at, null: true, precision: 6
|
48
|
+
end
|
49
|
+
add_index :event_store_events, :event_id, unique: true
|
50
|
+
add_index :event_store_events, :created_at
|
51
|
+
add_index :event_store_events, :valid_at
|
52
|
+
add_index :event_store_events, :event_type
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyEventStore
|
4
|
+
module ActiveRecord
|
5
|
+
class IndexViolationDetector
|
6
|
+
def initialize(event_store_events, event_store_events_in_streams)
|
7
|
+
@postgres_pkey_error = "Key (event_id)".freeze
|
8
|
+
@postgres_index_error = "Key (stream, event_id)".freeze
|
9
|
+
@mysql5_pkey_error = "for key 'index_#{event_store_events}_on_event_id'".freeze
|
10
|
+
@mysql8_pkey_error = "for key '#{event_store_events}.index_#{event_store_events}_on_event_id'".freeze
|
11
|
+
@mysql5_index_error = "for key 'index_#{event_store_events_in_streams}_on_stream_and_event_id'".freeze
|
12
|
+
@mysql8_index_error =
|
13
|
+
"for key '#{event_store_events_in_streams}.index_#{event_store_events_in_streams}_on_stream_and_event_id'"
|
14
|
+
.freeze
|
15
|
+
@sqlite3_pkey_error = "constraint failed: #{event_store_events}.event_id".freeze
|
16
|
+
@sqlite3_index_error =
|
17
|
+
"constraint failed: #{event_store_events_in_streams}.stream, #{event_store_events_in_streams}.event_id".freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def detect(message)
|
21
|
+
detect_postgres(message) || detect_mysql(message) || detect_sqlite(message)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def detect_postgres(message)
|
27
|
+
message.include?(@postgres_pkey_error) || message.include?(@postgres_index_error)
|
28
|
+
end
|
29
|
+
|
30
|
+
def detect_mysql(message)
|
31
|
+
message.include?(@mysql5_pkey_error) || message.include?(@mysql8_pkey_error) ||
|
32
|
+
message.include?(@mysql5_index_error) || message.include?(@mysql8_index_error)
|
33
|
+
end
|
34
|
+
|
35
|
+
def detect_sqlite(message)
|
36
|
+
message.include?(@sqlite3_pkey_error) || message.include?(@sqlite3_index_error)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private_constant(:IndexViolationDetector)
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyEventStore
|
4
|
+
module ActiveRecord
|
5
|
+
class PgLinearizedEventRepository < EventRepository
|
6
|
+
def start_transaction(&proc)
|
7
|
+
::ActiveRecord::Base.transaction(requires_new: true) do
|
8
|
+
::ActiveRecord::Base.connection.execute("SELECT pg_advisory_xact_lock(1845240511599988039) as l").each {}
|
9
|
+
|
10
|
+
proc.call
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ruby_event_store/active_record"
|
4
|
+
require "active_record"
|
5
|
+
load "ruby_event_store/active_record/tasks/migration_tasks.rake"
|
6
|
+
|
7
|
+
include ActiveRecord::Tasks
|
8
|
+
|
9
|
+
db_dir = ENV["DATABASE_DIR"] || './db'
|
10
|
+
|
11
|
+
task :environment do
|
12
|
+
connection = ActiveRecord::Base.establish_connection(ENV["DATABASE_URL"])
|
13
|
+
DatabaseTasks.env = connection.db_config.env_name
|
14
|
+
DatabaseTasks.db_dir = db_dir
|
15
|
+
DatabaseTasks.migrations_paths = ENV["MIGRATIONS_PATH"] || File.join(db_dir, 'migrate')
|
16
|
+
end
|
17
|
+
|
18
|
+
load 'active_record/railties/databases.rake'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative "../generators/migration_generator"
|
2
|
+
|
3
|
+
desc "Generate migration"
|
4
|
+
task "db:migrations:copy" do
|
5
|
+
data_type = ENV["DATA_TYPE"] || raise("Specify data type (binary, json, jsonb): rake db:migrations:copy DATA_TYPE=json")
|
6
|
+
|
7
|
+
path = RubyEventStore::ActiveRecord::MigrationGenerator
|
8
|
+
.new
|
9
|
+
.call(data_type, ENV["MIGRATION_PATH"] || "db/migrate")
|
10
|
+
|
11
|
+
puts "Migration file created #{path}"
|
12
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyEventStore
|
4
|
+
module ActiveRecord
|
5
|
+
class WithAbstractBaseClass
|
6
|
+
def initialize(base_klass)
|
7
|
+
unless base_klass < ::ActiveRecord::Base && base_klass.abstract_class?
|
8
|
+
raise ArgumentError.new("#{base_klass} must be an abstract class that inherits from ActiveRecord::Base")
|
9
|
+
end
|
10
|
+
@base_klass = base_klass
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(instance_id: SecureRandom.hex)
|
14
|
+
[build_event_klass(instance_id), build_stream_klass(instance_id)]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def build_event_klass(instance_id)
|
20
|
+
Object.const_set(
|
21
|
+
"Event_#{instance_id}",
|
22
|
+
Class.new(@base_klass) do
|
23
|
+
self.primary_key = :id
|
24
|
+
self.table_name = "event_store_events"
|
25
|
+
end
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_stream_klass(instance_id)
|
30
|
+
Object.const_set(
|
31
|
+
"EventInStream_#{instance_id}",
|
32
|
+
Class.new(@base_klass) do
|
33
|
+
self.primary_key = :id
|
34
|
+
self.table_name = "event_store_events_in_streams"
|
35
|
+
belongs_to :event, primary_key: :event_id, class_name: "Event_#{instance_id}"
|
36
|
+
end
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "active_record/generators/migration_generator"
|
4
|
+
require_relative "active_record/generators/rails_migration_generator"
|
5
|
+
require_relative "active_record/event"
|
6
|
+
require_relative "active_record/with_default_models"
|
7
|
+
require_relative "active_record/with_abstract_base_class"
|
8
|
+
require_relative "active_record/event_repository"
|
9
|
+
require_relative "active_record/batch_enumerator"
|
10
|
+
require_relative "active_record/event_repository_reader"
|
11
|
+
require_relative "active_record/index_violation_detector"
|
12
|
+
require_relative "active_record/pg_linearized_event_repository"
|
13
|
+
require_relative "active_record/version"
|
14
|
+
require_relative "active_record/railtie" if defined?(Rails::Engine)
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby_event_store-active_record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.7.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Arkency
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-12-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ruby_event_store
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.7.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.7.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '6.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '6.0'
|
41
|
+
description: |
|
42
|
+
Persistent event repository implementation for RubyEventStore based on ActiveRecord. Ships with database schema
|
43
|
+
and migrations suitable for PostgreSQL, MySQL ans SQLite database engines.
|
44
|
+
|
45
|
+
Includes repository implementation with linearized writes to achieve log-like properties of streams
|
46
|
+
on top of SQL database engine.
|
47
|
+
email: dev@arkency.com
|
48
|
+
executables: []
|
49
|
+
extensions: []
|
50
|
+
extra_rdoc_files:
|
51
|
+
- README.md
|
52
|
+
files:
|
53
|
+
- README.md
|
54
|
+
- lib/rails_event_store_active_record.rb
|
55
|
+
- lib/ruby_event_store/active_record.rb
|
56
|
+
- lib/ruby_event_store/active_record/batch_enumerator.rb
|
57
|
+
- lib/ruby_event_store/active_record/event.rb
|
58
|
+
- lib/ruby_event_store/active_record/event_repository.rb
|
59
|
+
- lib/ruby_event_store/active_record/event_repository_reader.rb
|
60
|
+
- lib/ruby_event_store/active_record/generators/migration_generator.rb
|
61
|
+
- lib/ruby_event_store/active_record/generators/rails_migration_generator.rb
|
62
|
+
- lib/ruby_event_store/active_record/generators/templates/create_event_store_events_template.erb
|
63
|
+
- lib/ruby_event_store/active_record/index_violation_detector.rb
|
64
|
+
- lib/ruby_event_store/active_record/pg_linearized_event_repository.rb
|
65
|
+
- lib/ruby_event_store/active_record/railtie.rb
|
66
|
+
- lib/ruby_event_store/active_record/rake_task.rb
|
67
|
+
- lib/ruby_event_store/active_record/tasks/migration_tasks.rake
|
68
|
+
- lib/ruby_event_store/active_record/version.rb
|
69
|
+
- lib/ruby_event_store/active_record/with_abstract_base_class.rb
|
70
|
+
- lib/ruby_event_store/active_record/with_default_models.rb
|
71
|
+
homepage: https://railseventstore.org
|
72
|
+
licenses:
|
73
|
+
- MIT
|
74
|
+
metadata:
|
75
|
+
homepage_uri: https://railseventstore.org
|
76
|
+
changelog_uri: https://github.com/RailsEventStore/rails_event_store/releases
|
77
|
+
source_code_uri: https://github.com/RailsEventStore/rails_event_store
|
78
|
+
bug_tracker_uri: https://github.com/RailsEventStore/rails_event_store/issues
|
79
|
+
rubygems_mfa_required: 'true'
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '2.7'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubygems_version: 3.3.26
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: Persistent event repository implementation for RubyEventStore based on ActiveRecord
|
99
|
+
test_files: []
|