pg_eventstore 0.3.0 → 0.4.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 (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