pg_eventstore 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +1 -0
  4. data/db/migrations/10_create_subscription_commands.sql +15 -0
  5. data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
  6. data/db/migrations/12_improve_events_indexes.sql +1 -0
  7. data/db/migrations/9_create_subscriptions.sql +46 -0
  8. data/docs/configuration.md +42 -21
  9. data/docs/subscriptions.md +170 -0
  10. data/lib/pg_eventstore/callbacks.rb +122 -0
  11. data/lib/pg_eventstore/client.rb +2 -2
  12. data/lib/pg_eventstore/config.rb +35 -3
  13. data/lib/pg_eventstore/errors.rb +63 -0
  14. data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +11 -14
  15. data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
  16. data/lib/pg_eventstore/extensions/options_extension.rb +25 -23
  17. data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
  18. data/lib/pg_eventstore/queries/event_queries.rb +5 -26
  19. data/lib/pg_eventstore/queries/event_type_queries.rb +13 -0
  20. data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
  21. data/lib/pg_eventstore/queries/subscription_queries.rb +160 -0
  22. data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
  23. data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
  24. data/lib/pg_eventstore/queries.rb +6 -0
  25. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +14 -2
  26. data/lib/pg_eventstore/sql_builder.rb +54 -10
  27. data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
  28. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
  29. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
  30. data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
  31. data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
  32. data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
  33. data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
  34. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
  35. data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
  36. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
  37. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
  38. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
  39. data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
  40. data/lib/pg_eventstore/tasks/setup.rake +1 -1
  41. data/lib/pg_eventstore/utils.rb +66 -0
  42. data/lib/pg_eventstore/version.rb +1 -1
  43. data/lib/pg_eventstore.rb +19 -1
  44. metadata +30 -4
@@ -2,7 +2,7 @@
2
2
 
3
3
  module PgEventstore
4
4
  # @!visibility private
5
- class PgResultDeserializer
5
+ class EventDeserializer
6
6
  attr_reader :middlewares, :event_class_resolver
7
7
 
8
8
  # @param middlewares [Array<Object<#deserialize, #serialize>>]
@@ -14,29 +14,21 @@ module PgEventstore
14
14
 
15
15
  # @param pg_result [PG::Result]
16
16
  # @return [Array<PgEventstore::Event>]
17
- def deserialize(pg_result)
18
- pg_result.map(&method(:_deserialize))
17
+ def deserialize_pg_result(pg_result)
18
+ pg_result.map(&method(:deserialize))
19
19
  end
20
- alias deserialize_many deserialize
21
20
 
22
21
  # @param pg_result [PG::Result]
23
22
  # @return [PgEventstore::Event, nil]
24
- def deserialize_one(pg_result)
23
+ def deserialize_one_pg_result(pg_result)
25
24
  return if pg_result.ntuples.zero?
26
25
 
27
- _deserialize(pg_result.first)
26
+ deserialize(pg_result.first)
28
27
  end
29
28
 
30
- # @return [PgEventstore::PgResultDeserializer]
31
- def without_middlewares
32
- self.class.new([], event_class_resolver)
33
- end
34
-
35
- private
36
-
37
29
  # @param attrs [Hash]
38
30
  # @return [PgEventstore::Event]
39
- def _deserialize(attrs)
31
+ def deserialize(attrs)
40
32
  event = event_class_resolver.call(attrs['type']).new(**attrs.transform_keys(&:to_sym))
41
33
  middlewares.each do |middleware|
42
34
  middleware.deserialize(event)
@@ -44,5 +36,10 @@ module PgEventstore
44
36
  event.stream = PgEventstore::Stream.new(**attrs['stream'].transform_keys(&:to_sym)) if attrs.key?('stream')
45
37
  event
46
38
  end
39
+
40
+ # @return [PgEventstore::EventDeserializer]
41
+ def without_middlewares
42
+ self.class.new([], event_class_resolver)
43
+ end
47
44
  end
