sandthorn_driver_sequel 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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