sandthorn_driver_sequel 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/Guardfile +7 -0
  4. data/lib/sandthorn_driver_sequel/access/aggregate_access.rb +50 -0
  5. data/lib/sandthorn_driver_sequel/access/event_access.rb +81 -0
  6. data/lib/sandthorn_driver_sequel/access/snapshot_access.rb +87 -0
  7. data/lib/sandthorn_driver_sequel/access.rb +20 -0
  8. data/lib/sandthorn_driver_sequel/errors.rb +47 -5
  9. data/lib/sandthorn_driver_sequel/event_query.rb +90 -0
  10. data/lib/sandthorn_driver_sequel/event_store.rb +90 -153
  11. data/lib/sandthorn_driver_sequel/event_store_context.rb +1 -0
  12. data/lib/sandthorn_driver_sequel/migration.rb +9 -1
  13. data/lib/sandthorn_driver_sequel/old_event_store.rb +228 -0
  14. data/lib/sandthorn_driver_sequel/sequel_driver.rb +8 -25
  15. data/lib/sandthorn_driver_sequel/storage.rb +46 -0
  16. data/lib/sandthorn_driver_sequel/utilities/array.rb +13 -0
  17. data/lib/sandthorn_driver_sequel/utilities.rb +1 -0
  18. data/lib/sandthorn_driver_sequel/version.rb +1 -1
  19. data/lib/sandthorn_driver_sequel/wrappers/event_wrapper.rb +12 -0
  20. data/lib/sandthorn_driver_sequel/wrappers/snapshot_wrapper.rb +11 -0
  21. data/lib/sandthorn_driver_sequel/wrappers.rb +2 -0
  22. data/lib/sandthorn_driver_sequel.rb +5 -0
  23. data/sandthorn_driver_sequel.gemspec +2 -2
  24. data/spec/aggregate_access_spec.rb +97 -0
  25. data/spec/asking_for_aggregates_to_snapshot_spec.rb +7 -4
  26. data/spec/driver_interface_spec.rb +23 -40
  27. data/spec/event_access_spec.rb +96 -0
  28. data/spec/event_store_with_context_spec.rb +4 -4
  29. data/spec/get_events_spec.rb +20 -13
  30. data/spec/migration_specifying_domain_spec.rb +10 -10
  31. data/spec/saving_events_spec.rb +42 -39
  32. data/spec/saving_snapshot_spec.rb +7 -7
  33. data/spec/snapshot_access_spec.rb +119 -0
  34. data/spec/spec_helper.rb +0 -4
  35. data/spec/storage_spec.rb +66 -0
  36. metadata +39 -18
@@ -1,5 +1,6 @@
1
1
  module SandthornDriverSequel
2
2
  module EventStoreContext
3
+ attr_reader :context
3
4
  def events_table_name
4
5
  with_context_if_exists :events
5
6
  end
@@ -37,6 +37,15 @@ module SandthornDriverSequel
37
37
  was_migrated aggr_migration_0, db
38
38
  end
39
39
  end
40
+ aggr_migration_1 = "#{aggregates_table_name}-20141024"
41
+ unless has_been_migrated?(aggr_migration_1)
42
+ driver.execute do |db|
43
+ db.alter_table(aggregates_table_name) do
44
+ set_column_default :aggregate_version, 0
45
+ end
46
+ end
47
+ end
48
+
40
49
  end
41
50
  def events
42
51
  events_migration_0 = "#{events_table_name}-20130308"
@@ -95,7 +104,6 @@ module SandthornDriverSequel
95
104
  String :migration_name, null: false
96
105
  index [:migration_name], unique: true
97
106
  DateTime :timestamp, :null=>false
98
- index [:migration_name], unique: true
99
107
  end
100
108
  end
101
109
  end
