ruby_event_store-active_record 2.7.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 +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: []
|