ruby_event_store-rom 1.3.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -1
  3. data/lib/ruby_event_store/rom/changesets/create_events.rb +10 -18
  4. data/lib/ruby_event_store/rom/changesets/create_stream_entries.rb +6 -11
  5. data/lib/ruby_event_store/rom/changesets/update_events.rb +32 -19
  6. data/lib/ruby_event_store/rom/event_repository.rb +53 -51
  7. data/lib/ruby_event_store/rom/index_violation_detector.rb +29 -0
  8. data/lib/ruby_event_store/rom/mappers/event_to_serialized_record.rb +4 -4
  9. data/lib/ruby_event_store/rom/mappers/stream_entry_to_serialized_record.rb +5 -4
  10. data/lib/ruby_event_store/rom/rake_task.rb +5 -0
  11. data/lib/ruby_event_store/rom/relations/events.rb +81 -0
  12. data/lib/ruby_event_store/rom/relations/stream_entries.rb +90 -0
  13. data/lib/ruby_event_store/rom/repositories/events.rb +43 -29
  14. data/lib/ruby_event_store/rom/repositories/stream_entries.rb +7 -13
  15. data/lib/ruby_event_store/rom/{adapters/sql/tasks → tasks}/migration_tasks.rake +9 -9
  16. data/lib/ruby_event_store/rom/types.rb +2 -2
  17. data/lib/ruby_event_store/rom/unit_of_work.rb +29 -13
  18. data/lib/ruby_event_store/rom/version.rb +1 -1
  19. data/lib/ruby_event_store/rom.rb +29 -102
  20. data/lib/ruby_event_store-rom.rb +1 -1
  21. metadata +29 -48
  22. data/.rubocop.yml +0 -1
  23. data/.rubocop_todo.yml +0 -84
  24. data/CHANGELOG.md +0 -9
  25. data/Gemfile +0 -12
  26. data/Makefile +0 -57
  27. data/Rakefile +0 -20
  28. data/db/migrate/20180327044629_create_ruby_event_store_tables.rb +0 -54
  29. data/db/migrate/20181026152045_index_by_event_type.rb +0 -9
  30. data/lib/ruby_event_store/rom/adapters/memory/changesets/create_events.rb +0 -19
  31. data/lib/ruby_event_store/rom/adapters/memory/changesets/create_stream_entries.rb +0 -19
  32. data/lib/ruby_event_store/rom/adapters/memory/changesets/update_events.rb +0 -18
  33. data/lib/ruby_event_store/rom/adapters/memory/relations/events.rb +0 -56
  34. data/lib/ruby_event_store/rom/adapters/memory/relations/stream_entries.rb +0 -114
  35. data/lib/ruby_event_store/rom/adapters/memory/unit_of_work.rb +0 -36
  36. data/lib/ruby_event_store/rom/adapters/sql/changesets/create_events.rb +0 -15
  37. data/lib/ruby_event_store/rom/adapters/sql/changesets/update_events.rb +0 -41
  38. data/lib/ruby_event_store/rom/adapters/sql/index_violation_detector.rb +0 -31
  39. data/lib/ruby_event_store/rom/adapters/sql/rake_task.rb +0 -5
  40. data/lib/ruby_event_store/rom/adapters/sql/relations/events.rb +0 -27
  41. data/lib/ruby_event_store/rom/adapters/sql/relations/stream_entries.rb +0 -72
  42. data/lib/ruby_event_store/rom/memory.rb +0 -82
  43. data/lib/ruby_event_store/rom/sql.rb +0 -169
  44. data/lib/ruby_event_store/rom/tuple_uniqueness_error.rb +0 -21
  45. data/lib/ruby_event_store/spec/rom/event_repository_lint.rb +0 -176
  46. data/lib/ruby_event_store/spec/rom/relations/events_lint.rb +0 -75
  47. data/lib/ruby_event_store/spec/rom/relations/stream_entries_lint.rb +0 -198
  48. data/lib/ruby_event_store/spec/rom/spec_helper_lint.rb +0 -15
  49. data/lib/ruby_event_store/spec/rom/unit_of_work_lint.rb +0 -37
  50. data/ruby_event_store-rom.gemspec +0 -37
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'ruby_event_store/rom'
4
- require 'rom/memory'
5
- require_relative 'adapters/memory/unit_of_work'
6
- require_relative 'adapters/memory/relations/events'
7
- require_relative 'adapters/memory/relations/stream_entries'
8
- require_relative 'adapters/memory/changesets/create_events'
9
- require_relative 'adapters/memory/changesets/update_events'
10
- require_relative 'adapters/memory/changesets/create_stream_entries'
11
-
12
- module RubyEventStore
13
- module ROM
14
- module Memory
15
- class << self
16
- def fetch_next_id
17
- @last_id ||= 0
18
- @mutex ||= Mutex.new
19
- @mutex.synchronize { @last_id += 1 }
20
- end
21
-
22
- def setup(config)
23
- config.register_relation Relations::Events
24
- config.register_relation Relations::StreamEntries
25
- end
26
-
27
- def configure(env)
28
- env.register_unit_of_work_options(class: UnitOfWork)
29
-
30
- env.register_error_handler :unique_violation, lambda { |ex|
31
- case ex
32
- when TupleUniquenessError
33
- raise EventDuplicatedInStream if ex.message =~ /event_id/
34
-
35
- raise WrongExpectedEventVersion
36
- end
37
- }
38
- end
39
- end
40
-
41
- class SpecHelper
42
- attr_reader :env
43
- attr_reader :connection_pool_size, :close_pool_connection
44
-
45
- def initialize
46
- @connection_pool_size = 5
47
- @env = ROM.setup(:memory)
48
- end
49
-
50
- def run_lifecycle
51
- yield
52
- ensure
53
- drop_gateway_schema
54
- end
55
-
56
- def gateway
57
- env.rom_container.gateways.fetch(:default)
58
- end
59
-
60
- def drop_gateway_schema
61
- gateway.connection.data.values.each { |v| v.data.clear }
62
- end
63
-
64
- def close_gateway_connection
65
- gateway.disconnect
66
- end
67
-
68
- def gateway_type?(name)
69
- name == :memory
70
- end
71
-
72
- def has_connection_pooling?
73
- true
74
- end
75
-
76
- def supports_upsert?
77
- true
78
- end
79
- end
80
- end
81
- end
82
- end
@@ -1,169 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'ruby_event_store/rom'
4
- require 'rom/sql'
5
- require_relative 'adapters/sql/index_violation_detector'
6
- require_relative 'adapters/sql/relations/events'
7
- require_relative 'adapters/sql/relations/stream_entries'
8
- require_relative 'adapters/sql/changesets/create_events'
9
- require_relative 'adapters/sql/changesets/update_events'
10
-
11
- module RubyEventStore
12
- module ROM
13
- module SQL
14
- class << self
15
- def setup(config)
16
- config.register_relation Relations::Events
17
- config.register_relation Relations::StreamEntries
18
- end
19
-
20
- def configure(env)
21
- # See: https://github.com/jeremyevans/sequel/blob/master/doc/transactions.rdoc
22
- env.register_unit_of_work_options(
23
- savepoint: true,
24
- # Committing changesets concurrently causes MySQL deadlocks
25
- # which are not caught and retried by Sequel's built-in
26
- # :retry_on option. This appears to be a result of how ROM
27
- # handles exceptions which don't bubble up so that Sequel
28
- # can retry transactions with the :retry_on option when there's
29
- # a deadlock.
30
- #
31
- # This is exacerbated by the fact that changesets insert multiple
32
- # tuples with individual INSERT statements because ROM specifies
33
- # to Sequel to return a list of primary keys created. The likelihood
34
- # of a deadlock is reduced with batched INSERT statements.
35
- #
36
- # For this reason we need to manually insert changeset records to avoid
37
- # MySQL deadlocks or to allow Sequel to retry transactions
38
- # when the :retry_on option is specified.
39
- retry_on: Sequel::SerializationFailure,
40
- before_retry: lambda { |_num, ex|
41
- env.logger.warn("RETRY TRANSACTION [#{self.class.name} => #{ex.class.name}] #{ex.message}")
42
- }
43
- )
44
-
45
- env.register_error_handler :unique_violation, lambda { |ex|
46
- case ex
47
- when ::ROM::SQL::UniqueConstraintError, Sequel::UniqueConstraintViolation
48
- raise EventDuplicatedInStream if IndexViolationDetector.new.detect(ex.message)
49
-
50
- raise WrongExpectedEventVersion
51
- end
52
- }
53
-
54
- env.register_error_handler :not_found, lambda { |ex, event_id|
55
- case ex
56
- when ::ROM::TupleCountMismatchError
57
- raise EventNotFound, event_id
58
- when Sequel::DatabaseError
59
- raise ex unless ex.message =~ /PG::InvalidTextRepresentation.*uuid/
60
-
61
- raise EventNotFound, event_id
62
- end
63
- }
64
- end
65
-
66
- def supports_upsert?(db)
67
- supports_on_duplicate_key_update?(db) ||
68
- supports_insert_conflict_update?(db)
69
- end
70
-
71
- def supports_on_duplicate_key_update?(db)
72
- db.adapter_scheme =~ /mysql/
73
- end
74
-
75
- def supports_insert_conflict_update?(db)
76
- case db.adapter_scheme
77
- when :postgres
78
- true
79
- when :sqlite
80
- # Sqlite 3.24.0+ supports PostgreSQL upsert syntax
81
- db.sqlite_version >= 32_400
82
- else
83
- false
84
- end
85
- end
86
- end
87
-
88
- class SpecHelper
89
- attr_reader :env
90
-
91
- def initialize(database_uri = ENV['DATABASE_URL'])
92
- config = ::ROM::Configuration.new(
93
- :sql,
94
- database_uri,
95
- max_connections: database_uri =~ /sqlite/ ? 1 : 5,
96
- preconnect: :concurrently
97
- # sql_mode: %w[NO_AUTO_VALUE_ON_ZERO STRICT_ALL_TABLES]
98
- )
99
- # $stdout.sync = true
100
- # config.default.use_logger Logger.new(STDOUT)
101
- # config.default.connection.pool.send(:preconnect, true)
102
- config.default.run_migrations
103
-
104
- @env = ROM.setup(config)
105
- end
106
-
107
- def run_lifecycle
108
- establish_gateway_connection
109
- load_gateway_schema
110
-
111
- yield
112
- ensure
113
- drop_gateway_schema
114
- close_gateway_connection
115
- end
116
-
117
- def gateway
118
- env.rom_container.gateways.fetch(:default)
119
- end
120
-
121
- def gateway_type?(name)
122
- gateway.connection.database_type.eql?(name)
123
- end
124
-
125
- def has_connection_pooling?
126
- !gateway_type?(:sqlite)
127
- end
128
-
129
- def connection_pool_size
130
- gateway.connection.pool.size
131
- end
132
-
133
- def close_pool_connection
134
- gateway.connection.pool.disconnect
135
- end
136
-
137
- def supports_upsert?
138
- SQL.supports_upsert?(gateway.connection)
139
- end
140
-
141
- protected
142
-
143
- def establish_gateway_connection
144
- # Manually preconnect because disconnecting and reconnecting
145
- # seems to lose the "preconnect concurrently" setting
146
- gateway.connection.pool.send(:preconnect, true)
147
- end
148
-
149
- def load_gateway_schema
150
- gateway.run_migrations
151
- end
152
-
153
- def drop_gateway_schema
154
- gateway.connection.drop_table?('event_store_events')
155
- gateway.connection.drop_table?('event_store_events_in_streams')
156
- gateway.connection.drop_table?('schema_migrations')
157
- end
158
-
159
- # See: https://github.com/rom-rb/rom-sql/blob/master/spec/shared/database_setup.rb
160
- def close_gateway_connection
161
- gateway.connection.disconnect
162
- # Prevent the auto-reconnect when the test completed
163
- # This will save from hardly reproducible connection run outs
164
- gateway.connection.pool.available_connections.freeze
165
- end
166
- end
167
- end
168
- end
169
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyEventStore
4
- module ROM
5
- class TupleUniquenessError < StandardError
6
- class << self
7
- def for_event_id(event_id)
8
- new "Uniquness violated for event_id (#{event_id.inspect})"
9
- end
10
-
11
- def for_stream_and_event_id(stream_name, event_id)
12
- new "Uniquness violated for stream (#{stream_name.inspect}) and event_id (#{event_id.inspect})"
13
- end
14
-
15
- def for_stream_and_position(stream_name, position)
16
- new "Uniquness violated for stream (#{stream_name.inspect}) and position (#{position.inspect})"
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,176 +0,0 @@
1
- require 'ruby_event_store/rom/event_repository'
2
- require 'ruby_event_store/spec/event_repository_lint'
3
-
4
- module RubyEventStore::ROM
5
- RSpec.shared_examples :rom_event_repository do |repository_class|
6
- subject(:repository) { repository_class.new(rom: env) }
7
-
8
- let(:env) { rom_helper.env }
9
- let(:rom_container) { env.rom_container }
10
- let(:rom_db) { rom_container.gateways[:default] }
11
-
12
- around(:each) do |example|
13
- rom_helper.run_lifecycle { example.run }
14
- end
15
-
16
- let(:test_race_conditions_auto) { rom_helper.has_connection_pooling? }
17
- let(:test_race_conditions_any) { rom_helper.has_connection_pooling? }
18
- let(:test_binary) { false }
19
- let(:test_change) { rom_helper.supports_upsert? }
20
-
21
- let(:default_stream) { RubyEventStore::Stream.new('stream') }
22
- let(:global_stream) { RubyEventStore::Stream.new('all') }
23
- let(:mapper) { RubyEventStore::Mappers::NullMapper.new }
24
-
25
- let(:reader) { RubyEventStore::SpecificationReader.new(repository, mapper) }
26
- let(:specification) { RubyEventStore::Specification.new(reader) }
27
-
28
- require 'ruby_event_store/rom/sql'
29
- it_behaves_like :event_repository, repository_class, [ROM::SQL::Error]
30
-
31
- specify '#initialize requires ROM::Env' do
32
- expect { repository_class.new(rom: nil) }.to raise_error do |err|
33
- expect(err).to be_a(ArgumentError)
34
- expect(err.message).to eq('Must specify rom')
35
- end
36
- end
37
-
38
- specify '#initialize uses ROM.env by default' do
39
- expect { repository_class.new }.to raise_error(ArgumentError)
40
- RubyEventStore::ROM.env = env
41
- expect { repository_class.new }.not_to raise_error
42
- RubyEventStore::ROM.env = nil
43
- end
44
-
45
- specify '#has_event? to raise exception for bad ID' do
46
- expect(repository.has_event?('0')).to eq(false)
47
- end
48
-
49
- specify 'all considered internal detail' do
50
- repository.append_to_stream(
51
- [RubyEventStore::SRecord.new],
52
- RubyEventStore::Stream.new(RubyEventStore::GLOBAL_STREAM),
53
- RubyEventStore::ExpectedVersion.any
54
- )
55
-
56
- expect { repository.read(specification.stream('all').result) }.to raise_error(RubyEventStore::ReservedInternalName)
57
- expect { repository.read(specification.stream('all').backward.result) }.to raise_error(RubyEventStore::ReservedInternalName)
58
- expect { repository.read(specification.stream('all').limit(5).result) }.to raise_error(RubyEventStore::ReservedInternalName)
59
- expect { repository.read(specification.stream('all').limit(5).backward.result) }.to raise_error(RubyEventStore::ReservedInternalName)
60
-
61
- expect { repository.count(specification.stream('all').result) }.to raise_error(RubyEventStore::ReservedInternalName)
62
- end
63
-
64
- specify 'explicit sorting by position rather than accidental' do
65
- events = [
66
- RubyEventStore::SRecord.new(event_id: u1 = SecureRandom.uuid),
67
- RubyEventStore::SRecord.new(event_id: u2 = SecureRandom.uuid),
68
- RubyEventStore::SRecord.new(event_id: u3 = SecureRandom.uuid)
69
- ]
70
-
71
- repo = Repositories::Events.new(rom_container)
72
- repo.create_changeset(events).commit
73
-
74
- expect(repo.events.to_a.size).to eq(3)
75
-
76
- repo.stream_entries.changeset(Changesets::CreateStreamEntries, [
77
- { stream: default_stream.name, event_id: events[1].event_id, position: 1 },
78
- { stream: default_stream.name, event_id: events[0].event_id, position: 0 },
79
- { stream: default_stream.name, event_id: events[2].event_id, position: 2 }
80
- ]).commit
81
-
82
- expect(repo.stream_entries.to_a.size).to eq(3)
83
-
84
- # ActiveRecord::Schema.define do
85
- # self.verbose = false
86
- # remove_index :event_store_events_in_streams, [:stream, :position]
87
- # end
88
-
89
- expect(repository.read(specification.stream('stream').limit(3).result).map(&:event_id)).to eq([u1, u2, u3])
90
- expect(repository.read(specification.stream('stream').result).map(&:event_id)).to eq([u1, u2, u3])
91
-
92
- expect(repository.read(specification.stream('stream').backward.limit(3).result).map(&:event_id)).to eq([u3, u2, u1])
93
- expect(repository.read(specification.stream('stream').backward.result).map(&:event_id)).to eq([u3, u2, u1])
94
- end
95
-
96
- specify 'explicit sorting by id rather than accidental for all events' do
97
- events = [
98
- RubyEventStore::SRecord.new(event_id: u1 = SecureRandom.uuid),
99
- RubyEventStore::SRecord.new(event_id: u2 = SecureRandom.uuid),
100
- RubyEventStore::SRecord.new(event_id: u3 = SecureRandom.uuid)
101
- ]
102
-
103
- repo = Repositories::Events.new(rom_container)
104
- repo.create_changeset(events).commit
105
-
106
- expect(repo.events.to_a.size).to eq(3)
107
-
108
- repo.stream_entries.changeset(Changesets::CreateStreamEntries, [
109
- { stream: global_stream.name, event_id: events[0].event_id, position: 1 },
110
- { stream: global_stream.name, event_id: events[1].event_id, position: 0 },
111
- { stream: global_stream.name, event_id: events[2].event_id, position: 2 }
112
- ]).commit
113
-
114
- expect(repo.stream_entries.to_a.size).to eq(3)
115
-
116
- expect(repository.read(specification.limit(3).result).map(&:event_id)).to eq([u1, u2, u3])
117
- expect(repository.read(specification.limit(3).backward.result).map(&:event_id)).to eq([u3, u2, u1])
118
- end
119
-
120
- specify 'nested transaction - events still not persisted if append failed' do
121
- repository.append_to_stream([
122
- event = RubyEventStore::SRecord.new(event_id: SecureRandom.uuid)
123
- ], default_stream, RubyEventStore::ExpectedVersion.none)
124
-
125
- env.unit_of_work do
126
- expect do
127
- repository.append_to_stream([
128
- RubyEventStore::SRecord.new(event_id: '9bedf448-e4d0-41a3-a8cd-f94aec7aa763')
129
- ], default_stream, RubyEventStore::ExpectedVersion.none)
130
- end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
131
- expect(repository.has_event?('9bedf448-e4d0-41a3-a8cd-f94aec7aa763')).to be_falsey
132
- expect(repository.read(specification.limit(2).result).to_a).to eq([event])
133
- end
134
- expect(repository.has_event?('9bedf448-e4d0-41a3-a8cd-f94aec7aa763')).to be_falsey
135
- expect(repository.read(specification.limit(2).result).to_a).to eq([event])
136
- end
137
-
138
- def cleanup_concurrency_test
139
- rom_helper.close_pool_connection
140
- end
141
-
142
- def verify_conncurency_assumptions
143
- expect(rom_helper.connection_pool_size).to eq(5)
144
- end
145
-
146
- # TODO: Port from AR to ROM
147
- def additional_limited_concurrency_for_auto_check
148
- positions = rom_container.relations[:stream_entries]
149
- .ordered(:forward, default_stream)
150
- .map { |entity| entity[:position] }
151
- expect(positions).to eq((0..positions.size - 1).to_a)
152
- end
153
-
154
- private
155
-
156
- # TODO: Port from AR to ROM
157
- def count_queries
158
- count = 0
159
- # counter_f = lambda { |_name, _started, _finished, _unique_id, payload|
160
- # count += 1 unless %w[CACHE SCHEMA].include?(payload[:name])
161
- # }
162
- # ActiveSupport::Notifications.subscribed(counter_f, "sql.active_record", &block)
163
- count
164
- end
165
-
166
- # TODO: Port from AR to ROM
167
- def expect_query(_match)
168
- count = 0
169
- # counter_f = lambda { |_name, _started, _finished, _unique_id, payload|
170
- # count += 1 if match === payload[:sql]
171
- # }
172
- # ActiveSupport::Notifications.subscribed(counter_f, "sql.active_record", &block)
173
- expect(count).to eq(1)
174
- end
175
- end
176
- end
@@ -1,75 +0,0 @@
1
- RSpec.shared_examples :events_relation do |_relation_class|
2
- subject(:relation) { rom_container.relations[:events] }
3
-
4
- let(:env) { rom_helper.env }
5
- let(:rom_container) { env.rom_container }
6
- let(:rom_db) { rom_container.gateways[:default] }
7
-
8
- around(:each) do |example|
9
- rom_helper.run_lifecycle { example.run }
10
- end
11
-
12
- it 'just created is empty' do
13
- expect(relation.to_a).to be_empty
14
- end
15
-
16
- specify '#exist? indicates if one instance of the record exists by primary key' do
17
- event = { id: SecureRandom.uuid, event_type: 'TestEvent', data: '{}', metadata: '{}', created_at: Time.now }
18
-
19
- expect(relation.by_pk(event[:id]).exist?).to eq(false)
20
-
21
- relation.insert(event)
22
-
23
- expect(relation.by_pk(event[:id]).exist?).to eq(true)
24
- end
25
-
26
- specify '#insert verifies tuple is unique' do
27
- events = [
28
- { id: SecureRandom.uuid, event_type: 'TestEvent', data: '{}', metadata: '{}', created_at: Time.now },
29
- { id: SecureRandom.uuid, event_type: 'TestEvent', data: '{}', metadata: '{}', created_at: Time.now },
30
- { id: SecureRandom.uuid, event_type: 'TestEvent', data: '{}', metadata: '{}', created_at: Time.now }
31
- ]
32
-
33
- relation.command(:create).call(events[0..1])
34
-
35
- expect do
36
- env.handle_error(:unique_violation) { relation.insert(events[0]) }
37
- end.to raise_error(RubyEventStore::EventDuplicatedInStream)
38
- expect do
39
- env.handle_error(:unique_violation) { relation.insert(events[1]) }
40
- end.to raise_error(RubyEventStore::EventDuplicatedInStream)
41
- expect { relation.insert(events[2]) }.not_to raise_error
42
- expect do
43
- env.handle_error(:unique_violation) { relation.insert(events[2]) }
44
- end.to raise_error(RubyEventStore::EventDuplicatedInStream)
45
- end
46
-
47
- specify '#by_pk finds tuples by ID' do
48
- events = [
49
- { id: id = SecureRandom.uuid, event_type: 'TestEvent', data: '{}', metadata: '{}', created_at: Time.now },
50
- { id: SecureRandom.uuid, event_type: 'TestEvent', data: '{}', metadata: '{}', created_at: Time.now },
51
- { id: SecureRandom.uuid, event_type: 'TestEvent', data: '{}', metadata: '{}', created_at: Time.now }
52
- ]
53
-
54
- relation.command(:create).call(events)
55
-
56
- expect(relation.by_pk(id).to_a.size).to eq(1)
57
- expect(relation.by_pk(id).to_a.map { |e| e[:id] }).to eq([id])
58
- end
59
-
60
- specify 'each method returns proper type' do
61
- events = [
62
- { id: id = SecureRandom.uuid, event_type: 'TestEvent', data: '{}', metadata: '{}', created_at: Time.now },
63
- { id: SecureRandom.uuid, event_type: 'TestEvent', data: '{}', metadata: '{}', created_at: Time.now },
64
- { id: SecureRandom.uuid, event_type: 'TestEvent', data: '{}', metadata: '{}', created_at: Time.now }
65
- ]
66
-
67
- relation.command(:create).call(events)
68
-
69
- expect(relation.by_pk(SecureRandom.uuid).exist?).to eq(false)
70
- expect(relation.by_pk(id).exist?).to eq(true)
71
-
72
- expect(relation.by_pk(id)).to be_a(relation.class)
73
- # expect(relation.by_pk(id).first).to be_a(::ROM::Struct)
74
- end
75
- end