@@ -0,0 +1,228 @@
1
+ require 'sandthorn_driver_sequel/sequel_driver'
2
+
3
+ module SandthornDriverSequel
4
+ class OldEventStore
5
+ include EventStoreContext
6
+ attr_reader :driver, :context, :url
7
+ def initialize url: nil, context: nil
8
+ @driver = SequelDriver.new url: url
9
+ @context = context
10
+ @url = url
11
+ end
12
+
13
+ def save_events aggregate_events, originating_aggregate_version, aggregate_id, class_name
14
+ current_aggregate_version = originating_aggregate_version
15
+ aggregate_type = class_name.to_s
16
+ driver.execute_in_transaction do |db|
17
+ if current_aggregate_version == 0
18
+ pk_id = register_new_aggregate(aggregate_id, aggregate_type, db)
19
+ else
20
+ current_aggregate = get_current_aggregate_from_aggregates_table(aggregate_id, aggregate_type, db)
21
+ check_initial_aggregate_version!(current_aggregate, current_aggregate_version)
22
+ pk_id = current_aggregate[:id]
23
+ end
24
+ timestamp = Time.now.utc
25
+ aggregate_events.each do |event|
26
+ current_aggregate_version += 1
27
+ check_event_aggregate_version!(event, class_name, current_aggregate_version)
28
+ insert_event(db, event, pk_id, timestamp)
29
+ end
30
+ db[aggregates_table_name].where(id: pk_id).update(aggregate_version: current_aggregate_version)
31
+ end
32
+ end
33
+
34
+ def insert_event(db, event, pk_id, timestamp)
35
+ to_insert = {
36
+ aggregate_table_id: pk_id,
37
+ aggregate_version: event[:aggregate_version],
38
+ event_name: event[:event_name],
39
+ event_data: event[:event_data],
40
+ timestamp: timestamp
41
+ }
42
+ db[events_table_name].insert(to_insert)
43
+ end
44
+
45
+ def check_event_aggregate_version!(event, aggregate_type, current_aggregate_version)
46
+ if event[:aggregate_version] != current_aggregate_version
47
+ raise SandthornDriverSequel::Errors::ConcurrencyError, event, aggregate_type, current_aggregate_version
48
+ end
49
+ end
50
+
51
+ def check_initial_aggregate_version!(aggregate, current_aggregate_version)
52
+ if aggregate[:aggregate_version] != current_aggregate_version
53
+ raise SandthornDriverSequel::Errors::WrongAggregateVersionError, aggregate, current_aggregate_version
54
+ end
55
+ end
56
+
57
+ def register_new_aggregate(aggregate_id, aggregate_type, db)
58
+ to_insert = {
59
+ aggregate_id: aggregate_id,
60
+ aggregate_type: aggregate_type,
61
+ aggregate_version: 0
62
+ }
63
+ pk_id = db[aggregates_table_name].insert(to_insert)
64
+ end
65
+
66
+ def save_snapshot aggregate_snapshot, aggregate_id, class_name
67
+ driver.execute_in_transaction do |db|
68
+ current_aggregate = get_current_aggregate_from_aggregates_table aggregate_id, class_name, db
69
+ pk_id = current_aggregate[:id]
70
+ current_snapshot = get_current_snapshot pk_id, db
71
+ aggregate_version = aggregate_snapshot[:aggregate_version]
72
+ return if snapshot_fresh?(current_snapshot, aggregate_version)
73
+ check_snapshot_version!(current_aggregate, aggregate_version)
74
+ if current_snapshot.nil?
75
+ to_insert = {aggregate_version: aggregate_version, snapshot_data: aggregate_snapshot[:event_data], aggregate_table_id: pk_id }
76
+ db[snapshots_table_name].insert(to_insert)
77
+ else
78
+ to_update = {aggregate_version: aggregate_version, snapshot_data: aggregate_snapshot[:event_data] }
79
+ db[snapshots_table_name].where(aggregate_table_id: pk_id).update(to_update)
80
+ end
81
+ end
82
+ end
83
+
84
+ def snapshot_fresh?(current_snapshot, aggregate_version)
85
+ !current_snapshot.nil? && current_snapshot[:aggregate_version] == aggregate_version
86
+ end
87
+
88
+ def check_snapshot_version!(aggregate, aggregate_version)
89
+ if aggregate[:aggregate_version] < aggregate_version
90
+ raise SandthornDriverSequel::Errors::WrongSnapshotVersionError, aggregate, version
91
+ end
92
+ end
93
+
94
+ def get_aggregate_events aggregate_id, *class_name
95
+ #aggregate_type = class_name.to_s unless class_name.nil?
96
+ return aggregate_events aggregate_id: aggregate_id
97
+ end
98
+
99
+ def get_aggregate aggregate_id, *class_name
100
+ snapshot = get_snapshot aggregate_id, class_name
101
+ after_aggregate_version = 0
102
+ after_aggregate_version = snapshot[:aggregate_version] unless snapshot.nil?
103
+ events = aggregate_events after_aggregate_version: after_aggregate_version, aggregate_id: aggregate_id
104
+ unless snapshot.nil?
105
+ snap_event = snapshot
106
+ snap_event[:event_name] = "aggregate_set_from_snapshot"
107
+ events = events.unshift(snap_event)
108
+ end
109
+ events
110
+ end
111
+ def get_aggregate_list_by_typename class_name
112
+ aggregate_type = class_name.to_s
113
+ driver.execute do |db|
114
+ db[aggregates_table_name].where(aggregate_type: aggregate_type).select(:aggregate_id).map { |e| e[:aggregate_id] }
115
+ end
116
+ end
117
+
118
+ def get_all_typenames
119
+ driver.execute do |db|
120
+ db[aggregates_table_name].select(:aggregate_type).distinct.order(:aggregate_type).map{|e| e[:aggregate_type]}
121
+ end
122
+ end
123
+
124
+ def get_snapshot aggregate_id, *class_name
125
+ aggregate_type = class_name.first.to_s
126
+ driver.execute do |db|
127
+ current_aggregate = get_current_aggregate_from_aggregates_table aggregate_id, aggregate_type, db
128
+ snap = get_current_snapshot current_aggregate[:id], db
129
+ return nil if snap.nil?
130
+ return {aggregate_version: snap[:aggregate_version], event_data: snap[:snapshot_data]}
131
+ end
132
+ end
133
+
134
+ def get_new_events_after_event_id_matching_classname event_id, class_name, args = {}
135
+ take = args.fetch(:take, 0)
136
+ aggregate_type = class_name.to_s
137
+ driver.execute do |db|
138
+ query = db[events_table_name].join(aggregates_table_name, id: :aggregate_table_id, aggregate_type: aggregate_type)
139
+ query = query.where{sequence_number > event_id}
140
+ rel = "#{events_table_name}__aggregate_version".to_sym
141
+ query = query.select(:aggregate_type, rel, :aggregate_id, :sequence_number, :event_name, :event_data, :timestamp)
142
+ query = query.limit(take) if take > 0
143
+ return query.order(:sequence_number).all
144
+ end
145
+ end
146
+ def get_events aggregate_types: [], take: 0, after_sequence_number: 0, include_events: [], exclude_events: []
147
+ include_events = include_events.map { |e| e.to_s }
148
+ exclude_events = exclude_events.map { |e| e.to_s }
149
+ aggregate_types = aggregate_types.map { |e| e.to_s }
150
+ driver.execute do |db|
151
+ if aggregate_types.empty?
152
+ query = db[events_table_name].join(aggregates_table_name, id: :aggregate_table_id)
153
+ else
154
+ query = db[events_table_name].join(aggregates_table_name, id: :aggregate_table_id, aggregate_type: aggregate_types)
155
+ end
156
+ query = query.where{sequence_number > after_sequence_number}
157
+ unless include_events.empty?
158
+ query = query.where(event_name: include_events)
159
+ end
160
+ unless exclude_events.empty?
161
+ query = query.exclude(event_name: exclude_events)
162
+ end
163
+ rel = "#{events_table_name}__aggregate_version".to_sym
164
+ query = query.select(:aggregate_type, rel, :aggregate_id, :sequence_number, :event_name, :event_data, :timestamp)
165
+ query = query.limit(take) if take > 0
166
+ return query.order(:sequence_number).all
167
+ end
168
+ end
169
+ def obsolete_snapshots aggregate_types: [], max_event_distance: 100
170
+ driver.execute do |db|
171
+ rel = "#{snapshots_table_name}__aggregate_version".to_sym
172
+ aggr_rel = "#{aggregates_table_name}__aggregate_version".to_sym
173
+ query_select = eval("lambda{(#{aggr_rel} - coalesce(#{rel},0)).as(distance)}")
174
+ query = db[aggregates_table_name].left_outer_join(snapshots_table_name, aggregate_table_id: :id)
175
+ query = query.select &query_select
176
+ query = query.select_append(:aggregate_id, :aggregate_type)
177
+ query_where = eval("lambda{(#{aggr_rel} - coalesce(#{rel},0)) > max_event_distance}")
178
+ query = query.where &query_where
179
+ unless class_names.empty?
180
+ class_names.map! {|c|c.to_s}
181
+ query = query.where(aggregate_type: class_names)
182
+ end
183
+ query.all
184
+ end
185
+ end
186
+ private
187
+
188
+ def aggregate_events after_aggregate_version: 0, aggregate_id: nil
189
+
190
+ rel = "#{events_table_name}__aggregate_version".to_sym
191
+ where_proc = eval("lambda{ #{rel} > after_aggregate_version }")
192
+ driver.execute do |db|
193
+ query = db[events_table_name].join(aggregates_table_name, id: :aggregate_table_id, aggregate_id: aggregate_id)
194
+ query = query.where &where_proc
195
+ result = query.select(rel, :aggregate_id, :sequence_number, :event_name, :event_data, :timestamp).order(:sequence_number).all
196
+ end
197
+
198
+ # result = nil
199
+ # Benchmark.bm do |x|
200
+ # x.report("find") {
201
+ # rel = "#{events_table_name}__aggregate_version".to_sym
202
+ # where_proc = eval("lambda{ #{rel} > after_aggregate_version }")
203
+ # driver.execute do |db|
204
+ # query = db[events_table_name].join(aggregates_table_name, id: :aggregate_table_id, aggregate_id: aggregate_id)
205
+ # query = query.where &where_proc
206
+ # result = query.select(rel, :aggregate_id, :sequence_number, :event_name, :event_data, :timestamp).order(:sequence_number).all
207
+ # end
208
+ # }
209
+
210
+ # end
211
+ # result
212
+ end
213
+ def get_current_aggregate_from_aggregates_table aggregate_id, aggregate_type, db
214
+ aggregate_type = aggregate_type.to_s
215
+ current_aggregate = db[aggregates_table_name].where(aggregate_id: aggregate_id)
216
+ if current_aggregate.empty?
217
+ error_message = "#{aggregate_type} with id #{aggregate_id} was not found in the eventstore."
218
+ raise SandthornDriverSequel::Errors::NoAggregateError.new(error_message)
219
+ end
220
+ current_aggregate.first
221
+ end
222
+ def get_current_snapshot aggregate_table_id, db
223
+ snap = db[snapshots_table_name].where(aggregate_table_id: aggregate_table_id)
224
+ return nil if snap.empty?
225
+ snap.first
226
+ end
227
+ end
228
+ end
@@ -2,39 +2,22 @@ require 'sequel'
2
2
 
