pg_eventstore 0.3.0 → 0.5.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/CODE_OF_CONDUCT.md +1 -1
  4. data/README.md +2 -0
  5. data/db/migrations/10_create_subscription_commands.sql +15 -0
  6. data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
  7. data/db/migrations/12_improve_events_indexes.sql +1 -0
  8. data/db/migrations/13_remove_duplicated_index.sql +1 -0
  9. data/db/migrations/9_create_subscriptions.sql +46 -0
  10. data/docs/configuration.md +42 -21
  11. data/docs/linking_events.md +96 -0
  12. data/docs/reading_events.md +56 -0
  13. data/docs/subscriptions.md +170 -0
  14. data/lib/pg_eventstore/callbacks.rb +122 -0
  15. data/lib/pg_eventstore/client.rb +32 -2
  16. data/lib/pg_eventstore/commands/append.rb +3 -11
  17. data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +22 -0
  18. data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +24 -0
  19. data/lib/pg_eventstore/commands/link_to.rb +33 -0
  20. data/lib/pg_eventstore/commands/regular_stream_read_paginated.rb +63 -0
  21. data/lib/pg_eventstore/commands/system_stream_read_paginated.rb +62 -0
  22. data/lib/pg_eventstore/commands.rb +5 -0
  23. data/lib/pg_eventstore/config.rb +35 -3
  24. data/lib/pg_eventstore/errors.rb +80 -0
  25. data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +10 -22
  26. data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
  27. data/lib/pg_eventstore/extensions/options_extension.rb +69 -29
  28. data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
  29. data/lib/pg_eventstore/pg_connection.rb +20 -3
  30. data/lib/pg_eventstore/queries/event_queries.rb +18 -34
  31. data/lib/pg_eventstore/queries/event_type_queries.rb +24 -0
  32. data/lib/pg_eventstore/queries/preloader.rb +37 -0
  33. data/lib/pg_eventstore/queries/stream_queries.rb +14 -1
  34. data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
  35. data/lib/pg_eventstore/queries/subscription_queries.rb +166 -0
  36. data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
  37. data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
  38. data/lib/pg_eventstore/queries.rb +7 -0
  39. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +17 -22
  40. data/lib/pg_eventstore/sql_builder.rb +54 -10
  41. data/lib/pg_eventstore/stream.rb +2 -1
  42. data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
  43. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
  44. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
  45. data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
  46. data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
  47. data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
  48. data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
  49. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
  50. data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
  51. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
  52. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
  53. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
  54. data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
  55. data/lib/pg_eventstore/tasks/setup.rake +5 -1
  56. data/lib/pg_eventstore/utils.rb +66 -0
  57. data/lib/pg_eventstore/version.rb +1 -1
  58. data/lib/pg_eventstore.rb +19 -1
  59. metadata +38 -4
@@ -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
@@ -48,7 +50,11 @@ module PgEventstore
48
50
  self.options = (options + Set.new([opt_name])).freeze
49
51
  warn_already_defined(opt_name)
50
52
  warn_already_defined(:"#{opt_name}=")
51
- attr_writer opt_name
53
+ define_method "#{opt_name}=" do |value|
54
+ readonly_error(opt_name) if readonly?(opt_name)
55
+
56
+ instance_variable_set(:"@#{opt_name}", value)
57
+ end
52
58
 
53
59
  define_method opt_name do
54
60
  result = instance_variable_get(:"@#{opt_name}")
@@ -75,6 +81,8 @@ module PgEventstore
75
81
  end
76
82
  end
77
83
 
84
+ ReadonlyAttributeError = Class.new(StandardError)
85
+
78
86
  def self.included(klass)
79
87
  klass.singleton_class.attr_accessor(:options)
80
88
  klass.options = Set.new.freeze
@@ -82,11 +90,8 @@ module PgEventstore
82
90
  end
83
91
 
84
92
  def initialize(**options)
85
- self.class.options.each do |option|
86
- # init default values of options
87
- value = options.key?(option) ? options[option] : public_send(option)
88
- public_send("#{option}=", value)
89
- end
93
+ @readonly = Set.new
94
+ init_default_values(options)
90
95
  end
91
96
 
92
97
  # Construct a hash from options, where key is the option's name and the value is option's
@@ -98,6 +103,41 @@ module PgEventstore
98
103
  end