48
45
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Extensions
5
+ # Integrates PgEventstore::Calbacks into your object. Example usage:
6
+ # class MyAwesomeClass
7
+ # include CallbacksExtension
8
+ # end
9
+ # Now you have {#define_callback} public method to define callbacks outside your class' object, and you can use
10
+ # _#callbacks_ private method to run callbacks inside your class' object. You can also use _.has_callbacks_
11
+ # public class method to wrap the desired method into {Callbacks#run_callbacks}. Example:
12
+ # class MyAwesomeClass
13
+ # include PgEventstore::Extensions::CallbacksExtension
14
+ #
15
+ # def initialize(foo)
16
+ # @foo = foo
17
+ # end
18
+ #
19
+ # def do_something
20
+ # puts "I did something useful: #{@foo.inspect}!"
21
+ # end
22
+ # has_callbacks :something_happened, :do_something
23
+ #
24
+ # def do_something_else
25
+ # callbacks.run_callbacks(:something_else_happened) do
26
+ # puts "I did something else!"
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # obj = MyAwesomeClass.new(:foo)
32
+ # obj.define_callback(
33
+ # :something_happened, :before, proc { puts "In before callback of :something_happened." }
34
+ # )
35
+ # obj.define_callback(
36
+ # :something_else_happened, :before, proc { puts "In before callback of :something_else_happened." }
37
+ # )
38
+ # obj.do_something
39
+ # obj.do_something_else
40
+ # Outputs:
41
+ # In before callback of :something_happened.
42
+ # I did something useful: :foo!
43
+ # In before callback of :something_else_happened.
44
+ # I did something else!
45
+ module CallbacksExtension
46
+ def self.included(klass)
47
+ klass.extend(ClassMethods)
48
+ klass.prepend(InitCallbacks)
49
+ klass.class_eval do
50
+ attr_reader :callbacks
51
+ private :callbacks
52
+ end
53
+ end
54
+
55
+ def define_callback(...)
56
+ callbacks.define_callback(...)
57
+ end
58
+
59
+ # @!visibility private
60
+ module InitCallbacks
61
+ def initialize(...)
62
+ @callbacks = Callbacks.new
63
+ super
64
+ end
65
+ end
66
+
67
+ # @!visibility private
68
+ module ClassMethods
69
+ # Wraps method with Callbacks#run_callbacks. This allows you to define callbacks by the given action
70
+ # @param action [String, Symbol]
71
+ # @param method_name [Symbol]
72
+ # @return [void]
73
+ def has_callbacks(action, method_name)
74
+ visibility_method = visibility_method(method_name)
75
+ m = Module.new do
76
+ define_method(method_name) do |*args, **kwargs, &blk|
77
+ callbacks.run_callbacks(action) { super(*args, **kwargs, &blk) }
78
+ end
79
+ send visibility_method, method_name
80
+ end
81
+ prepend m
82
+ end
83
+
84
+ private
85
+
86
+ def visibility_method(method_name)
87
+ return :public if public_method_defined?(method_name)
88
+ return :protected if protected_method_defined?(method_name)
89
+
90
+ :private
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -7,38 +7,40 @@ module PgEventstore
7
7
  # A very simple extension that implements a DSL for adding attr_accessors with default values,
8
8
  # and assigning their values during object initialization.
9
9
  # Example. Let's say you frequently do something like this:
10
- # ```ruby
11
- # class SomeClass
12
- # attr_accessor :attr1, :attr2, :attr3, :attr4
10
+ # class SomeClass
11
+ # attr_accessor :attr1, :attr2, :attr3, :attr4
13
12
  #
14
- # def initialize(opts = {})
15
- # @attr1 = opts[:attr1] || 'Attr 1 value'
16
- # @attr2 = opts[:attr2] || 'Attr 2 value'
17
- # @attr3 = opts[:attr3] || do_some_calc
18
- # @attr4 = opts[:attr4]
19
- # end
13
+ # def initialize(opts = {})
14
+ # @attr1 = opts[:attr1] || 'Attr 1 value'
15
+ # @attr2 = opts[:attr2] || 'Attr 2 value'
16
+ # @attr3 = opts[:attr3] || do_some_calc
17
+ # @attr4 = opts[:attr4]
18
+ # end
20
19
  #
21
- # def do_some_calc
20
+ # def do_some_calc
21
+ # "Some calculations"
22
+ # end
22
23
  # end
23
- # end
24
24
  #
25
- # SomeClass.new(attr1: 'hihi', attr4: 'byebye')
26
- # ```
25
+ # SomeClass.new(attr1: 'hihi', attr4: 'byebye')
27
26
  #
28
27
  # You can replace the code above using the OptionsExtension:
29
- # ```ruby
30
- # class SomeClass
31
- # include PgEventstore::Extensions::OptionsExtension
28
+ # class SomeClass
29
+ # include PgEventstore::Extensions::OptionsExtension
30
+ #
31
+ # option(:attr1) { 'Attr 1 value' }
32
+ # option(:attr2) { 'Attr 2 value' }
33
+ # option(:attr3) { do_some_calc }
34
+ # option(:attr4)
32
35
  #
33
- # option(:attr1) { 'Attr 1 value' }
34
- # option(:attr2) { 'Attr 2 value' }
35
- # option(:attr3) { do_some_calc }
36
- # option(:attr4)
37
- # end
36
+ # def do_some_calc
37
+ # "Some calculations"
38
+ # end
39
+ # end
38
40
  #
39
- # SomeClass.new(attr1: 'hihi', attr4: 'byebye')
40
- # ```
41
+ # SomeClass.new(attr1: 'hihi', attr4: 'byebye')
41
42
  module OptionsExtension
43
+ # @!visibility private
42
44
  module ClassMethods
43
45
  # @param opt_name [Symbol] option name
44
46
  # @param blk [Proc] provide define value using block. It will be later evaluated in the
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Extensions
5
+ # Extension that implements creating of a subclass of the class it is used in. The point of creating a subclass is
6
+ # to bound it to the specific connection. This way the specific connection will be available within tha class and
7
+ # all its instances without affecting on the original class.
8
+ # @!visibility private
9
+ module UsingConnectionExtension
10
+ def self.included(klass)
11
+ klass.extend(ClassMethods)
12
+ end
13
+
14
+ module ClassMethods
15
+ def connection
16
+ raise("No connection was set. Are you trying to manipulate #{name} outside of its lifecycle?")
17
+ end
18
+
19
+ # @param config_name [Symbol]
20
+ # @return [Class<PgEventstore::Subscription>]
21
+ def using_connection(config_name)
22
+ original_class = self
23
+ Class.new(original_class).tap do |klass|
24
+ klass.define_singleton_method(:connection) { PgEventstore.connection(config_name) }
25
+ klass.class_eval do
26
+ [:to_s, :inspect, :name].each do |m|
27
+ define_singleton_method(m, &original_class.method(m))
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pg_eventstore/query_builders/events_filtering_query'
4
-
5
3
  module PgEventstore
6
4
  # @!visibility private
7
5
  class EventQueries
@@ -10,7 +8,7 @@ module PgEventstore
10
8
 
11
9
  # @param connection [PgEventstore::Connection]
12
10
  # @param serializer [PgEventstore::EventSerializer]
13
- # @param deserializer [PgEventstore::PgResultDeserializer]
11
+ # @param deserializer [PgEventstore::EventDeserializer]
14
12
  def initialize(connection, serializer, deserializer)
15
13
  @connection = connection
16
14
  @serializer = serializer
@@ -22,12 +20,12 @@ module PgEventstore
22
20
  # @param options [Hash]
23
21
  # @return [Array<PgEventstore::Event>]
24
22
  def stream_events(stream, options)
25
- options = include_event_types_ids(options)
23
+ options = event_type_queries.include_event_types_ids(options)
26
24
  exec_params = events_filtering(stream, options).to_exec_params
27
25
  pg_result = connection.with do |conn|
28
26
  conn.exec_params(*exec_params)
29
27
  end
30
- deserializer.deserialize_many(pg_result)
28
+ deserializer.deserialize_pg_result(pg_result)
31
29
  end
32
30
 
33
31
  # @param stream [PgEventstore::Stream] persisted stream
@@ -42,14 +40,14 @@ module PgEventstore
42
40
 
43
41
  sql = <<~SQL
