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 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: []