pg_eventstore 0.3.0 → 0.5.0

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