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,166 @@
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
+ raw_events = connection.with do |conn|
83
+ conn.exec_params(*final_builder.to_exec_params)
84
+ end.to_a
85
+ preloader.preload_related_objects(raw_events)
86
+ end
87
+
88
+ # @param id [Integer] subscription's id
89
+ # @param lock_id [String] UUIDv4 id of the subscriptions set which reserves the subscription
90
+ # @param force [Boolean] whether to lock the subscription despite on #locked_by value
91
+ # @return [String] UUIDv4 lock id
92
+ # @raise [SubscriptionAlreadyLockedError] in case the Subscription is already locked
93
+ def lock!(id, lock_id, force = false)
94
+ transaction_queries.transaction do
95
+ attrs = find!(id)
96
+ if attrs[:locked_by] && !force
97
+ raise SubscriptionAlreadyLockedError.new(attrs[:set], attrs[:name], attrs[:locked_by])
98
+ end
99
+ connection.with do |conn|
100
+ conn.exec_params('UPDATE subscriptions SET locked_by = $1 WHERE id = $2', [lock_id, id])
101
+ end
102
+ end
103
+ lock_id
104
+ end
105
+
106
+ # @param id [Integer] subscription's id
107
+ # @param lock_id [String] UUIDv4 id of the set which reserved the subscription after itself
108
+ # @return [void]
109
+ # @raise [SubscriptionUnlockError] in case the Subscription is locked by some SubscriptionsSet, other than the one,
110
+ # persisted in memory
111
+ def unlock!(id, lock_id)
112
+ transaction_queries.transaction do
113
+ attrs = find!(id)
114
+ # Normally this should never happen as locking/unlocking happens within the same process. This is done only for
115
+ # the matter of consistency.
116
+ unless attrs[:locked_by] == lock_id
117
+ raise SubscriptionUnlockError.new(attrs[:set], attrs[:name], lock_id, attrs[:locked_by])
118
+ end
119
+ connection.with do |conn|
120
+ conn.exec_params('UPDATE subscriptions SET locked_by = $1 WHERE id = $2', [nil, id])
121
+ end
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ # @param id [Integer] runner id
128
+ # @param options [Hash] query options
129
+ # @return [PgEventstore::SQLBuilder]
130
+ def query_builder(id, options)
131
+ builder = PgEventstore::QueryBuilders::EventsFiltering.subscriptions_events_filtering(
132
+ event_type_queries.include_event_types_ids(options)
133
+ ).to_sql_builder
134
+ builder.select("#{id} as runner_id")
135
+ end
136
+
137
+ # @param builders [Array<PgEventstore::SQLBuilder>]
138
+ # @return [PgEventstore::SQLBuilder]
139
+ def union_builders(builders)
140
+ builders[1..].each_with_object(builders[0]) do |builder, first_builder|
141
+ first_builder.union(builder)
142
+ end
143
+ end
144
+
145
+ # @return [PgEventstore::TransactionQueries]
146
+ def transaction_queries
147
+ TransactionQueries.new(connection)
148
+ end
149
+
150
+ # @return [PgEventstore::EventTypeQueries]
151
+ def event_type_queries
152
+ EventTypeQueries.new(connection)
153
+ end
154
+
155
+ # @return [PgEventstore::Preloader]
156
+ def preloader
157
+ Preloader.new(connection)
158
+ end
159
+
160
+ # @param hash [Hash]
161
+ # @return [Hash]
162
+ def deserialize(hash)
163
+ hash.transform_keys(&:to_sym)
164
+ end
165
+ end
166
+ 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
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ class SubscriptionsSetQueries
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 [Array<Hash>]
16
+ def find_all(attrs)
17
+ builder = SQLBuilder.new.select('*').from('subscriptions_set')
18
+ attrs.each do |attr, val|
19
+ builder.where("#{attr} = ?", val)
20
+ end
21
+
22
+ pg_result = connection.with do |conn|
23
+ conn.exec_params(*builder.to_exec_params)
24
+ end
25
+ pg_result.to_a.map(&method(:deserialize))
26
+ end
27
+
28
+ # The same as #find_all, but returns first result
29
+ # @return [Hash, nil]
30
+ def find_by(...)
31
+ find_all(...).first
32
+ end
33
+
34
+ # @param id [String] UUIDv4
35
+ # @return [Hash]
36
+ # @raise [PgEventstore::RecordNotFound]
37
+ def find!(id)
38
+ find_by(id: id) || raise(RecordNotFound.new("subscriptions_set", id))
39
+ end
40
+
41
+ # @param attrs [Hash]
42
+ # @return [Hash]
43
+ def create(attrs)
44
+ sql = <<~SQL
45
+ INSERT INTO subscriptions_set (#{attrs.keys.join(', ')})
46
+ VALUES (#{Utils.positional_vars(attrs.values)})
47
+ RETURNING *
48
+ SQL
49
+ pg_result = connection.with do |conn|
50
+ conn.exec_params(sql, attrs.values)
51
+ end
52
+ deserialize(pg_result.to_a.first)
53
+ end
54
+
55
+ # @param id [String] UUIDv4
56
+ # @param attrs [Hash]
57
+ def update(id, attrs)
58
+ attrs = { updated_at: Time.now.utc }.merge(attrs)
59
+ attrs_sql = attrs.keys.map.with_index(1) do |attr, index|
60
+ "#{attr} = $#{index}"
61
+ end.join(', ')
62
+ sql = <<~SQL
63
+ UPDATE subscriptions_set SET #{attrs_sql} WHERE id = $#{attrs.keys.size + 1} RETURNING *
64
+ SQL
65
+ pg_result = connection.with do |conn|
66
+ conn.exec_params(sql, [*attrs.values, id])
67
+ end
68
+ raise(RecordNotFound.new("subscriptions_set", id)) if pg_result.ntuples.zero?
69
+
70
+ deserialize(pg_result.to_a.first)
71
+ end
72
+
73
+ # @return id [Integer]
74
+ # @return [void]
75
+ def delete(id)
76
+ connection.with do |conn|
77
+ conn.exec_params('DELETE FROM subscriptions_set WHERE id = $1', [id])
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ # @param hash [Hash]
84
+ # @return [Hash]
85
+ def deserialize(hash)
86
+ hash.transform_keys(&:to_sym)
87
+ end
88
+ end
89
+ end
@@ -1,9 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'sql_builder'
4
+ require_relative 'query_builders/events_filtering_query'
3
5
  require_relative 'queries/transaction_queries'
4
6
  require_relative 'queries/event_queries'
5
7
  require_relative 'queries/stream_queries'
6
8
  require_relative 'queries/event_type_queries'
9
+ require_relative 'queries/subscription_queries'
10
+ require_relative 'queries/subscriptions_set_queries'
11
+ require_relative 'queries/subscription_command_queries'
12
+ require_relative 'queries/subscriptions_set_command_queries'
13
+ require_relative 'queries/preloader'
7
14
 
8
15
  module PgEventstore
9
16
  # @!visibility private
@@ -4,8 +4,7 @@ module PgEventstore
4
4
  module QueryBuilders
5
5
  # @!visibility private
6
6
  class EventsFiltering
7
- DEFAULT_OFFSET = 0
8
- DEFAULT_LIMIT = 1000
7
+ DEFAULT_LIMIT = 1_000
9
8
  SQL_DIRECTIONS = {
10
9
  'asc' => 'ASC',
11
10
  'desc' => 'DESC',
@@ -16,17 +15,22 @@ module PgEventstore
16
15
  }.tap do |directions|
17
16
  directions.default = 'ASC'
18
17
  end.freeze
18
+ SUBSCRIPTIONS_OPTIONS = %i[from_position resolve_link_tos filter max_count].freeze
19
19
 
20
20
  class << self
21
21
  # @param options [Hash]
22
- # @param offset [Integer]
23
22
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
24
- def all_stream_filtering(options, offset: 0)
23
+ def subscriptions_events_filtering(options)
24
+ all_stream_filtering(options.slice(*SUBSCRIPTIONS_OPTIONS))
25
+ end
26
+
27
+ # @param options [Hash]
28
+ # @return [PgEventstore::QueryBuilders::EventsFiltering]
29
+ def all_stream_filtering(options)
25
30
  event_filter = new
26
31
  options in { filter: { event_type_ids: Array => event_type_ids } }
27
32
  event_filter.add_event_types(event_type_ids)
28
33
  event_filter.add_limit(options[:max_count])
29
- event_filter.add_offset(offset)
30
34
  event_filter.resolve_links(options[:resolve_link_tos])
31
35
  options in { filter: { streams: Array => streams } }
32
36
  streams&.each { |attrs| event_filter.add_stream_attrs(**attrs) }
@@ -37,14 +41,12 @@ module PgEventstore
37
41
 
38
42
  # @param stream [PgEventstore::Stream]
39
43
  # @param options [Hash]
40
- # @param offset [Integer]
41
44
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
42
- def specific_stream_filtering(stream, options, offset: 0)
45
+ def specific_stream_filtering(stream, options)
43
46
  event_filter = new
44
47
  options in { filter: { event_type_ids: Array => event_type_ids } }
45
48
  event_filter.add_event_types(event_type_ids)
46
49
  event_filter.add_limit(options[:max_count])
47
- event_filter.add_offset(offset)
48
50
  event_filter.resolve_links(options[:resolve_link_tos])
49
51
  event_filter.add_stream(stream)
50
52
  event_filter.add_revision(options[:from_revision], options[:direction])
@@ -57,13 +59,10 @@ module PgEventstore
57
59
  @sql_builder =
58
60
  SQLBuilder.new.
59
61
  select('events.*').
60
- select('row_to_json(streams.*) as stream').
61
- select('event_types.type as type').
62
62
  from('events').
63
63
  join('JOIN streams ON streams.id = events.stream_id').
64
64
  join('JOIN event_types ON event_types.id = events.event_type_id').
65
- limit(DEFAULT_LIMIT).
66
- offset(DEFAULT_OFFSET)
65
+ limit(DEFAULT_LIMIT)
67
66
  end
68
67
 
69
68
  # @param context [String, nil]
@@ -96,7 +95,7 @@ module PgEventstore
96
95
  sql = event_type_ids.size.times.map do
97
96
  "?"
98
97
  end.join(", ")
99
- @sql_builder.where("event_types.id IN (#{sql})", *event_type_ids)
98
+ @sql_builder.where("events.event_type_id IN (#{sql})", *event_type_ids)
100
99
  end
101
100
 
102
101
  # @param revision [Integer, nil]
@@ -137,14 +136,6 @@ module PgEventstore
137
136
  @sql_builder.limit(limit)
138
137
  end
139
138
 
140
- # @param offset [Integer, nil]
141
- # @return [void]
142
- def add_offset(offset)
143
- return unless offset
144
-
145
- @sql_builder.offset(offset)
146
- end
147
-
148
139
  # @param should_resolve [Boolean]
149
140
  # @return [void]
150
141
  def resolve_links(should_resolve)
@@ -153,10 +144,14 @@ module PgEventstore
153
144
  @sql_builder.
154
145
  unselect.
155
146
  select("(COALESCE(original_events.*, events.*)).*").
156
- select('row_to_json(streams.*) as stream').
157
147
  join("LEFT JOIN events original_events ON original_events.id = events.link_id")
158
148
  end
159
149
 
150
+ # @return [PgEventstore::SQLBuilder]
151
+ def to_sql_builder
152
+ @sql_builder
153
+ end
154
+
160
155
  # @return [Array]
161
156
  def to_exec_params
162
157
  @sql_builder.to_exec_params
@@ -13,6 +13,8 @@ module PgEventstore
13
13
  @limit_value = nil
14
14
  @offset_value = nil
15
15
  @positional_values = []
16
+ @positional_values_size = 0
17
+ @union_values = []
16
18
  end
17
19
 
18
20
  # @param sql [String]
@@ -32,8 +34,7 @@ module PgEventstore
32
34
  # @param arguments [Array] positional values
33
35
  # @return self
34
36
  def where(sql, *arguments)
35
- sql = extract_positional_args(sql, *arguments)
36
- @where_values['AND'].push("(#{sql})")
37
+ @where_values['AND'].push([sql, arguments])
37
38
  self
38
39
  end
39
40
 
@@ -41,8 +42,7 @@ module PgEventstore
41
42
  # @param arguments [Object] positional values
42
43
  # @return self
43
44
  def where_or(sql, *arguments)
44
- sql = extract_positional_args(sql, *arguments)
45
- @where_values['OR'].push("(#{sql})")
45
+ @where_values['OR'].push([sql, arguments])
46
46
  self
47
47
  end
48
48
 
@@ -57,7 +57,7 @@ module PgEventstore
57
57
  # @param arguments [Object]
58
58
  # @return self
59
59
  def join(sql, *arguments)
60
- @join_values.push(extract_positional_args(sql, *arguments))
60
+ @join_values.push([sql, arguments])
61
61
  self
62
62
  end
63
63
 
@@ -82,7 +82,34 @@ module PgEventstore
82
82
  self
83
83
  end
84
84
 
85
+ # @param another_builder [PgEventstore::SQLBuilder]
86
+ # @return [self]
87
+ def union(another_builder)
88
+ @union_values.push(another_builder)
89
+ self
90
+ end
91
+
85
92
  def to_exec_params
93
+ return [single_query_sql, @positional_values] if @union_values.empty?
94
+
95
+ [union_query_sql, @positional_values]
96
+ end
97
+
98
+ protected
99
+
100
+ # @return [Array<Object>] sql positional values
101
+ def positional_values
102
+ @positional_values
103
+ end
104
+
105
+ def positional_values_size=(val)
106
+ @positional_values_size = val
107
+ end
108
+
109
+ private
110
+
111
+ # @return [String]
112
+ def single_query_sql
86
113
  where_sql = [where_sql('OR'), where_sql('AND')].reject(&:empty?).map { |sql| "(#{sql})" }.join(' AND ')
87
114
  sql = "SELECT #{select_sql} FROM #{@from_value}"
88
115
  sql += " #{join_sql}" unless @join_values.empty?
@@ -90,10 +117,22 @@ module PgEventstore
90
117
  sql += " ORDER BY #{order_sql}" unless @order_values.empty?
91
118
  sql += " LIMIT #{@limit_value}" if @limit_value
92
119
  sql += " OFFSET #{@offset_value}" if @offset_value
93
- [sql, @positional_values]
120
+ sql
94
121
  end
95
122
 
96
- private
123
+ # @return [String]
124
+ def union_query_sql
125
+ sql = single_query_sql
126
+ union_parts = ["(#{sql})"]
127
+ union_parts += @union_values.map do |builder|
128
+ builder.positional_values_size = @positional_values_size
129
+ builder_sql, values = builder.to_exec_params
130
+ @positional_values.push(*values)
131
+ @positional_values_size += values.size
132
+ "(#{builder_sql})"
133
+ end
134
+ union_parts.join(' UNION ALL ')
135
+ end
97
136
 
98
137
  # @return [String]
99
138
  def select_sql
@@ -103,12 +142,14 @@ module PgEventstore
103
142
  # @param join_pattern [String] "OR"/"AND"
104
143
  # @return [String]
105
144
  def where_sql(join_pattern)
106
- @where_values[join_pattern].join(" #{join_pattern} ")
145
+ @where_values[join_pattern].map do |sql, args|
146
+ "(#{extract_positional_args(sql, *args)})"
147
+ end.join(" #{join_pattern} ")
107
148
  end
108
149
 
109
150
  # @return [String]
110
151
  def join_sql
111
- @join_values.join(" ")
152
+ @join_values.map { |sql, args| extract_positional_args(sql, *args) }.join(" ")
112
153
  end
113
154
 
114
155
  # @return [String]
@@ -116,10 +157,13 @@ module PgEventstore
116
157
  @order_values.join(', ')
117
158
  end
118
159
 
160
+ # Replaces "?" signs in the given string with positional variables and memorize positional values they refer to.
161
+ # @return [String]
119
162
  def extract_positional_args(sql, *arguments)
120
163
  sql.gsub("?").each_with_index do |_, index|
121
164
  @positional_values.push(arguments[index])
122
- "$#{@positional_values.size}"
165
+ @positional_values_size += 1
166
+ "$#{@positional_values_size}"
123
167
  end
124
168
  end
125
169
  end
@@ -18,7 +18,8 @@ module PgEventstore
18
18
  end
19
19
  end
20
20
 
21
- attr_reader :context, :stream_name, :stream_id, :id, :stream_revision
21
+ attr_reader :context, :stream_name, :stream_id, :id
22
+ attr_accessor :stream_revision
22
23
 
23
24
  # @param context [String]
24
25
  # @param stream_name [String]