99
104
  end
100
105
  alias attributes_hash options_hash
106
+
107
+ # @param opt_name [Symbol]
108
+ # @return [Boolean]
109
+ def readonly!(opt_name)
110
+ return false unless self.class.options.include?(opt_name)
111
+
112
+ @readonly.add(opt_name)
113
+ true
114
+ end
115
+
116
+ # @param opt_name [Symbol]
117
+ # @return [Boolean]
118
+ def readonly?(opt_name)
119
+ @readonly.include?(opt_name)
120
+ end
121
+
122
+ private
123
+
124
+ # @param opt_name [Symbol]
125
+ # @raise [PgEventstore::Extensions::OptionsExtension::ReadOnlyError]
126
+ def readonly_error(opt_name)
127
+ raise(
128
+ ReadonlyAttributeError, "#{opt_name.inspect} attribute was marked as read only. You can no longer modify it."
129
+ )
130
+ end
131
+
132
+ # @param options [Hash]
133
+ # @return [void]
134
+ def init_default_values(options)
135
+ self.class.options.each do |option|
136
+ # init default values of options
137
+ value = options.key?(option) ? options[option] : public_send(option)
138
+ public_send("#{option}=", value)
139
+ end
140
+ end
101
141
  end
102
142
  end
103
143
  end
@@ -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
@@ -19,11 +19,28 @@ module PgEventstore
19
19
 
20
20
  sql = sql.gsub(/\$\d+/).each do |matched|
21
21
  value = params[matched[1..].to_i - 1]
22
-
23
- value = type_map_for_queries[value.class]&.encode(value) || value
24
- value.is_a?(String) ? "'#{value}'" : value
22
+ value = encode_value(value)
23
+ normalize_value(value)
25
24
  end unless params&.empty?
26
25
  PgEventstore.logger.debug(sql)
27
26
  end
27
+
28
+ def encode_value(value)
29
+ encoder = type_map_for_queries[value.class]
30
+ return type_map_for_queries.send(encoder, value).encode(value) if encoder.is_a?(Symbol)
31
+
32
+ type_map_for_queries[value.class]&.encode(value) || value
33
+ end
34
+
35
+ def normalize_value(value)
36
+ case value
37
+ when String
38
+ "'#{value}'"
39
+ when NilClass
40
+ 'NULL'
41
+ else
42
+ value
43
+ end
44
+ end
28
45
  end
29
46
  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,13 @@ 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
- pg_result = connection.with do |conn|
25
+ raw_events = connection.with do |conn|
28
26
  conn.exec_params(*exec_params)
29
- end
30
- deserializer.deserialize_many(pg_result)
27
+ end.to_a
28
+ preloader.preload_related_objects(raw_events)
29
+ deserializer.deserialize_many(raw_events)
31
30
  end
32
31
 
33
32
  # @param stream [PgEventstore::Stream] persisted stream
@@ -42,14 +41,14 @@ module PgEventstore
42
41
 
43
42
  sql = <<~SQL