3
3
  module SandthornDriverSequel
4
4
  class SequelDriver
5
+
5
6
  def initialize args = {}
6
7
  @url = args.fetch(:url)
7
8
  Sequel.default_timezone = :utc
8
9
  @db = Sequel.connect(@url)
9
10
  end
10
- def execute &block
11
- return block.call @db
11
+
12
+ def execute
13
+ yield @db
12
14
  end
15
+
13
16
  def execute_in_transaction &block
14
- @db.transaction {|tr|
15
- return block.call @db
16
- }
17
+ @db.transaction do
18
+ block.call(@db)
19
+ end
17
20
  end
18
21
 
19
22
  end
20
23
  end
21
-
22
-
23
- # module SandthornDriverSequel
24
- # class SequelDriver
25
- # def initialize args = {}
26
- # @url = args.fetch(:url)
27
- # Sequel.default_timezone = :utc
28
- # end
29
- # def execute &block
30
- # Sequel.connect(@url) { |db| return block.call db}
31
- # end
32
- # def execute_in_transaction &block
33
- # Sequel.connect(@url) do |db|
34
- # db.transaction do
35
- # return block.call db
36
- # end
37
- # end
38
- # end
39
- # end
40
- # end
@@ -0,0 +1,46 @@
1
+ module SandthornDriverSequel
2
+ class Storage
3
+ # = Storage
4
+ # Abstracts access to contextualized database tables.
5
+ #
6
+ # == Rationale
7
+ # Provide object-oriented access to the different tables to other objects.
8
+ # Make it unnecessary for them to know about the current context.
9
+ include EventStoreContext
10
+
11
+ attr_reader :db
12
+
13
+ def initialize(db, context)
14
+ @db = db
15
+ @context = context
16
+ end
17
+
18
+ # Returns a Sequel::Model for accessing aggregates
19
+ def aggregates
20
+ Class.new(Sequel::Model(aggregates_table))
21
+ end
22
+
23
+ # Returns a Sequel::Model for accessing events
24
+ def events
25
+ Class.new(Sequel::Model(events_table))
26
+ end
27
+
28
+ # Returns a Sequel::Model for accessing snapshots
29
+ def snapshots
30
+ Class.new(Sequel::Model(snapshots_table))
31
+ end
32
+
33
+ def aggregates_table
34
+ db[aggregates_table_name]
35
+ end
36
+
37
+ def events_table
38
+ db[events_table_name]
39
+ end
40
+
41
+ def snapshots_table
42
+ db[snapshots_table_name]
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ module SandthornDriverSequel
2
+ module Utilities
3
+ def self.array_wrap(object)
4
+ if object.nil?
5
+ []
6
+ elsif object.respond_to?(:to_ary)
7
+ object.to_ary || [object]
8
+ else
9
+ [object]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1 @@
1
+ require "sandthorn_driver_sequel/utilities/array"
@@ -1,3 +1,3 @@
1
1
  module SandthornDriverSequel
