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 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,3 @@
1
+ require "ruby_event_store/active_record"
2
+
3
+ RailsEventStoreActiveRecord = RubyEventStore::ActiveRecord
@@ -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)
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module ActiveRecord
5
+ Railtie = Class.new(::Rails::Railtie)
6
+ end
7
+ 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module ActiveRecord
5
+ VERSION = "2.7.0"
6
+ end
7
+ 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module ActiveRecord
5
+ class WithDefaultModels
6
+ def call
7
+ [Event, EventInStream]
8
+ end
9
+ end
10
+ end
11
+ 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: []