44
43
  INSERT INTO events (#{attributes.keys.join(', ')})
45
- VALUES (#{positional_vars(attributes.values)})
44
+ VALUES (#{Utils.positional_vars(attributes.values)})
46
45
  RETURNING *, $#{attributes.values.size + 1} as type
47
46
  SQL
48
47
 
49
- pg_result = connection.with do |conn|
48
+ raw_event = connection.with do |conn|
50
49
  conn.exec_params(sql, [*attributes.values, event.type])
51
- end
52
- deserializer.without_middlewares.deserialize_one(pg_result).tap do |persisted_event|
50
+ end.to_a.first
51
+ deserializer.without_middlewares.deserialize(raw_event).tap do |persisted_event|
53
52
  persisted_event.stream = stream
54
53
  end
55
54
  end
@@ -58,36 +57,21 @@ module PgEventstore
58
57
 
59
58
  # @param stream [PgEventstore::Stream]
60
59
  # @param options [Hash]
61
- # @param offset [Integer]
62
60
  # @return [PgEventstore::EventsFilteringQuery]
63
- def events_filtering(stream, options, offset: 0)
64
- return QueryBuilders::EventsFiltering.all_stream_filtering(options, offset: offset) if stream.all_stream?
61
+ def events_filtering(stream, options)
62
+ return QueryBuilders::EventsFiltering.all_stream_filtering(options) if stream.all_stream?
65
63
 
66
- QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options, offset: offset)
67
- end
68
-
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(', ')
64
+ QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options)
86
65
  end
87
66
 
88
67
  # @return [PgEventstore::EventTypeQueries]
89
68
  def event_type_queries
90
69
  EventTypeQueries.new(connection)
91
70
  end
71
+
72
+ # @return [PgEventstore::Preloader]
73
+ def preloader
74
+ Preloader.new(connection)
75
+ end
92
76
  end
93
77
  end
@@ -33,6 +33,17 @@ module PgEventstore
33
33
  end.to_a.dig(0, 'id')
34
34
  end
35
35
 
36
+ # @param ids [Array<Integer>]
37
+ # @return [Array<Hash>]
38
+ def find_by_ids(ids)
39
+ return [] if ids.empty?
40
+
41
+ builder = SQLBuilder.new.from('event_types').where('id = ANY(?)', ids.uniq)
42
+ connection.with do |conn|
43
+ conn.exec_params(*builder.to_exec_params)
44
+ end.to_a
45
+ end
46
+
36
47
  # @param types [Array<String>]
37
48
  # @return [Array<Integer, nil>]
38
49
  def find_event_types(types)
@@ -46,5 +57,18 @@ module PgEventstore
46
57
  SQL
47
58
  end.to_a.map { |attrs| attrs['id'] }
48
59
  end
60
+
61
+ # Replaces filter by event type strings with filter by event type ids
62
+ # @param options [Hash]
63
+ # @return [Hash]
64
+ def include_event_types_ids(options)
65
+ options in { filter: { event_types: Array => event_types } }
66
+ return options unless event_types
67
+
68
+ options = Utils.deep_dup(options)
69
+ options[:filter][:event_type_ids] = find_event_types(event_types).uniq
70
+ options[:filter].delete(:event_types)
71
+ options
72
+ end
49
73
  end
50
74
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ class Preloader
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 raw_events [Array<Hash>]
15
+ # @return [Array<Hash>]
16
+ def preload_related_objects(raw_events)
17
+ streams = stream_queries.find_by_ids(raw_events.map { _1['stream_id'] }).to_h { [_1['id'], _1] }
18
+ types = event_type_queries.find_by_ids(raw_events.map { _1['event_type_id'] }).to_h { [_1['id'], _1] }
19
+ raw_events.each do |event|
20
+ event['stream'] = streams[event['stream_id']]
21
+ event['type'] = types[event['event_type_id']]['type']
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # @return [PgEventstore::EventTypeQueries]
28
+ def event_type_queries
29
+ EventTypeQueries.new(connection)
30
+ end
31
+
32
+ # @return [PgEventstore::StreamQueries]
33
+ def stream_queries
34
+ StreamQueries.new(connection)
35
+ end
36
+ end
37
+ end
@@ -26,6 +26,17 @@ module PgEventstore
26
26
  deserialize(pg_result) if pg_result.ntuples == 1
27
27
  end
28
28
 
29
+ # @param ids [Array<Integer>]
30
+ # @return [Array<Hash>]
31
+ def find_by_ids(ids)
32
+ return [] if ids.empty?
33
+
34
+ builder = SQLBuilder.new.from('streams').where('id = ANY(?)', ids.uniq.sort)
35
+ connection.with do |conn|
36
+ conn.exec_params(*builder.to_exec_params)
37
+ end.to_a
38
+ end
39
+
29
40
  # @param stream [PgEventstore::Stream]
30
41
  # @return [PgEventstore::RawStream] persisted stream
31
42
  def create_stream(stream)
@@ -44,13 +55,15 @@ module PgEventstore
44
55
  end
45
56
 
46
57
  # @param stream [PgEventstore::Stream] persisted stream
47
- # @return [void]
58
+ # @return [PgEventstore::Stream]
48
59
  def update_stream_revision(stream, revision)
49
60
  connection.with do |conn|
50
61
  conn.exec_params(<<~SQL, [revision, stream.id])
51
62
  UPDATE streams SET stream_revision = $1 WHERE id = $2
52
63
  SQL
53
64
  end
65
+ stream.stream_revision = revision
66
+ stream
54
67
  end
55
68
 
56
69
  private
@@ -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