From 619b9129c9643abe00a393d175ff8348a73a61e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Wed, 18 Jul 2018 13:50:27 +0200 Subject: [PATCH 01/27] New methods to read first & last events from specified read spec --- .../ruby_event_store/in_memory_repository.rb | 4 +++ .../lib/ruby_event_store/specification.rb | 35 ++++++++++++++---- ruby_event_store/spec/specification_spec.rb | 36 ++++++++++++++----- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb b/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb index 521275d1b0..9ca8077b2f 100644 --- a/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb +++ b/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb @@ -43,6 +43,10 @@ def read(spec) if spec.batched? batch_reader = ->(offset, limit) { events.drop(offset).take(limit) } BatchEnumerator.new(spec.batch_size, events.size, batch_reader).each + elsif spec.first? + events.first + elsif spec.last? + events.last else events.each end diff --git a/ruby_event_store/lib/ruby_event_store/specification.rb b/ruby_event_store/lib/ruby_event_store/specification.rb index 5d7efe6990..951970c24b 100644 --- a/ruby_event_store/lib/ruby_event_store/specification.rb +++ b/ruby_event_store/lib/ruby_event_store/specification.rb @@ -7,10 +7,16 @@ class Specification NO_LIMIT = Object.new.freeze # @private # @api private - NO_BATCH = Object.new.freeze + FIRST = Object.new.freeze + # @private + # @api private + LAST = Object.new.freeze + # @private + # @api private + BATCH = Object.new.freeze DEFAULT_BATCH_SIZE = 100 - class Result < Struct.new(:direction, :start, :count, :stream, :batch_size) + class Result < Struct.new(:direction, :start, :count, :stream, :read_as, :batch_size) def limit? !count.equal?(NO_LIMIT) end @@ -36,7 +42,15 @@ def backward? end def batched? - !batch_size.equal?(NO_BATCH) + read_as == BATCH + end + + def first? + read_as == FIRST + end + + def last? + read_as == LAST end end private_constant :Result @@ -47,7 +61,7 @@ def batched? # @api private # @private - def initialize(repository, mapper, result = Result.new(:forward, :head, NO_LIMIT, Stream.new(GLOBAL_STREAM), NO_BATCH)) + def initialize(repository, mapper, result = Result.new(:forward, :head, NO_LIMIT, Stream.new(GLOBAL_STREAM), nil, DEFAULT_BATCH_SIZE)) @mapper = mapper @repository = repository @result = result @@ -115,8 +129,7 @@ def limit(count) def each_batch return to_enum(:each_batch) unless block_given? - result_ = result.batched? ? result : result.tap { |r| r.batch_size = DEFAULT_BATCH_SIZE } - repository.read(result_).each do |batch| + repository.read(result.tap { |r| r.read_as = BATCH }).each do |batch| yield batch.map { |serialized_record| mapper.serialized_record_to_event(serialized_record) } end end @@ -149,10 +162,18 @@ def each # @param batch_size [Integer] number of events to read in a single batch # @return [Specification] def in_batches(batch_size = DEFAULT_BATCH_SIZE) - Specification.new(repository, mapper, result.dup.tap { |r| r.batch_size = batch_size }) + Specification.new(repository, mapper, result.tap { |r| r.read_as = BATCH; r.batch_size = batch_size }) end alias :in_batches_of :in_batches + def first + mapper.serialized_record_to_event(repository.read(result.tap { |r| r.read_as = FIRST })) + end + + def last + mapper.serialized_record_to_event(repository.read(result.tap { |r| r.read_as = LAST })) + end + private attr_reader :repository, :mapper end diff --git a/ruby_event_store/spec/specification_spec.rb b/ruby_event_store/spec/specification_spec.rb index af96fd0f91..9ccc0d682a 100644 --- a/ruby_event_store/spec/specification_spec.rb +++ b/ruby_event_store/spec/specification_spec.rb @@ -139,34 +139,39 @@ module RubyEventStore start: :head, count: Specification::NO_LIMIT, stream_name: GLOBAL_STREAM, - batch_size: Specification::NO_BATCH + read_as: nil, + batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(specification.from(event_id)).to match_result({ direction: :forward, start: event_id, count: Specification::NO_LIMIT, stream_name: GLOBAL_STREAM, - batch_size: Specification::NO_BATCH + read_as: nil, + batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(specification.limit(10)).to match_result({ direction: :forward, start: :head, count: 10, stream_name: GLOBAL_STREAM, - batch_size: Specification::NO_BATCH + read_as: nil, + batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(specification.stream(stream_name)).to match_result({ direction: :forward, start: :head, count: Specification::NO_LIMIT, stream_name: stream_name, - batch_size: Specification::NO_BATCH + read_as: nil, + batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(specification.in_batches).to match_result({ direction: :forward, start: :head, count: Specification::NO_LIMIT, stream_name: GLOBAL_STREAM, + read_as: Specification::BATCH, batch_size: 100 }) expect(specification).to match_result({ @@ -174,21 +179,24 @@ module RubyEventStore start: :head, count: Specification::NO_LIMIT, stream_name: GLOBAL_STREAM, - batch_size: Specification::NO_BATCH + read_as: Specification::BATCH, + batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(backward_specifcation.forward).to match_result({ direction: :forward, start: :head, count: Specification::NO_LIMIT, stream_name: GLOBAL_STREAM, - batch_size: Specification::NO_BATCH + read_as: nil, + batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(backward_specifcation).to match_result({ direction: :backward, start: :head, count: Specification::NO_LIMIT, stream_name: GLOBAL_STREAM, - batch_size: Specification::NO_BATCH + read_as: nil, + batch_size: Specification::DEFAULT_BATCH_SIZE }) end end @@ -248,7 +256,7 @@ module RubyEventStore specify { expect(specification.in_batches).to match_result(batch_size: 100) } - specify { expect(specification).to match_result(batch_size: Specification::NO_BATCH) } + specify { expect(specification).to match_result(batch_size: Specification::DEFAULT_BATCH_SIZE) } specify { expect(specification.in_batches(1000)).to match_result(batch_size: 1000) } @@ -295,6 +303,18 @@ module RubyEventStore expect(specification.each_batch.to_a).not_to eq(specification.in_batches(1000).each_batch.to_a) end + specify do + records = 5.times.map { test_record } + repository.append_to_stream(records, Stream.new("Dummy"), ExpectedVersion.none) + + expect(specification.first).to eq(TestEvent.new(event_id: records[0].event_id)) + expect(specification.last).to eq(TestEvent.new(event_id: records[4].event_id)) + expect(specification.from(records[2].event_id).first).to eq(TestEvent.new(event_id: records[3].event_id)) + expect(specification.from(records[2].event_id).last).to eq(TestEvent.new(event_id: records[4].event_id)) + expect(specification.from(records[2].event_id).backward.first).to eq(TestEvent.new(event_id: records[1].event_id)) + expect(specification.from(records[2].event_id).backward.last).to eq(TestEvent.new(event_id: records[0].event_id)) + end + let(:repository) { InMemoryRepository.new } let(:mapper) { Mappers::Default.new } let(:specification) { Specification.new(repository, mapper) } From 1fc66bfc3478bf0f66e5bc9c91e9fa5081848c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Wed, 18 Jul 2018 13:51:26 +0200 Subject: [PATCH 02/27] Implementation of first & last read spec methond on AR event repositories --- .../legacy/event_repository.rb | 4 ++++ .../event_repository_reader.rb | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb b/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb index e6e1855be4..721fb4ac4a 100644 --- a/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb +++ b/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb @@ -68,6 +68,10 @@ def read(spec) if spec.batched? batch_reader = ->(offset, limit) { stream.offset(offset).limit(limit).map(&method(:build_event_entity)) } RubyEventStore::BatchEnumerator.new(spec.batch_size, total_limit(spec), batch_reader).each + elsif spec.first? + build_event_entity(stream.first) + elsif spec.last? + build_event_entity(stream.last) else stream.map(&method(:build_event_entity)).each end diff --git a/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb b/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb index d0ffefa7b2..6c5fb46607 100644 --- a/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb +++ b/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb @@ -35,6 +35,10 @@ def read(spec) if spec.batched? batch_reader = ->(offset, limit) { stream.offset(offset).limit(limit).map(&method(:build_event_instance)) } RubyEventStore::BatchEnumerator.new(spec.batch_size, total_limit(spec), batch_reader).each + elsif spec.first? + build_event_instance(stream.first) + elsif spec.last? + build_event_instance(stream.last) else stream.map(&method(:build_event_instance)).each end From b96360affb05f4f46789790f02b41f5494d34cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Wed, 18 Jul 2018 14:50:05 +0200 Subject: [PATCH 03/27] Additional requirements for event repository implementations --- .../ruby_event_store/spec/event_repository_lint.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb b/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb index d343856201..ba113dcb96 100644 --- a/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb +++ b/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb @@ -1100,4 +1100,18 @@ def read_all_streams_backward(repository, start, count) expect(batches[0].size).to eq(99) expect(batches[0]).to eq(events[101..199]) end + + specify do + events = Array.new(5) { SRecord.new } + repository.append_to_stream( + events, + RubyEventStore::Stream.new(RubyEventStore::GLOBAL_STREAM), + RubyEventStore::ExpectedVersion.any + ) + + expect(repository.read(specification.result.tap{|r| r.read_as = RubyEventStore::Specification::FIRST})).to eq(events[0]) + expect(repository.read(specification.result.tap{|r| r.read_as = RubyEventStore::Specification::LAST})).to eq(events[4]) + expect(repository.read(specification.backward.result.tap{|r| r.read_as = RubyEventStore::Specification::FIRST})).to eq(events[4]) + expect(repository.read(specification.backward.result.tap{|r| r.read_as = RubyEventStore::Specification::LAST})).to eq(events[0]) + end end From afaaf64689ba9e84da22214f1f67866137b140c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Wed, 18 Jul 2018 14:50:38 +0200 Subject: [PATCH 04/27] Handle first/last reads in ROM event repository --- .../lib/ruby_event_store/rom/event_repository.rb | 4 +++- .../ruby_event_store/rom/repositories/events.rb | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb b/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb index 569fb8a154..a952bfbc92 100644 --- a/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb +++ b/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb @@ -11,7 +11,7 @@ class EventRepository def initialize(rom: ROM.env) raise ArgumentError, "Must specify rom" unless rom && rom.instance_of?(Env) - + @rom = rom @events = Repositories::Events.new(rom.container) @stream_entries = Repositories::StreamEntries.new(rom.container) @@ -77,6 +77,7 @@ def last_stream_event(stream) stream, from: :head, limit: 1, + read_as: nil, batch_size: nil ).first end @@ -95,6 +96,7 @@ def read(specification) specification.stream, from: specification.start, limit: (specification.count if specification.limit?), + read_as: specification.read_as, batch_size: (specification.batch_size if specification.batched?) ) end diff --git a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb index 9ca6c9cd35..312703b406 100644 --- a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb +++ b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb @@ -35,12 +35,12 @@ def by_id(event_id) events.map_with(:event_to_serialized_record).by_pk(event_id).one! end - def read(direction, stream, from:, limit:, batch_size:) + def read(direction, stream, from:, limit:, read_as:, batch_size:) unless from.equal?(:head) offset_entry_id = stream_entries.by_stream_and_event_id(stream, from).fetch(:id) end - if batch_size + if read_as == RubyEventStore::Specification::BATCH reader = ->(offset, limit) do stream_entries .ordered(direction, stream, offset_entry_id) @@ -51,6 +51,18 @@ def read(direction, stream, from:, limit:, batch_size:) .to_ary end BatchEnumerator.new(batch_size, limit || Float::INFINITY, reader).each + elsif read_as == RubyEventStore::Specification::FIRST + stream_entries + .ordered(direction, stream, offset_entry_id) + .combine(:event) + .map_with(:stream_entry_to_serialized_record, auto_struct: false) + .to_ary.first + elsif read_as == RubyEventStore::Specification::LAST + stream_entries + .ordered(direction, stream, offset_entry_id) + .combine(:event) + .map_with(:stream_entry_to_serialized_record, auto_struct: false) + .to_ary.last else stream_entries .ordered(direction, stream, offset_entry_id) From d19b46cae56333200118735e6f7cd9735f998051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Wed, 18 Jul 2018 14:54:41 +0200 Subject: [PATCH 05/27] Simplify fetching last event from a stream using new API --- rails_event_store-rspec/lib/rails_event_store/rspec/publish.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rails_event_store-rspec/lib/rails_event_store/rspec/publish.rb b/rails_event_store-rspec/lib/rails_event_store/rspec/publish.rb index 53f6b003c9..7fc0526a58 100644 --- a/rails_event_store-rspec/lib/rails_event_store/rspec/publish.rb +++ b/rails_event_store-rspec/lib/rails_event_store/rspec/publish.rb @@ -79,7 +79,7 @@ def match_events? end def last_event(spec) - spec.backward.limit(1).each.first + spec.last end def raise_event_store_not_set From 8af5d0d52c984e8e1f46b4fa86c9da94c7a21898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Thu, 19 Jul 2018 22:55:55 +0200 Subject: [PATCH 06/27] Unnecessary code remove --- .../lib/ruby_event_store/rom/repositories/events.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb index 312703b406..21ed19cc83 100644 --- a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb +++ b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb @@ -56,7 +56,7 @@ def read(direction, stream, from:, limit:, read_as:, batch_size:) .ordered(direction, stream, offset_entry_id) .combine(:event) .map_with(:stream_entry_to_serialized_record, auto_struct: false) - .to_ary.first + .first elsif read_as == RubyEventStore::Specification::LAST stream_entries .ordered(direction, stream, offset_entry_id) From 5de1b93ca4cb8109d6f8f7050fbab06e1e9b8b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Thu, 19 Jul 2018 23:02:18 +0200 Subject: [PATCH 07/27] Killing the mutants --- ruby_event_store/lib/ruby_event_store/specification.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ruby_event_store/lib/ruby_event_store/specification.rb b/ruby_event_store/lib/ruby_event_store/specification.rb index 951970c24b..e07952ca6d 100644 --- a/ruby_event_store/lib/ruby_event_store/specification.rb +++ b/ruby_event_store/lib/ruby_event_store/specification.rb @@ -42,15 +42,15 @@ def backward? end def batched? - read_as == BATCH + read_as.equal?(BATCH) end def first? - read_as == FIRST + read_as.equal?(FIRST) end def last? - read_as == LAST + read_as.equal?(LAST) end end private_constant :Result From a02ff950c35150b02025c3a03dd4fc466ac1090f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Thu, 19 Jul 2018 23:13:23 +0200 Subject: [PATCH 08/27] No need for that if here --- .../lib/ruby_event_store/rom/event_repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb b/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb index a952bfbc92..306b61a4f1 100644 --- a/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb +++ b/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb @@ -97,7 +97,7 @@ def read(specification) from: specification.start, limit: (specification.count if specification.limit?), read_as: specification.read_as, - batch_size: (specification.batch_size if specification.batched?) + batch_size: specification.batch_size ) end From 7082e7ae25679abf86e623a93977ee4aeedb3206 Mon Sep 17 00:00:00 2001 From: Joel Van Horn Date: Fri, 20 Jul 2018 18:58:50 -0400 Subject: [PATCH 09/27] DRY'd up and work around `last` to use `first` --- .../rom/repositories/events.rb | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb index 21ed19cc83..b932139ba0 100644 --- a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb +++ b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb @@ -35,43 +35,46 @@ def by_id(event_id) events.map_with(:event_to_serialized_record).by_pk(event_id).one! end + MATERIALIZE_READ_AS = { + RubyEventStore::Specification::BATCH => :to_ary, + RubyEventStore::Specification::FIRST => :first, + RubyEventStore::Specification::LAST => :first + }.freeze + def read(direction, stream, from:, limit:, read_as:, batch_size:) unless from.equal?(:head) offset_entry_id = stream_entries.by_stream_and_event_id(stream, from).fetch(:id) end + # Note: `last` is problematic, so we switch direction and get `first`. + # See `MATERIALIZE_READ_AS` + if read_as == RubyEventStore::Specification::LAST + direction = direction == :forward ? :backward : :forward + end + + query = stream_entries.ordered(direction, stream, offset_entry_id) + if read_as == RubyEventStore::Specification::BATCH reader = ->(offset, limit) do - stream_entries - .ordered(direction, stream, offset_entry_id) - .offset(offset) - .take(limit) - .combine(:event) - .map_with(:stream_entry_to_serialized_record, auto_struct: false) - .to_ary + query_builder(query, offset: offset, limit: limit).to_ary end BatchEnumerator.new(batch_size, limit || Float::INFINITY, reader).each - elsif read_as == RubyEventStore::Specification::FIRST - stream_entries - .ordered(direction, stream, offset_entry_id) - .combine(:event) - .map_with(:stream_entry_to_serialized_record, auto_struct: false) - .first - elsif read_as == RubyEventStore::Specification::LAST - stream_entries - .ordered(direction, stream, offset_entry_id) - .combine(:event) - .map_with(:stream_entry_to_serialized_record, auto_struct: false) - .to_ary.last else - stream_entries - .ordered(direction, stream, offset_entry_id) - .take(limit) - .combine(:event) - .map_with(:stream_entry_to_serialized_record, auto_struct: false) - .each + materialize_method = MATERIALIZE_READ_AS.fetch(read_as, :each) + query_builder(query, limit: limit).__send__(materialize_method) end end + + protected + + def query_builder(query, offset: nil, limit: nil) + query = query.offset(offset) if offset + query = query.take(limit) if limit + + query + .combine(:event) + .map_with(:stream_entry_to_serialized_record, auto_struct: false) + end end end end From fbc5c81861747dd914f8203560740e4b2466ab5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Sun, 22 Jul 2018 13:10:10 +0200 Subject: [PATCH 10/27] Additional test to kill mutations --- ruby_event_store/spec/specification_spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ruby_event_store/spec/specification_spec.rb b/ruby_event_store/spec/specification_spec.rb index 9ccc0d682a..81b4b62bf5 100644 --- a/ruby_event_store/spec/specification_spec.rb +++ b/ruby_event_store/spec/specification_spec.rb @@ -315,6 +315,26 @@ module RubyEventStore expect(specification.from(records[2].event_id).backward.last).to eq(TestEvent.new(event_id: records[0].event_id)) end + specify do + repository.append_to_stream([test_record], Stream.new("Dummy"), ExpectedVersion.none) + + expect(specification.result.batched?).to be_falsey + expect(specification.result.first?).to be_falsey + expect(specification.result.last?).to be_falsey + specification.first + expect(specification.result.batched?).to be_falsey + expect(specification.result.first?).to be_truthy + expect(specification.result.last?).to be_falsey + specification.last + expect(specification.result.batched?).to be_falsey + expect(specification.result.first?).to be_falsey + expect(specification.result.last?).to be_truthy + specification.in_batches + expect(specification.result.batched?).to be_truthy + expect(specification.result.first?).to be_falsey + expect(specification.result.last?).to be_falsey + end + let(:repository) { InMemoryRepository.new } let(:mapper) { Mappers::Default.new } let(:specification) { Specification.new(repository, mapper) } From eb9bf12892e9c009c55d7efca24a9270be016534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Sun, 22 Jul 2018 13:28:11 +0200 Subject: [PATCH 11/27] API documentation --- ruby_event_store/lib/ruby_event_store/specification.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ruby_event_store/lib/ruby_event_store/specification.rb b/ruby_event_store/lib/ruby_event_store/specification.rb index e07952ca6d..6e6cbba702 100644 --- a/ruby_event_store/lib/ruby_event_store/specification.rb +++ b/ruby_event_store/lib/ruby_event_store/specification.rb @@ -166,10 +166,20 @@ def in_batches(batch_size = DEFAULT_BATCH_SIZE) end alias :in_batches_of :in_batches + # Executes the query based on the specification built up to this point. + # Returns the first event in specified collection of events. + # {http://railseventstore.org/docs/read/ Find out more}. + # + # @return [Event] def first mapper.serialized_record_to_event(repository.read(result.tap { |r| r.read_as = FIRST })) end + # Executes the query based on the specification built up to this point. + # Returns the last event in specified collection of events. + # {http://railseventstore.org/docs/read/ Find out more}. + # + # @return [Event] def last mapper.serialized_record_to_event(repository.read(result.tap { |r| r.read_as = LAST })) end From 8f1f88e81b94d4dce141d3c204543fcaba7c7af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Sun, 22 Jul 2018 14:33:54 +0200 Subject: [PATCH 12/27] Stop using Specification private internals in ROM repository --- .../ruby_event_store/rom/event_repository.rb | 18 ++-------- .../rom/repositories/events.rb | 34 +++++++++---------- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb b/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb index 306b61a4f1..b611ecf87d 100644 --- a/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb +++ b/ruby_event_store-rom/lib/ruby_event_store/rom/event_repository.rb @@ -72,14 +72,7 @@ def has_event?(event_id) end def last_stream_event(stream) - @events.read( - :backward, - stream, - from: :head, - limit: 1, - read_as: nil, - batch_size: nil - ).first + @events.last_stream_event(stream) end def read_event(event_id) @@ -91,14 +84,7 @@ def read_event(event_id) def read(specification) raise ReservedInternalName if specification.stream_name.eql?(@stream_entries.stream_entries.class::SERIALIZED_GLOBAL_STREAM_NAME) - @events.read( - specification.direction, - specification.stream, - from: specification.start, - limit: (specification.count if specification.limit?), - read_as: specification.read_as, - batch_size: specification.batch_size - ) + @events.read(specification) end private diff --git a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb index b932139ba0..28eace9d65 100644 --- a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb +++ b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb @@ -35,33 +35,33 @@ def by_id(event_id) events.map_with(:event_to_serialized_record).by_pk(event_id).one! end - MATERIALIZE_READ_AS = { - RubyEventStore::Specification::BATCH => :to_ary, - RubyEventStore::Specification::FIRST => :first, - RubyEventStore::Specification::LAST => :first - }.freeze + def last_stream_event(stream) + query = stream_entries.ordered(:backward, stream) + query = query_builder(query, limit: 1) + query.first + end - def read(direction, stream, from:, limit:, read_as:, batch_size:) - unless from.equal?(:head) - offset_entry_id = stream_entries.by_stream_and_event_id(stream, from).fetch(:id) + def read(specification) + unless specification.head? + offset_entry_id = stream_entries.by_stream_and_event_id(specification.stream, specification.start).fetch(:id) end - # Note: `last` is problematic, so we switch direction and get `first`. - # See `MATERIALIZE_READ_AS` - if read_as == RubyEventStore::Specification::LAST - direction = direction == :forward ? :backward : :forward + direction = specification.direction + limit = specification.count if specification.limit? + if specification.last? + direction = specification.forward? ? :backward : :forward end - query = stream_entries.ordered(direction, stream, offset_entry_id) + query = stream_entries.ordered(direction, specification.stream, offset_entry_id) - if read_as == RubyEventStore::Specification::BATCH + if specification.batched? reader = ->(offset, limit) do query_builder(query, offset: offset, limit: limit).to_ary end - BatchEnumerator.new(batch_size, limit || Float::INFINITY, reader).each + BatchEnumerator.new(specification.batch_size, limit || Float::INFINITY, reader).each else - materialize_method = MATERIALIZE_READ_AS.fetch(read_as, :each) - query_builder(query, limit: limit).__send__(materialize_method) + query = query_builder(query, limit: limit) + specification.first? || specification.last? ? query.first : query.each end end From 3c6b20560cc498815c258045ae79df7a279c6a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Mon, 23 Jul 2018 17:14:51 +0200 Subject: [PATCH 13/27] Specification is immutable - never modify it directly --- .../spec/event_repository_lint.rb | 9 +++---- .../lib/ruby_event_store/specification.rb | 14 ++++++++--- ruby_event_store/spec/specification_spec.rb | 24 +++++++++---------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb b/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb index ba113dcb96..9a52e65d82 100644 --- a/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb +++ b/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb @@ -1109,9 +1109,10 @@ def read_all_streams_backward(repository, start, count) RubyEventStore::ExpectedVersion.any ) - expect(repository.read(specification.result.tap{|r| r.read_as = RubyEventStore::Specification::FIRST})).to eq(events[0]) - expect(repository.read(specification.result.tap{|r| r.read_as = RubyEventStore::Specification::LAST})).to eq(events[4]) - expect(repository.read(specification.backward.result.tap{|r| r.read_as = RubyEventStore::Specification::FIRST})).to eq(events[4]) - expect(repository.read(specification.backward.result.tap{|r| r.read_as = RubyEventStore::Specification::LAST})).to eq(events[0]) + expect(repository.read(specification.read_first.result)).to eq(events[0]) + expect(repository.read(specification.read_last.result)).to eq(events[4]) + + expect(repository.read(specification.backward.read_first.result)).to eq(events[4]) + expect(repository.read(specification.backward.read_last.result)).to eq(events[0]) end end diff --git a/ruby_event_store/lib/ruby_event_store/specification.rb b/ruby_event_store/lib/ruby_event_store/specification.rb index 6e6cbba702..93fae97472 100644 --- a/ruby_event_store/lib/ruby_event_store/specification.rb +++ b/ruby_event_store/lib/ruby_event_store/specification.rb @@ -162,17 +162,25 @@ def each # @param batch_size [Integer] number of events to read in a single batch # @return [Specification] def in_batches(batch_size = DEFAULT_BATCH_SIZE) - Specification.new(repository, mapper, result.tap { |r| r.read_as = BATCH; r.batch_size = batch_size }) + Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = BATCH; r.batch_size = batch_size }) end alias :in_batches_of :in_batches + def read_first + Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = FIRST }) + end + + def read_last + Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = LAST }) + end + # Executes the query based on the specification built up to this point. # Returns the first event in specified collection of events. # {http://railseventstore.org/docs/read/ Find out more}. # # @return [Event] def first - mapper.serialized_record_to_event(repository.read(result.tap { |r| r.read_as = FIRST })) + mapper.serialized_record_to_event(repository.read(read_first.result)) end # Executes the query based on the specification built up to this point. @@ -181,7 +189,7 @@ def first # # @return [Event] def last - mapper.serialized_record_to_event(repository.read(result.tap { |r| r.read_as = LAST })) + mapper.serialized_record_to_event(repository.read(read_last.result)) end private diff --git a/ruby_event_store/spec/specification_spec.rb b/ruby_event_store/spec/specification_spec.rb index 81b4b62bf5..89c349b734 100644 --- a/ruby_event_store/spec/specification_spec.rb +++ b/ruby_event_store/spec/specification_spec.rb @@ -321,18 +321,18 @@ module RubyEventStore expect(specification.result.batched?).to be_falsey expect(specification.result.first?).to be_falsey expect(specification.result.last?).to be_falsey - specification.first - expect(specification.result.batched?).to be_falsey - expect(specification.result.first?).to be_truthy - expect(specification.result.last?).to be_falsey - specification.last - expect(specification.result.batched?).to be_falsey - expect(specification.result.first?).to be_falsey - expect(specification.result.last?).to be_truthy - specification.in_batches - expect(specification.result.batched?).to be_truthy - expect(specification.result.first?).to be_falsey - expect(specification.result.last?).to be_falsey + + expect(specification.read_first.result.batched?).to be_falsey + expect(specification.read_first.result.first?).to be_truthy + expect(specification.read_first.result.last?).to be_falsey + + expect(specification.read_last.result.batched?).to be_falsey + expect(specification.read_last.result.first?).to be_falsey + expect(specification.read_last.result.last?).to be_truthy + + expect(specification.in_batches.result.batched?).to be_truthy + expect(specification.in_batches.result.first?).to be_falsey + expect(specification.in_batches.result.last?).to be_falsey end let(:repository) { InMemoryRepository.new } From 14b2936064b93d96af987930974f0e3452bdb518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Mon, 23 Jul 2018 17:24:32 +0200 Subject: [PATCH 14/27] Fix expectation - no mutatable specification --- ruby_event_store/spec/specification_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby_event_store/spec/specification_spec.rb b/ruby_event_store/spec/specification_spec.rb index 89c349b734..e975dbc711 100644 --- a/ruby_event_store/spec/specification_spec.rb +++ b/ruby_event_store/spec/specification_spec.rb @@ -179,7 +179,7 @@ module RubyEventStore start: :head, count: Specification::NO_LIMIT, stream_name: GLOBAL_STREAM, - read_as: Specification::BATCH, + read_as: nil, batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(backward_specifcation.forward).to match_result({ From 970452d6121fd6ef595500400ebb2f8d7d2665b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Mon, 23 Jul 2018 17:35:03 +0200 Subject: [PATCH 15/27] Symbols over constants That's just an implementation details that should never leak out of the Specification class. Still used in specification tests however should it also should be avoided. --- .../lib/ruby_event_store/specification.rb | 30 ++++++----------- ruby_event_store/spec/specification_spec.rb | 32 +++++++++---------- 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/ruby_event_store/lib/ruby_event_store/specification.rb b/ruby_event_store/lib/ruby_event_store/specification.rb index 93fae97472..1dd62a95d6 100644 --- a/ruby_event_store/lib/ruby_event_store/specification.rb +++ b/ruby_event_store/lib/ruby_event_store/specification.rb @@ -2,23 +2,11 @@ module RubyEventStore # Used for building and executing the query specification. class Specification - # @private - # @api private - NO_LIMIT = Object.new.freeze - # @private - # @api private - FIRST = Object.new.freeze - # @private - # @api private - LAST = Object.new.freeze - # @private - # @api private - BATCH = Object.new.freeze DEFAULT_BATCH_SIZE = 100 class Result < Struct.new(:direction, :start, :count, :stream, :read_as, :batch_size) def limit? - !count.equal?(NO_LIMIT) + !count.nil? end def global_stream? @@ -42,15 +30,15 @@ def backward? end def batched? - read_as.equal?(BATCH) + read_as.equal?(:batch) end def first? - read_as.equal?(FIRST) + read_as.equal?(:first) end def last? - read_as.equal?(LAST) + read_as.equal?(:last) end end private_constant :Result @@ -61,7 +49,7 @@ def last? # @api private # @private - def initialize(repository, mapper, result = Result.new(:forward, :head, NO_LIMIT, Stream.new(GLOBAL_STREAM), nil, DEFAULT_BATCH_SIZE)) + def initialize(repository, mapper, result = Result.new(:forward, :head, nil, Stream.new(GLOBAL_STREAM), :all, DEFAULT_BATCH_SIZE)) @mapper = mapper @repository = repository @result = result @@ -129,7 +117,7 @@ def limit(count) def each_batch return to_enum(:each_batch) unless block_given? - repository.read(result.tap { |r| r.read_as = BATCH }).each do |batch| + repository.read(result.tap { |r| r.read_as = :batch }).each do |batch| yield batch.map { |serialized_record| mapper.serialized_record_to_event(serialized_record) } end end @@ -162,16 +150,16 @@ def each # @param batch_size [Integer] number of events to read in a single batch # @return [Specification] def in_batches(batch_size = DEFAULT_BATCH_SIZE) - Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = BATCH; r.batch_size = batch_size }) + Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = :batch; r.batch_size = batch_size }) end alias :in_batches_of :in_batches def read_first - Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = FIRST }) + Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = :first }) end def read_last - Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = LAST }) + Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = :last }) end # Executes the query based on the specification built up to this point. diff --git a/ruby_event_store/spec/specification_spec.rb b/ruby_event_store/spec/specification_spec.rb index e975dbc711..88e07e9793 100644 --- a/ruby_event_store/spec/specification_spec.rb +++ b/ruby_event_store/spec/specification_spec.rb @@ -8,7 +8,7 @@ module RubyEventStore specify { expect(specification).to match_result({ start: :head }) } - specify { expect(specification).to match_result({ count: Specification::NO_LIMIT }) } + specify { expect(specification).to match_result({ count: nil }) } specify { expect(specification).to match_result({ stream_name: GLOBAL_STREAM }) } @@ -137,17 +137,17 @@ module RubyEventStore expect(backward_specifcation = specification.backward).to match_result({ direction: :backward, start: :head, - count: Specification::NO_LIMIT, + count: nil, stream_name: GLOBAL_STREAM, - read_as: nil, + read_as: :all, batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(specification.from(event_id)).to match_result({ direction: :forward, start: event_id, - count: Specification::NO_LIMIT, + count: nil, stream_name: GLOBAL_STREAM, - read_as: nil, + read_as: :all, batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(specification.limit(10)).to match_result({ @@ -155,47 +155,47 @@ module RubyEventStore start: :head, count: 10, stream_name: GLOBAL_STREAM, - read_as: nil, + read_as: :all, batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(specification.stream(stream_name)).to match_result({ direction: :forward, start: :head, - count: Specification::NO_LIMIT, + count: nil, stream_name: stream_name, - read_as: nil, + read_as: :all, batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(specification.in_batches).to match_result({ direction: :forward, start: :head, - count: Specification::NO_LIMIT, + count: nil, stream_name: GLOBAL_STREAM, - read_as: Specification::BATCH, + read_as: :batch, batch_size: 100 }) expect(specification).to match_result({ direction: :forward, start: :head, - count: Specification::NO_LIMIT, + count: nil, stream_name: GLOBAL_STREAM, - read_as: nil, + read_as: :all, batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(backward_specifcation.forward).to match_result({ direction: :forward, start: :head, - count: Specification::NO_LIMIT, + count: nil, stream_name: GLOBAL_STREAM, - read_as: nil, + read_as: :all, batch_size: Specification::DEFAULT_BATCH_SIZE }) expect(backward_specifcation).to match_result({ direction: :backward, start: :head, - count: Specification::NO_LIMIT, + count: nil, stream_name: GLOBAL_STREAM, - read_as: nil, + read_as: :all, batch_size: Specification::DEFAULT_BATCH_SIZE }) end From b319a246d35bb74ef653798e5cfee3b709b5d5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Mon, 23 Jul 2018 18:18:13 +0200 Subject: [PATCH 16/27] Killing mutants --- ruby_event_store/spec/specification_spec.rb | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/ruby_event_store/spec/specification_spec.rb b/ruby_event_store/spec/specification_spec.rb index 88e07e9793..512997b415 100644 --- a/ruby_event_store/spec/specification_spec.rb +++ b/ruby_event_store/spec/specification_spec.rb @@ -132,6 +132,23 @@ module RubyEventStore end end + specify do + with_event_of_id(event_id) do + specs = [ + specification.forward, + specification.backward, + specification.in_batches, + specification.read_first, + specification.read_last, + specification.limit(10), + specification.from(event_id), + specification.stream(stream_name), + ] + expect(specs.map{|s| s.send(:repository)}.uniq).to eq([repository]) + expect(specs.map{|s| s.send(:mapper)}.uniq).to eq([mapper]) + end + end + specify 'immutable specification' do with_event_of_id(event_id) do expect(backward_specifcation = specification.backward).to match_result({ @@ -198,6 +215,38 @@ module RubyEventStore read_as: :all, batch_size: Specification::DEFAULT_BATCH_SIZE }) + expect(specification.read_first).to match_result({ + direction: :forward, + start: :head, + count: nil, + stream_name: GLOBAL_STREAM, + read_as: :first, + batch_size: 100 + }) + expect(specification).to match_result({ + direction: :forward, + start: :head, + count: nil, + stream_name: GLOBAL_STREAM, + read_as: :all, + batch_size: Specification::DEFAULT_BATCH_SIZE + }) + expect(specification.read_last).to match_result({ + direction: :forward, + start: :head, + count: nil, + stream_name: GLOBAL_STREAM, + read_as: :last, + batch_size: 100 + }) + expect(specification).to match_result({ + direction: :forward, + start: :head, + count: nil, + stream_name: GLOBAL_STREAM, + read_as: :all, + batch_size: Specification::DEFAULT_BATCH_SIZE + }) end end From 083b4ecad5f47ea3ad4f85a53a71c093b247f019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Mon, 23 Jul 2018 22:47:41 +0200 Subject: [PATCH 17/27] Additional tests to verify offset works correctly when reading first/last event --- .../lib/ruby_event_store/spec/event_repository_lint.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb b/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb index 9a52e65d82..7ab3c93f27 100644 --- a/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb +++ b/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb @@ -1114,5 +1114,10 @@ def read_all_streams_backward(repository, start, count) expect(repository.read(specification.backward.read_first.result)).to eq(events[4]) expect(repository.read(specification.backward.read_last.result)).to eq(events[0]) + + expect(repository.read(specification.from(events[2].event_id).read_first.result)).to eq(events[3]) + expect(repository.read(specification.from(events[2].event_id).read_last.result)).to eq(events[4]) + expect(repository.read(specification.from(events[2].event_id).backward.read_first.result)).to eq(events[1]) + expect(repository.read(specification.from(events[2].event_id).backward.read_last.result)).to eq(events[0]) end end From 8c10eabbd2f55312c98dceb911f7438946eddcd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Tue, 24 Jul 2018 00:31:28 +0200 Subject: [PATCH 18/27] Use specification limit instead of count Limit is a count value or infinity. Becuase when we do not set count for specification our limit is infinity. It cound be clearly spotted in total_limit method of AR eveny repository. --- .../legacy/event_repository.rb | 4 ++-- .../event_repository_reader.rb | 4 ++-- .../lib/ruby_event_store/rom/repositories/events.rb | 2 +- ruby_event_store/lib/ruby_event_store/in_memory_repository.rb | 2 +- ruby_event_store/lib/ruby_event_store/specification.rb | 4 ++++ 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb b/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb index 721fb4ac4a..dfef242c24 100644 --- a/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb +++ b/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb @@ -61,7 +61,7 @@ def read_event(event_id) def read(spec) stream = LegacyEvent.order(id: order(spec.direction)) - stream = stream.limit(spec.count) if spec.limit? + stream = stream.limit(spec.limit) if spec.limit? stream = stream.where(start_condition(spec)) unless spec.head? stream = stream.where(stream: spec.stream_name) unless spec.global_stream? @@ -80,7 +80,7 @@ def read(spec) private def total_limit(specification) - specification.limit? ? specification.count : Float::INFINITY + specification.limit end def start_condition(specification) diff --git a/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb b/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb index 6c5fb46607..f2c0a2a4ff 100644 --- a/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb +++ b/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb @@ -28,7 +28,7 @@ def read(spec) stream = EventInStream.preload(:event).where(stream: normalize_stream_name(spec)) stream = stream.order(position: order(spec.direction)) unless spec.global_stream? - stream = stream.limit(spec.count) if spec.limit? + stream = stream.limit(spec.limit) if spec.limit? stream = stream.where(start_condition(spec)) unless spec.head? stream = stream.order(id: order(spec.direction)) @@ -47,7 +47,7 @@ def read(spec) private def total_limit(specification) - specification.limit? ? specification.count : Float::INFINITY + specification.limit end def normalize_stream_name(specification) diff --git a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb index 28eace9d65..3f1904401a 100644 --- a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb +++ b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb @@ -47,7 +47,7 @@ def read(specification) end direction = specification.direction - limit = specification.count if specification.limit? + limit = specification.limit if specification.limit? if specification.last? direction = specification.forward? ? :backward : :forward end diff --git a/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb b/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb index 9ca8077b2f..328e90c0af 100644 --- a/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb +++ b/ruby_event_store/lib/ruby_event_store/in_memory_repository.rb @@ -39,7 +39,7 @@ def read(spec) events = spec.global_stream? ? global : stream_of(spec.stream_name) events = events.reverse if spec.backward? events = events.drop(index_of(events, spec.start) + 1) unless spec.head? - events = events[0...spec.count] if spec.limit? + events = events[0...spec.limit] if spec.limit? if spec.batched? batch_reader = ->(offset, limit) { events.drop(offset).take(limit) } BatchEnumerator.new(spec.batch_size, events.size, batch_reader).each diff --git a/ruby_event_store/lib/ruby_event_store/specification.rb b/ruby_event_store/lib/ruby_event_store/specification.rb index 1dd62a95d6..480adb4c94 100644 --- a/ruby_event_store/lib/ruby_event_store/specification.rb +++ b/ruby_event_store/lib/ruby_event_store/specification.rb @@ -9,6 +9,10 @@ def limit? !count.nil? end + def limit + count || Float::INFINITY + end + def global_stream? stream.global? end From 757b4ac85919a4f18b51d3a24288ecf740841fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Wed, 25 Jul 2018 17:22:27 +0200 Subject: [PATCH 19/27] The read last optimalization for ROM event repository fix when not reading whole stream --- .../lib/ruby_event_store/rom/repositories/events.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb index 3f1904401a..5454255a4a 100644 --- a/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb +++ b/ruby_event_store-rom/lib/ruby_event_store/rom/repositories/events.rb @@ -48,7 +48,7 @@ def read(specification) direction = specification.direction limit = specification.limit if specification.limit? - if specification.last? + if specification.last? && specification.head? direction = specification.forward? ? :backward : :forward end @@ -61,7 +61,15 @@ def read(specification) BatchEnumerator.new(specification.batch_size, limit || Float::INFINITY, reader).each else query = query_builder(query, limit: limit) - specification.first? || specification.last? ? query.first : query.each + if specification.head? + specification.first? || specification.last? ? query.first : query.each + else + if specification.last? + query.to_ary.last + else + specification.first? ? query.first : query.each + end + end end end From a677aa5ea276e0931aabc143a1d0da99aca81b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Wed, 25 Jul 2018 23:19:45 +0200 Subject: [PATCH 20/27] Kill a few more mutants --- ruby_event_store/spec/specification_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ruby_event_store/spec/specification_spec.rb b/ruby_event_store/spec/specification_spec.rb index 512997b415..96c32b6382 100644 --- a/ruby_event_store/spec/specification_spec.rb +++ b/ruby_event_store/spec/specification_spec.rb @@ -12,12 +12,16 @@ module RubyEventStore specify { expect(specification).to match_result({ stream_name: GLOBAL_STREAM }) } + specify { expect(specification).to match_result({ limit: Float::INFINITY }) } + specify { expect{specification.limit(nil) }.to raise_error(InvalidPageSize) } specify { expect{specification.limit(0)}.to raise_error(InvalidPageSize) } specify { expect(specification.limit(1)).to match_result({ count: 1 }) } + specify { expect(specification.limit(1)).to match_result({ limit: 1 }) } + specify { expect(specification.forward).to match_result({ direction: :forward }) } specify { expect(specification.backward).to match_result({ direction: :backward }) } From 951fb59591f9ebb9742fa049d8c5e19e90e8d16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Thu, 26 Jul 2018 23:11:13 +0200 Subject: [PATCH 21/27] Inline methods. --- .../lib/rails_event_store/rspec/publish.rb | 6 +----- .../legacy/event_repository.rb | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/rails_event_store-rspec/lib/rails_event_store/rspec/publish.rb b/rails_event_store-rspec/lib/rails_event_store/rspec/publish.rb index 7fc0526a58..00ad0216d0 100644 --- a/rails_event_store-rspec/lib/rails_event_store/rspec/publish.rb +++ b/rails_event_store-rspec/lib/rails_event_store/rspec/publish.rb @@ -17,7 +17,7 @@ def matches?(event_proc) raise_event_store_not_set unless @event_store spec = @event_store.read spec = spec.stream(@stream) if @stream - last_event_before_block = last_event(spec) + last_event_before_block = spec.last event_proc.call spec = spec.from(last_event_before_block.event_id) if last_event_before_block @published_events = spec.each.to_a @@ -78,10 +78,6 @@ def match_events? !@expected.empty? end - def last_event(spec) - spec.last - end - def raise_event_store_not_set raise SyntaxError, "You have to set the event store instance with `in`, e.g. `expect { ... }.to publish(an_event(MyEvent)).in(event_store)`" end diff --git a/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb b/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb index dfef242c24..8abdf57146 100644 --- a/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb +++ b/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb @@ -67,7 +67,7 @@ def read(spec) if spec.batched? batch_reader = ->(offset, limit) { stream.offset(offset).limit(limit).map(&method(:build_event_entity)) } - RubyEventStore::BatchEnumerator.new(spec.batch_size, total_limit(spec), batch_reader).each + RubyEventStore::BatchEnumerator.new(spec.batch_size, spec.limit, batch_reader).each elsif spec.first? build_event_entity(stream.first) elsif spec.last? @@ -79,10 +79,6 @@ def read(spec) private - def total_limit(specification) - specification.limit - end - def start_condition(specification) event_record = LegacyEvent.find_by!(event_id: specification.start) From a1179ef67339663b7427d2163759532c95461faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Thu, 26 Jul 2018 23:11:30 +0200 Subject: [PATCH 22/27] More precise expectations. --- ruby_event_store/spec/specification_spec.rb | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ruby_event_store/spec/specification_spec.rb b/ruby_event_store/spec/specification_spec.rb index 96c32b6382..480a28028b 100644 --- a/ruby_event_store/spec/specification_spec.rb +++ b/ruby_event_store/spec/specification_spec.rb @@ -371,21 +371,21 @@ module RubyEventStore specify do repository.append_to_stream([test_record], Stream.new("Dummy"), ExpectedVersion.none) - expect(specification.result.batched?).to be_falsey - expect(specification.result.first?).to be_falsey - expect(specification.result.last?).to be_falsey + expect(specification.result.batched?).to eq(false) + expect(specification.result.first?).to eq(false) + expect(specification.result.last?).to eq(false) - expect(specification.read_first.result.batched?).to be_falsey - expect(specification.read_first.result.first?).to be_truthy - expect(specification.read_first.result.last?).to be_falsey + expect(specification.read_first.result.batched?).to eq(false) + expect(specification.read_first.result.first?).to eq(true) + expect(specification.read_first.result.last?).to eq(false) - expect(specification.read_last.result.batched?).to be_falsey - expect(specification.read_last.result.first?).to be_falsey - expect(specification.read_last.result.last?).to be_truthy + expect(specification.read_last.result.batched?).to eq(false) + expect(specification.read_last.result.first?).to eq(false) + expect(specification.read_last.result.last?).to eq(true) - expect(specification.in_batches.result.batched?).to be_truthy - expect(specification.in_batches.result.first?).to be_falsey - expect(specification.in_batches.result.last?).to be_falsey + expect(specification.in_batches.result.batched?).to eq(true) + expect(specification.in_batches.result.first?).to eq(false) + expect(specification.in_batches.result.last?).to eq(false) end let(:repository) { InMemoryRepository.new } From bf4decb665f7105263878dc2ada40a4f77a703ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Thu, 26 Jul 2018 23:11:43 +0200 Subject: [PATCH 23/27] Missing docs added. --- ruby_event_store/lib/ruby_event_store/specification.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ruby_event_store/lib/ruby_event_store/specification.rb b/ruby_event_store/lib/ruby_event_store/specification.rb index 480adb4c94..997a0aa3c9 100644 --- a/ruby_event_store/lib/ruby_event_store/specification.rb +++ b/ruby_event_store/lib/ruby_event_store/specification.rb @@ -158,10 +158,18 @@ def in_batches(batch_size = DEFAULT_BATCH_SIZE) end alias :in_batches_of :in_batches + # Specifies that only first event should be read. + # {http://railseventstore.org/docs/read/ Find out more}. + # + # @return [Specification] def read_first Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = :first }) end + # Specifies that only last event should be read. + # {http://railseventstore.org/docs/read/ Find out more}. + # + # @return [Specification] def read_last Specification.new(repository, mapper, result.dup.tap { |r| r.read_as = :last }) end From bbac69c854815f7fe680ab2622f3c2de92d47a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Thu, 26 Jul 2018 23:26:19 +0200 Subject: [PATCH 24/27] Be like enumerable - nils over errors Method first & last returns nil when no event is found based on given read specification. No error is raised here. --- .../ruby_event_store/spec/event_repository_lint.rb | 13 +++++++++++++ .../lib/ruby_event_store/specification.rb | 10 ++++++---- ruby_event_store/spec/specification_spec.rb | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb b/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb index 7ab3c93f27..d40fd0654f 100644 --- a/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb +++ b/ruby_event_store/lib/ruby_event_store/spec/event_repository_lint.rb @@ -1102,6 +1102,9 @@ def read_all_streams_backward(repository, start, count) end specify do + expect(repository.read(specification.read_first.result)).to be_nil + expect(repository.read(specification.read_last.result)).to be_nil + events = Array.new(5) { SRecord.new } repository.append_to_stream( events, @@ -1109,6 +1112,9 @@ def read_all_streams_backward(repository, start, count) RubyEventStore::ExpectedVersion.any ) + expect(repository.read(specification.stream("Any").read_first.result)).to be_nil + expect(repository.read(specification.stream("Any").read_last.result)).to be_nil + expect(repository.read(specification.read_first.result)).to eq(events[0]) expect(repository.read(specification.read_last.result)).to eq(events[4]) @@ -1117,7 +1123,14 @@ def read_all_streams_backward(repository, start, count) expect(repository.read(specification.from(events[2].event_id).read_first.result)).to eq(events[3]) expect(repository.read(specification.from(events[2].event_id).read_last.result)).to eq(events[4]) + expect(repository.read(specification.from(events[2].event_id).backward.read_first.result)).to eq(events[1]) expect(repository.read(specification.from(events[2].event_id).backward.read_last.result)).to eq(events[0]) + + expect(repository.read(specification.from(events[4].event_id).read_first.result)).to be_nil + expect(repository.read(specification.from(events[4].event_id).read_last.result)).to be_nil + + expect(repository.read(specification.from(events[0].event_id).backward.read_first.result)).to be_nil + expect(repository.read(specification.from(events[0].event_id).backward.read_last.result)).to be_nil end end diff --git a/ruby_event_store/lib/ruby_event_store/specification.rb b/ruby_event_store/lib/ruby_event_store/specification.rb index 997a0aa3c9..f506d28774 100644 --- a/ruby_event_store/lib/ruby_event_store/specification.rb +++ b/ruby_event_store/lib/ruby_event_store/specification.rb @@ -178,18 +178,20 @@ def read_last # Returns the first event in specified collection of events. # {http://railseventstore.org/docs/read/ Find out more}. # - # @return [Event] + # @return [Event, nil] def first - mapper.serialized_record_to_event(repository.read(read_first.result)) + record = repository.read(read_first.result) + mapper.serialized_record_to_event(record) if record end # Executes the query based on the specification built up to this point. # Returns the last event in specified collection of events. # {http://railseventstore.org/docs/read/ Find out more}. # - # @return [Event] + # @return [Event, nil] def last - mapper.serialized_record_to_event(repository.read(read_last.result)) + record = repository.read(read_last.result) + mapper.serialized_record_to_event(record) if record end private diff --git a/ruby_event_store/spec/specification_spec.rb b/ruby_event_store/spec/specification_spec.rb index 480a28028b..6fc59fa881 100644 --- a/ruby_event_store/spec/specification_spec.rb +++ b/ruby_event_store/spec/specification_spec.rb @@ -357,15 +357,29 @@ module RubyEventStore end specify do + expect(specification.first).to be_nil + expect(specification.last).to be_nil + records = 5.times.map { test_record } repository.append_to_stream(records, Stream.new("Dummy"), ExpectedVersion.none) + expect(specification.stream("Another").first).to be_nil + expect(specification.stream("Another").last).to be_nil + expect(specification.first).to eq(TestEvent.new(event_id: records[0].event_id)) expect(specification.last).to eq(TestEvent.new(event_id: records[4].event_id)) + expect(specification.from(records[2].event_id).first).to eq(TestEvent.new(event_id: records[3].event_id)) expect(specification.from(records[2].event_id).last).to eq(TestEvent.new(event_id: records[4].event_id)) + expect(specification.from(records[2].event_id).backward.first).to eq(TestEvent.new(event_id: records[1].event_id)) expect(specification.from(records[2].event_id).backward.last).to eq(TestEvent.new(event_id: records[0].event_id)) + + expect(specification.from(records[4].event_id).first).to be_nil + expect(specification.from(records[4].event_id).last).to be_nil + + expect(specification.from(records[0].event_id).backward.first).to be_nil + expect(specification.from(records[0].event_id).backward.last).to be_nil end specify do From 15636c0b3de97e928c061f06060c73ede4960f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Thu, 26 Jul 2018 23:50:12 +0200 Subject: [PATCH 25/27] And actually implement nils over errors in event repositories. --- .../legacy/event_repository.rb | 6 ++++-- .../event_repository_reader.rb | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb b/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb index 8abdf57146..762d3a480c 100644 --- a/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb +++ b/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb @@ -69,9 +69,11 @@ def read(spec) batch_reader = ->(offset, limit) { stream.offset(offset).limit(limit).map(&method(:build_event_entity)) } RubyEventStore::BatchEnumerator.new(spec.batch_size, spec.limit, batch_reader).each elsif spec.first? - build_event_entity(stream.first) + record = stream.first + build_event_entity(record) if record elsif spec.last? - build_event_entity(stream.last) + record = stream.last + build_event_entity(record) if record else stream.map(&method(:build_event_entity)).each end diff --git a/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb b/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb index f2c0a2a4ff..8910f02df5 100644 --- a/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb +++ b/rails_event_store_active_record/lib/rails_event_store_active_record/event_repository_reader.rb @@ -36,9 +36,11 @@ def read(spec) batch_reader = ->(offset, limit) { stream.offset(offset).limit(limit).map(&method(:build_event_instance)) } RubyEventStore::BatchEnumerator.new(spec.batch_size, total_limit(spec), batch_reader).each elsif spec.first? - build_event_instance(stream.first) + record = stream.first + build_event_instance(record) if record elsif spec.last? - build_event_instance(stream.last) + record = stream.last + build_event_instance(record) if record else stream.map(&method(:build_event_instance)).each end From f489dd029c224782e060f50fd4f19cd19adbec95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Mon, 30 Jul 2018 15:20:50 +0200 Subject: [PATCH 26/27] No need to check for nils here - build in into build_event_entity --- .../legacy/event_repository.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb b/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb index 762d3a480c..8abdf57146 100644 --- a/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb +++ b/rails_event_store_active_record-legacy/lib/rails_event_store_active_record/legacy/event_repository.rb @@ -69,11 +69,9 @@ def read(spec) batch_reader = ->(offset, limit) { stream.offset(offset).limit(limit).map(&method(:build_event_entity)) } RubyEventStore::BatchEnumerator.new(spec.batch_size, spec.limit, batch_reader).each elsif spec.first? - record = stream.first - build_event_entity(record) if record + build_event_entity(stream.first) elsif spec.last? - record = stream.last - build_event_entity(record) if record + build_event_entity(stream.last) else stream.map(&method(:build_event_entity)).each end From 9d0b696baaf722c34a673c40585d4b1aa85a1d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miros=C5=82aw=20Prag=C5=82owski?= Date: Mon, 30 Jul 2018 17:06:44 +0200 Subject: [PATCH 27/27] Killing mutants --- .../spec/rails_event_store/rspec/publish_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rails_event_store-rspec/spec/rails_event_store/rspec/publish_spec.rb b/rails_event_store-rspec/spec/rails_event_store/rspec/publish_spec.rb index 0e09711f41..7daac231c1 100644 --- a/rails_event_store-rspec/spec/rails_event_store/rspec/publish_spec.rb +++ b/rails_event_store-rspec/spec/rails_event_store/rspec/publish_spec.rb @@ -71,6 +71,18 @@ def matcher(*expected) }.not_to matcher(matchers.an_event(FooEvent)).in(event_store).in_stream("Foo$1") end + specify do + event_store.publish(FooEvent.new) + event_store.publish(FooEvent.new) + event_store.publish(FooEvent.new) + expect { + event_store.publish(BarEvent.new) + }.to matcher(matchers.an_event(BarEvent)).in(event_store) + expect { + event_store.publish(BarEvent.new) + }.not_to matcher(matchers.an_event(FooEvent)).in(event_store) + end + specify do foo_event = FooEvent.new bar_event = BarEvent.new