44
42
  INSERT INTO events (#{attributes.keys.join(', ')})
45
- VALUES (#{positional_vars(attributes.values)})
43
+ VALUES (#{Utils.positional_vars(attributes.values)})
46
44
  RETURNING *, $#{attributes.values.size + 1} as type
47
45
  SQL
48
46
 
49
47
  pg_result = connection.with do |conn|
50
48
  conn.exec_params(sql, [*attributes.values, event.type])
51
49
  end
52
- deserializer.without_middlewares.deserialize_one(pg_result).tap do |persisted_event|
50
+ deserializer.without_middlewares.deserialize_one_pg_result(pg_result).tap do |persisted_event|
53
51
  persisted_event.stream = stream
54
52
  end
55
53
  end
@@ -66,25 +64,6 @@ module PgEventstore
66
64
  QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options, offset: offset)
67
65
  end
68
66
 
69
- # Replaces filter by event type strings with filter by event type ids
70
- # @param options [Hash]
71
- # @return [Hash]
72
- def include_event_types_ids(options)
73
- options in { filter: { event_types: Array => event_types } }
74
- return options unless event_types
75
-
76
- filter = options[:filter].dup
77
- filter[:event_type_ids] = event_type_queries.find_event_types(event_types).uniq
78
- filter.delete(:event_types)
79
- options.merge(filter: filter)
80
- end
81
-
82
- # @param array [Array]
83
- # @return [String] positional variables, based on array size. Example: "$1, $2, $3"
84
- def positional_vars(array)
85
- array.size.times.map { |t| "$#{t + 1}" }.join(', ')
86
- end
87
-
88
67
  # @return [PgEventstore::EventTypeQueries]
89
68
  def event_type_queries
90
69
  EventTypeQueries.new(connection)
@@ -46,5 +46,18 @@ module PgEventstore
46
46
  SQL
47
47
  end.to_a.map { |attrs| attrs['id'] }
48
48
  end
49
+
50
+ # Replaces filter by event type strings with filter by event type ids
51
+ # @param options [Hash]
52
+ # @return [Hash]
53
+ def include_event_types_ids(options)
54
+ options in { filter: { event_types: Array => event_types } }
55
+ return options unless event_types
56
+
57
+ options = Utils.deep_dup(options)
58
+ options[:filter][:event_type_ids] = find_event_types(event_types).uniq
59
+ options[:filter].delete(:event_types)
60
+ options
61
+ end
49
62
  end
50
63
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ class SubscriptionCommandQueries
6
+ attr_reader :connection
7
+ private :connection
8
+
9
+ # @param connection [PgEventstore::Connection]
10
+ def initialize(connection)
11
+ @connection = connection
12
+ end
13
+
14
+ # @param subscription_id [Integer]
15
+ # @param command_name [String]
16
+ # @return [Hash, nil]
17
+ def find_by(subscription_id:, command_name:)
18
+ sql_builder =
19
+ SQLBuilder.new.
20
+ select('*').
21
+ from('subscription_commands').
22
+ where('subscription_id = ? AND name = ?', subscription_id, command_name)
23
+ pg_result = connection.with do |conn|
24
+ conn.exec_params(*sql_builder.to_exec_params)
25
+ end
26
+ return if pg_result.ntuples.zero?
27
+
28
+ deserialize(pg_result.to_a.first)
29
+ end
30
+
31
+ # @param subscription_id [Integer]
32
+ # @param command_name [String]
33
+ # @return [Hash]
34
+ def create_by(subscription_id:, command_name:)
35
+ sql = <<~SQL
36
+ INSERT INTO subscription_commands (name, subscription_id)
37
+ VALUES ($1, $2)
38
+ RETURNING *
39
+ SQL
40
+ pg_result = connection.with do |conn|
41
+ conn.exec_params(sql, [command_name, subscription_id])
42
+ end
43
+ deserialize(pg_result.to_a.first)
44
+ end
45
+
46
+ # @param subscription_ids [Array<Integer>]
47
+ # @return [Array<Hash>]
48
+ def find_commands(subscription_ids)
49
+ return [] if subscription_ids.empty?
50
+
51
+ sql = subscription_ids.size.times.map do
52
+ "?"
53
+ end.join(", ")
54
+ sql_builder =
55
+ SQLBuilder.new.select('*').
56
+ from('subscription_commands').
57
+ where("subscription_id IN (#{sql})", *subscription_ids).
58
+ order('id ASC')
59
+ pg_result = connection.with do |conn|
60
+ conn.exec_params(*sql_builder.to_exec_params)
61
+ end
62
+ pg_result.to_a.map(&method(:deserialize))
63
+ end
64
+
65
+ # @param id [Integer]
66
+ # @return [void]
67
+ def delete(id)
68
+ connection.with do |conn|
69
+ conn.exec_params('DELETE FROM subscription_commands WHERE id = $1', [id])
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ # @param hash [Hash]
76
+ # @return [Hash]
77
+ def deserialize(hash)
78
+ hash.transform_keys(&:to_sym)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ class SubscriptionQueries
6
+ attr_reader :connection
7
+ private :connection
8
+
9
+ # @param connection [PgEventstore::Connection]
10
+ def initialize(connection)
11
+ @connection = connection
12
+ end
13
+
14
+ # @param attrs [Hash]
15
+ # @return [Hash]
16
+ def find_or_create_by(attrs)
17
+ transaction_queries.transaction do
18
+ find_by(attrs) || create(attrs)
19
+ end
20
+ end
21
+
22
+ # @param attrs [Hash]
23
+ # @return [Hash, nil]
24
+ def find_by(attrs)
25
+ builder = SQLBuilder.new.select('*').from('subscriptions')
26
+ attrs.each do |attr, val|
27
+ builder.where("#{attr} = ?", val)
28
+ end
29
+
30
+ pg_result = connection.with do |conn|
31
+ conn.exec_params(*builder.to_exec_params)
32
+ end
33
+ return if pg_result.ntuples.zero?
34
+
35
+ deserialize(pg_result.to_a.first)
36
+ end
37
+
38
+ # @param id [Integer]
39
+ # @return [Hash]
40
+ # @raise [PgEventstore::RecordNotFound]
41
+ def find!(id)
42
+ find_by(id: id) || raise(RecordNotFound.new("subscriptions", id))
43
+ end
44
+
45
+ # @param attrs [Hash]
46
+ # @return [Hash]
47
+ def create(attrs)
48
+ sql = <<~SQL
49
+ INSERT INTO subscriptions (#{attrs.keys.join(', ')})
50
+ VALUES (#{Utils.positional_vars(attrs.values)})
51
+ RETURNING *
52
+ SQL
53
+ pg_result = connection.with do |conn|
54
+ conn.exec_params(sql, attrs.values)
55
+ end
56
+ deserialize(pg_result.to_a.first)
57
+ end
58
+
59
+ # @param id [Integer]
60
+ # @param attrs [Hash]
61
+ def update(id, attrs)
62
+ attrs = { updated_at: Time.now.utc }.merge(attrs)
63
+ attrs_sql = attrs.keys.map.with_index(1) do |attr, index|
64
+ "#{attr} = $#{index}"
65
+ end.join(', ')
66
+ sql =
67
+ "UPDATE subscriptions SET #{attrs_sql} WHERE id = $#{attrs.keys.size + 1} RETURNING *"
68
+ pg_result = connection.with do |conn|
69
+ conn.exec_params(sql, [*attrs.values, id])
70
+ end
71
+ raise(RecordNotFound.new("subscriptions", id)) if pg_result.ntuples.zero?
72
+
73
+ deserialize(pg_result.to_a.first)
74
+ end
75
+
76
+ # @param query_options [Array<Array<Integer, Hash>>] array of runner ids and query options
77
+ # @return [Array<Hash>] array of raw events
78
+ def subscriptions_events(query_options)
79
+ return [] if query_options.empty?
80
+
81
+ final_builder = union_builders(query_options.map { |id, opts| query_builder(id, opts) })
82
+ connection.with do |conn|
83
+ conn.exec_params(*final_builder.to_exec_params)
84
+ end.to_a
85
+ end
86
+
87
+ # @param id [Integer] subscription's id
88
+ # @param lock_id [String] UUIDv4 id of the subscriptions set which reserves the subscription
89
+ # @param force [Boolean] whether to lock the subscription despite on #locked_by value
90
+ # @return [String] UUIDv4 lock id
91
+ # @raise [SubscriptionAlreadyLockedError] in case the Subscription is already locked
92
+ def lock!(id, lock_id, force = false)
93
+ transaction_queries.transaction do
94
+ attrs = find!(id)
95
+ if attrs[:locked_by] && !force
96
+ raise SubscriptionAlreadyLockedError.new(attrs[:set], attrs[:name], attrs[:locked_by])
97
+ end
98
+ connection.with do |conn|
99
+ conn.exec_params('UPDATE subscriptions SET locked_by = $1 WHERE id = $2', [lock_id, id])
100
+ end
101
+ end
102
+ lock_id
103
+ end
104
+
105
+ # @param id [Integer] subscription's id
106
+ # @param lock_id [String] UUIDv4 id of the set which reserved the subscription after itself
107
+ # @return [void]
108
+ # @raise [SubscriptionUnlockError] in case the Subscription is locked by some SubscriptionsSet, other than the one,
109
+ # persisted in memory
110
+ def unlock!(id, lock_id)
111
+ transaction_queries.transaction do
112
+ attrs = find!(id)
113
+ # Normally this should never happen as locking/unlocking happens within the same process. This is done only for
114
+ # the matter of consistency.
115
+ unless attrs[:locked_by] == lock_id
116
+ raise SubscriptionUnlockError.new(attrs[:set], attrs[:name], lock_id, attrs[:locked_by])
117
+ end
118
+ connection.with do |conn|
119
+ conn.exec_params('UPDATE subscriptions SET locked_by = $1 WHERE id = $2', [nil, id])
120
+ end
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ # @param id [Integer] runner id
127
+ # @param options [Hash] query options
128
+ # @return [PgEventstore::SQLBuilder]
129
+ def query_builder(id, options)
130
+ builder = PgEventstore::QueryBuilders::EventsFiltering.subscriptions_events_filtering(
131
+ event_type_queries.include_event_types_ids(options)
132
+ ).to_sql_builder
133
+ builder.select("#{id} as runner_id")
134
+ end
135
+
136
+ # @param builders [Array<PgEventstore::SQLBuilder>]
137
+ # @return [PgEventstore::SQLBuilder]
138
+ def union_builders(builders)
139
+ builders[1..].each_with_object(builders[0]) do |builder, first_builder|
140
+ first_builder.union(builder)
141
+ end
142
+ end
143
+
144
+ # @return [PgEventstore::TransactionQueries]
145
+ def transaction_queries
146
+ TransactionQueries.new(connection)
147
+ end
148
+
149
+ # @return [PgEventstore::EventTypeQueries]
150
+ def event_type_queries
151
+ EventTypeQueries.new(connection)
152
+ end
153
+
154
+ # @param hash [Hash]
155
+ # @return [Hash]
156
+ def deserialize(hash)
157
+ hash.transform_keys(&:to_sym)
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ class SubscriptionsSetCommandQueries
6
+ attr_reader :connection
7
+ private :connection
8
+
9
+ # @param connection [PgEventstore::Connection]
10
+ def initialize(connection)
11
+ @connection = connection
12
+ end
13
+
14
+ # @param subscriptions_set_id [Integer]
15
+ # @param command_name [String]
16
+ # @return [Hash, nil]
17
+ def find_by(subscriptions_set_id:, command_name:)
18
+ sql_builder =
19
+ SQLBuilder.new.
20
+ select('*').
21
+ from('subscriptions_set_commands').
22
+ where('subscriptions_set_id = ? AND name = ?', subscriptions_set_id, command_name)
23
+ pg_result = connection.with do |conn|
24
+ conn.exec_params(*sql_builder.to_exec_params)
25
+ end
26
+ return if pg_result.ntuples.zero?
27
+
28
+ deserialize(pg_result.to_a.first)
29
+ end
30
+
31
+ # @param subscriptions_set_id [Integer]
32
+ # @param command_name [String]
33
+ # @return [Hash]
34
+ def create_by(subscriptions_set_id:, command_name:)
35
+ sql = <<~SQL
36
+ INSERT INTO subscriptions_set_commands (name, subscriptions_set_id)
37
+ VALUES ($1, $2)
38
+ RETURNING *
39
+ SQL
40
+ pg_result = connection.with do |conn|
41
+ conn.exec_params(sql, [command_name, subscriptions_set_id])
42
+ end
43
+ deserialize(pg_result.to_a.first)
44
+ end
45
+
46
+ # @param subscriptions_set_id [Integer]
47
+ # @return [Array<Hash>]
48
+ def find_commands(subscriptions_set_id)
49
+ sql_builder =
50
+ SQLBuilder.new.select('*').
51
+ from('subscriptions_set_commands').
52
+ where("subscriptions_set_id = ?", subscriptions_set_id).
53
+ order('id ASC')
54
+ pg_result = connection.with do |conn|
55
+ conn.exec_params(*sql_builder.to_exec_params)
56
+ end
57
+ pg_result.to_a.map(&method(:deserialize))
58
+ end
59
+
60
+ # @param id [Integer]
61
+ # @return [void]
62
+ def delete(id)
63
+ connection.with do |conn|
64
+ conn.exec_params('DELETE FROM subscriptions_set_commands WHERE id = $1', [id])
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # @param hash [Hash]
71
+ # @return [Hash]
72
+ def deserialize(hash)
73
+ hash.transform_keys(&:to_sym)
74
+ end
75
+ end
76
+ end