2
- VERSION = "1.1.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -0,0 +1,12 @@
1
+ require 'delegate'
2
+ module SandthornDriverSequel
3
+ class EventWrapper < SimpleDelegator
4
+
5
+ [:aggregate_version, :event_name, :event_data, :timestamp, :aggregate_table_id].each do |attribute|
6
+ define_method(attribute) do
7
+ fetch(attribute)
8
+ end
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module SandthornDriverSequel
2
+ class SnapshotWrapper < SimpleDelegator
3
+ def aggregate_version
4
+ self[:aggregate_version]
5
+ end
6
+
7
+ def data
8
+ self[:event_data]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,2 @@
1
+ require "sandthorn_driver_sequel/wrappers/event_wrapper"
2
+ require "sandthorn_driver_sequel/wrappers/snapshot_wrapper"
@@ -1,5 +1,10 @@
1
1
  require "sandthorn_driver_sequel/version"
2
+ require "sandthorn_driver_sequel/utilities"
3
+ require "sandthorn_driver_sequel/wrappers"
4
+ require "sandthorn_driver_sequel/event_query"
2
5
  require "sandthorn_driver_sequel/event_store_context"
6
+ require "sandthorn_driver_sequel/access"
7
+ require "sandthorn_driver_sequel/storage"
3
8
  require 'sandthorn_driver_sequel/event_store'
4
9
  require 'sandthorn_driver_sequel/errors'
5
10
  require 'sandthorn_driver_sequel/migration'
@@ -22,7 +22,6 @@ Gem::Specification.new do |spec|
22
22
  spec.add_development_dependency "rake"
23
23
 
24
24
  spec.add_development_dependency "rspec"
25
- spec.add_development_dependency "coveralls"
26
25
  spec.add_development_dependency "gem-release"
27
26
  spec.add_development_dependency "sqlite3"
28
27
  spec.add_development_dependency "pry"
@@ -33,7 +32,8 @@ Gem::Specification.new do |spec|
33
32
  spec.add_development_dependency "ruby-beautify"
34
33
  spec.add_development_dependency "msgpack"
35
34
  spec.add_development_dependency "snappy"
35
+ spec.add_development_dependency "guard-rspec"
36
36
 
37
- spec.add_runtime_dependency "sequel"
37
+ spec.add_runtime_dependency "sequel", "~> 4.17"
38
38
  spec.add_runtime_dependency "pg"
39
39
  end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ module SandthornDriverSequel
4
+ describe AggregateAccess do
5
+ include EventStoreContext
6
+ let(:context) { :test }
7
+ let(:db) { Sequel.connect(event_store_url)}
8
+ let(:aggregate_id) { generate_uuid }
9
+ let(:storage) { Storage.new(db, :test) }
10
+ let(:access) { AggregateAccess.new(storage) }
11
+
12
+ before { prepare_for_test }
13
+
14
+ describe "#find" do
15
+ it "finds by table id" do
16
+ aggregate = access.register_aggregate(aggregate_id, "boo")
17
+ aggregate = access.find(aggregate.id)
18
+ expect(aggregate.aggregate_id).to eq(aggregate_id)
19
+ expect(aggregate.aggregate_type).to eq("boo")
20
+ end
21
+
22
+ it "doesn't find by table id" do
23
+ access.register_aggregate(aggregate_id, "foo")
24
+ max_id = db[aggregates_table_name].max(:id)
25
+ expect(access.find(max_id + 1)).to be_nil
26
+ end
27
+ end
28
+
29
+ describe "#find_by_aggregate_id" do
30
+ context "when the aggregate is registered" do
31
+ it "returns the aggregate" do
32
+ access.register_aggregate(aggregate_id, "bar")
33
+ aggregate = access.find_by_aggregate_id(aggregate_id)
34
+ expect(aggregate.aggregate_id).to eq(aggregate_id)
35
+ end
36
+ end
37
+
38
+ context "when the aggregate isn't registered" do
39
+ it "returns nil" do
40
+ expect(access.find_by_aggregate_id(aggregate_id)).to be_nil
41
+ end
42
+ end
43
+ end
44
+
45
+ describe "#find_or_register" do
46
+ context "when the aggregate is registered" do
47
+ it "returns the aggregate" do
48
+ access.register_aggregate(aggregate_id, "baz")
49
+ aggregate = access.find_or_register(aggregate_id, "qux")
50
+ expect(aggregate.aggregate_id).to eq(aggregate_id)
51
+ expect(aggregate.aggregate_type).to eq("baz")
52
+ end
53
+ end
54
+ end
55
+
56
+ describe "#register_aggregate" do
57
+ it "returns the aggregate" do
58
+ aggregate = access.register_aggregate(aggregate_id, "bar")
59
+ expect(aggregate.aggregate_id).to eq(aggregate_id)
60
+ expect(aggregate.aggregate_type).to eq("bar")
61
+ expect(aggregate.id).to_not be_nil
62
+ end
63
+ end
64
+
65
+ describe "#aggregate_types" do
66
+ it "returns all aggregate types in the event store" do
67
+ types = ["foo", "bar", "qux"]
68
+ types.each do |type|
69
+ access.register_aggregate(generate_uuid, type)
70
+ end
71
+ expect(access.aggregate_types).to eq(types.sort)
72
+ end
73
+ end
74
+
75
+ describe "#aggregate_ids" do
76
+ context "when given no argument" do
77
+ it "returns all aggregate ids" do
78
+ aggregate_ids = 3.times.map { generate_uuid }
79
+ aggregate_ids.each { |id| access.register_aggregate(id, "foo") }
80
+ expect(access.aggregate_ids).to eq(aggregate_ids)
81
+ end
82
+ end
83
+ context "when given an aggregate type" do
84
+ it "returns only aggregates of that type" do
85
+ foo_agg_id, bar_agg_id = generate_uuid, generate_uuid
86
+ access.register_aggregate(foo_agg_id, "foo")
87
+ access.register_aggregate(bar_agg_id, "bar")
88
+ expect(access.aggregate_ids(aggregate_type: "foo")).to eq([foo_agg_id])
89
+ end
90
+ end
91
+ end
92
+
93
+ def generate_uuid
94
+ SecureRandom.uuid
95
+ end
96
+ end
97
+ end