pg_eventstore 1.11.0 → 1.13.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/docs/reading_events.md +98 -0
  4. data/lib/pg_eventstore/cli/commands/base_command.rb +2 -0
  5. data/lib/pg_eventstore/cli/commands/callback_handlers/start_cmd_handlers.rb +1 -0
  6. data/lib/pg_eventstore/cli/commands/help_command.rb +1 -0
  7. data/lib/pg_eventstore/cli/commands/start_subscriptions_command.rb +3 -1
  8. data/lib/pg_eventstore/cli/commands/stop_subscriptions_command.rb +1 -0
  9. data/lib/pg_eventstore/cli/exit_codes.rb +1 -0
  10. data/lib/pg_eventstore/cli/parser_options/base_options.rb +1 -0
  11. data/lib/pg_eventstore/cli/parser_options/default_options.rb +1 -0
  12. data/lib/pg_eventstore/cli/parser_options/metadata.rb +1 -0
  13. data/lib/pg_eventstore/cli/parser_options/subscription_options.rb +1 -0
  14. data/lib/pg_eventstore/cli/try_to_delete_subscriptions_set.rb +2 -1
  15. data/lib/pg_eventstore/cli/try_unlock_subscriptions_set.rb +1 -0
  16. data/lib/pg_eventstore/cli/wait_for_subscriptions_set_shutdown.rb +1 -0
  17. data/lib/pg_eventstore/client.rb +22 -4
  18. data/lib/pg_eventstore/commands/all_stream_read_grouped.rb +69 -0
  19. data/lib/pg_eventstore/commands/regular_stream_read_grouped.rb +31 -0
  20. data/lib/pg_eventstore/commands.rb +2 -0
  21. data/lib/pg_eventstore/connection.rb +8 -3
  22. data/lib/pg_eventstore/extensions/callback_handlers_extension.rb +1 -0
  23. data/lib/pg_eventstore/partition.rb +23 -0
  24. data/lib/pg_eventstore/pg_connection.rb +1 -0
  25. data/lib/pg_eventstore/queries/event_queries.rb +18 -0
  26. data/lib/pg_eventstore/queries/partition_queries.rb +21 -0
  27. data/lib/pg_eventstore/queries.rb +2 -0
  28. data/lib/pg_eventstore/query_builders/basic_filtering.rb +27 -0
  29. data/lib/pg_eventstore/query_builders/events_filtering.rb +47 -31
  30. data/lib/pg_eventstore/query_builders/partitions_filtering.rb +83 -0
  31. data/lib/pg_eventstore/sql_builder.rb +10 -0
  32. data/lib/pg_eventstore/subscriptions/basic_runner.rb +122 -35
  33. data/lib/pg_eventstore/subscriptions/callback_handlers/commands_handler_handlers.rb +1 -14
  34. data/lib/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rb +4 -3
  35. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rb +1 -14
  36. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +1 -19
  37. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +1 -0
  38. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +1 -0
  39. data/lib/pg_eventstore/subscriptions/commands_handler.rb +5 -8
  40. data/lib/pg_eventstore/subscriptions/events_processor.rb +7 -2
  41. data/lib/pg_eventstore/subscriptions/extensions/base_command_extension.rb +1 -0
  42. data/lib/pg_eventstore/subscriptions/extensions/command_class_lookup_extension.rb +1 -0
  43. data/lib/pg_eventstore/subscriptions/queries/subscription_queries.rb +1 -9
  44. data/lib/pg_eventstore/subscriptions/runner_recovery_strategies/restore_connection.rb +44 -0
  45. data/lib/pg_eventstore/subscriptions/runner_recovery_strategies/restore_subscription_feeder.rb +27 -0
  46. data/lib/pg_eventstore/subscriptions/runner_recovery_strategies/restore_subscription_runner.rb +34 -0
  47. data/lib/pg_eventstore/subscriptions/runner_recovery_strategies.rb +5 -0
  48. data/lib/pg_eventstore/subscriptions/runner_recovery_strategy.rb +21 -0
  49. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +18 -5
  50. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +1 -13
  51. data/lib/pg_eventstore/subscriptions/subscriptions_lifecycle.rb +1 -0
  52. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +20 -3
  53. data/lib/pg_eventstore/subscriptions/subscriptions_set_lifecycle.rb +1 -0
  54. data/lib/pg_eventstore/utils.rb +5 -4
  55. data/lib/pg_eventstore/version.rb +1 -1
  56. data/lib/pg_eventstore/web/application.rb +5 -3
  57. data/lib/pg_eventstore.rb +3 -1
  58. data/sig/pg_eventstore/client.rbs +2 -0
  59. data/sig/pg_eventstore/commands/all_stream_read_grouped.rbs +16 -0
  60. data/sig/pg_eventstore/commands/regular_stream_read_grouped.rbs +8 -0
  61. data/sig/pg_eventstore/connection.rbs +2 -0
  62. data/sig/pg_eventstore/partition.rbs +15 -0
  63. data/sig/pg_eventstore/queries/event_queries.rbs +2 -0
  64. data/sig/pg_eventstore/queries/partition_queries.rbs +6 -0
  65. data/sig/pg_eventstore/query_builders/basic_filtering.rbs +15 -0
  66. data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +17 -17
  67. data/sig/pg_eventstore/query_builders/partitions_filtering.rbs +21 -0
  68. data/sig/pg_eventstore/sql_builder.rbs +1 -1
  69. data/sig/pg_eventstore/subscriptions/basic_runner.rbs +26 -10
  70. data/sig/pg_eventstore/subscriptions/callback_handlers/commands_handler_handlers.rbs +5 -5
  71. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rbs +1 -3
  72. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +0 -4
  73. data/sig/pg_eventstore/subscriptions/commands_handler.rbs +8 -11
  74. data/sig/pg_eventstore/subscriptions/events_processor.rbs +5 -17
  75. data/sig/pg_eventstore/subscriptions/runner_recovery_strategies/restore_connection.rbs +18 -0
  76. data/sig/pg_eventstore/subscriptions/runner_recovery_strategies/restore_subscription_feeder.rbs +11 -0
  77. data/sig/pg_eventstore/subscriptions/runner_recovery_strategies/restore_subscription_runner.rbs +17 -0
  78. data/sig/pg_eventstore/subscriptions/runner_recovery_strategy.rbs +7 -0
  79. data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +6 -8
  80. data/sig/pg_eventstore/subscriptions/subscription_runner.rbs +6 -35
  81. data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +8 -0
  82. metadata +22 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 402dc90a012b5e1ee934da4f62c4af4ba62e69693a45dac1a84d461950da5717
4
- data.tar.gz: 6c5d1fdc2e9475eca72b5ef27d0081e0aec6331f76639b7172e676d8b6e63e3d
3
+ metadata.gz: 3de3a26ba4a2773815fdc36adc4852acf4ce1248b6add884673c9009bae8f9d1
4
+ data.tar.gz: 2a160259af68bb8522497f5013754215ee19a95d862388246a45498c8f24d8a0
5
5
  SHA512:
6
- metadata.gz: b20c1124f2108b671bd28f3677fb3b2ae88f3b19d2470eb5a31102eb597d4bd5af1080f9bd337b1528107eef2e4bce6b882d434aa8536abc0a5a80b373bfe3a3
7
- data.tar.gz: 9fd5af079cedc277e278b2e0b50e628601880a81ec5bf7c94700595f6472be389b2fa3d3a2ba2f5d311c8844da40de2c4a52e4dc6c2bda2ca3559e94b267f7f2
6
+ metadata.gz: 5b044fb3e689885d85b3af84caade98f5a66a020cf40991a6b0b2fd2bbf352089be24b7f31313de8c537fe65dc56c9fad2dec4b281eb792aae0bfa150c4d63be
7
+ data.tar.gz: '0899781c0d9fb93cd7e166897edcf099e06d5b1582a32839d016b897a690e5ef141db79c94d8fd807312bb3d03ba867ceb905dbe0e31f92a3e4be4cc5cc1f91f'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.13.0]
4
+
5
+ - Introduce automatic subscriptions recovery from connection errors. This way if a subscription process loses the connection to the database - it will be trying to reconnect until the connection is restored.
6
+ - Resolve ambiguity in usage of `PgEventstore.config` method. It now returns the frozen object.
7
+
8
+ ## [1.12.0]
9
+
10
+ - Introduce `#read_grouped` API method that allows to group events by type
11
+
3
12
  ## [1.11.0]
4
13
 
5
14
  - Add a global position that caused an error to the subscription's error JSON info. This will help you understand what event caused your subscription to fail.
@@ -230,3 +230,101 @@ PgEventstore.client.read_paginated(projection_stream, options: { resolve_link_to
230
230
  end
231
231
  end
232
232
  ```
233
+
234
+ ## Grouping events by type
235
+
236
+ `pg_eventstore` implements an ability to group events by type when reading from a stream. Example:
237
+
238
+ ```ruby
239
+ stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'Foo', stream_id: '1')
240
+ event1 = PgEventstore::Event.new(type: 'Foo', data: { foo: 1 })
241
+ event2 = PgEventstore::Event.new(type: 'Foo', data: { foo: 2 })
242
+ event3 = PgEventstore::Event.new(type: 'Bar', data: { bar: 2 })
243
+
244
+ PgEventstore.client.append_to_stream(stream, [event1, event2, event3])
245
+
246
+ PgEventstore.client.read_grouped(stream) # => returns event1 and event3
247
+ ```
248
+
249
+ API is very similar to the API of `#read`, but it ignores `:max_count` options as the result is always returns a set of event types in your stream.
250
+
251
+ Reading most recent events:
252
+
253
+ ```ruby
254
+ PgEventstore.client.read_grouped(stream, options: { direction: :desc })
255
+ ```
256
+
257
+ Filtering the result by stream attributes:
258
+
259
+ ```ruby
260
+ PgEventstore.client.read_grouped(
261
+ PgEventstore::Stream.all_stream,
262
+ options: { filter: { streams: [{ context: 'FooCtx' }] } }
263
+ )
264
+ ```
265
+
266
+ Filtering the result by event types:
267
+
268
+ ```ruby
269
+ PgEventstore.client.read_grouped(
270
+ PgEventstore::Stream.all_stream,
271
+ options: { filter: { event_types: ['Foo', 'Bar'] } }
272
+ )
273
+ ```
274
+
275
+ Filtering by stream attributes and event types:
276
+
277
+ ```ruby
278
+ PgEventstore.client.read_grouped(
279
+ PgEventstore::Stream.all_stream,
280
+ options: { filter: { streams: [{ context: 'FooCtx' }], event_types: ['Foo', 'Bar'] } }
281
+ )
282
+ ```
283
+
284
+ Reading most recent events until the certain stream revision:
285
+
286
+ ```ruby
287
+ PgEventstore.client.read_grouped(stream, options: { direction: :desc, from_revision: 1 })
288
+ ```
289
+
290
+ Reading the oldest events from the certain stream revision:
291
+
292
+ ```ruby
293
+ PgEventstore.client.read_grouped(stream, options: { direction: :asc, from_revision: 1 })
294
+ ```
295
+
296
+ Reading most recent events until the certain global position:
297
+
298
+ ```ruby
299
+ PgEventstore.client.read_grouped(PgEventstore::Stream.all_stream, options: { direction: :desc, from_position: 5 })
300
+ ```
301
+
302
+ Reading the oldest events from the certain global position:
303
+
304
+ ```ruby
305
+ PgEventstore.client.read_grouped(PgEventstore::Stream.all_stream, options: { direction: :asc, from_position: 5 })
306
+ ```
307
+
308
+ ### Event types list lookup
309
+
310
+ If you do not provide event types filter - event types list will be determined based on the rest of arguments(a stream argument or a stream filters option).
311
+
312
+ ### Multiple events of same type in the result
313
+
314
+ If same event type appear in different streams(different by `#context` and `#stream_name`) - those events will appear in the result. This is because even though `Event#type` value may be the same - its meaning may have different meaning in different `context`/`stream_name` couple. Example:
315
+
316
+ ```ruby
317
+ stream1 = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'Foo', stream_id: '1')
318
+ stream2 = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'Bar', stream_id: '1')
319
+
320
+ event1 = PgEventstore::Event.new(type: 'Foo', data: { foo: 1 })
321
+ event2 = PgEventstore::Event.new(type: 'Foo', data: { foo: 2 })
322
+
323
+ PgEventstore.client.append_to_stream(stream1, event1)
324
+ PgEventstore.client.append_to_stream(stream2, event2)
325
+
326
+ PgEventstore.client.read_grouped(
327
+ PgEventstore::Stream.all_stream,
328
+ options: { filter: { streams: [{ context: 'FooCtx' }] } }
329
+ ) # => returns both events even though they are of "Foo" type
330
+ ```
@@ -3,7 +3,9 @@
3
3
  module PgEventstore
4
4
  module CLI
5
5
  module Commands
6
+ # @!visibility private
6
7
  class BaseCommand
8
+ # @!visibility private
7
9
  module BaseCommandActions
8
10
  # @return [Integer] exit code
9
11
  def call
@@ -4,6 +4,7 @@ module PgEventstore
4
4
  module CLI
5
5
  module Commands
6
6
  module CallbackHandlers
7
+ # @!visibility private
7
8
  class StartCmdHandlers
8
9
  include Extensions::CallbackHandlersExtension
9
10
 
@@ -3,6 +3,7 @@
3
3
  module PgEventstore
4
4
  module CLI
5
5
  module Commands
6
+ # @!visibility private
6
7
  class HelpCommand
7
8
  attr_reader :options
8
9
 
@@ -5,6 +5,7 @@ require_relative 'callback_handlers/start_cmd_handlers'
5
5
  module PgEventstore
6
6
  module CLI
7
7
  module Commands
8
+ # @!visibility private
8
9
  class StartSubscriptionsCommand < BaseCommand
9
10
  # @return [Integer] seconds
10
11
  KEEP_ALIVE_INTERVAL = 2
@@ -55,8 +56,9 @@ module PgEventstore
55
56
  manager.stop
56
57
  end
57
58
  end.each(&:join)
58
- Utils.remove_file(options.pid_path)
59
+ ensure
59
60
  @running = false
61
+ Utils.remove_file(options.pid_path)
60
62
  end
61
63
  end
62
64
 
@@ -3,6 +3,7 @@
3
3
  module PgEventstore
4
4
  module CLI
5
5
  module Commands
6
+ # @!visibility private
6
7
  class StopSubscriptionsCommand < BaseCommand
7
8
  # @return [Integer] exit code
8
9
  def call
@@ -2,6 +2,7 @@
2
2
 
3
3
  module PgEventstore
4
4
  module CLI
5
+ # @!visibility private
5
6
  module ExitCodes
6
7
  # @return [Integer]
7
8
  SUCCESS = 0
@@ -3,6 +3,7 @@
3
3
  module PgEventstore
4
4
  module CLI
5
5
  module ParserOptions
6
+ # @!visibility private
6
7
  class BaseOptions
7
8
  include Extensions::OptionsExtension
8
9
 
@@ -3,6 +3,7 @@
3
3
  module PgEventstore
4
4
  module CLI
5
5
  module ParserOptions
6
+ # @!visibility private
6
7
  class DefaultOptions < BaseOptions
7
8
  end
8
9
  end
@@ -3,6 +3,7 @@
3
3
  module PgEventstore
4
4
  module CLI
5
5
  module ParserOptions
6
+ # @!visibility private
6
7
  class Metadata
7
8
  include Extensions::OptionsExtension
8
9
 
@@ -3,6 +3,7 @@
3
3
  module PgEventstore
4
4
  module CLI
5
5
  module ParserOptions
6
+ # @!visibility private
6
7
  class SubscriptionOptions < BaseOptions
7
8
  option(
8
9
  :pid_path,
@@ -2,6 +2,7 @@
2
2
 
3
3
  module PgEventstore
4
4
  module CLI
5
+ # @!visibility private
5
6
  class TryToDeleteSubscriptionsSet
6
7
  class << self
7
8
  def try_to_delete(...)
@@ -30,7 +31,7 @@ module PgEventstore
30
31
  # Potentially CommandsHandler can be dead exactly at the same moment we expect it to process "Ping" command.
31
32
  # Wait for potential recover plus run interval and plus another second to allow potential processing of
32
33
  # "Ping" command. "Ping" command comes in prio, so it is guaranteed it will be processed as a first command.
33
- sleep CommandsHandler::RESTART_DELAY + CommandsHandler::PULL_INTERVAL + 1
34
+ sleep RunnerRecoveryStrategies::RestoreConnection::TIME_BETWEEN_RETRIES + CommandsHandler::PULL_INTERVAL + 1
34
35
  if subscriptions_set_commands_queries.find_by(subscriptions_set_id: subscriptions_set_id, command_name: cmd_name)
35
36
  # "Ping" command wasn't consumed. Related process must be dead.
36
37
  subscriptions_set_queries.delete(subscriptions_set_id)
@@ -5,6 +5,7 @@ require_relative 'wait_for_subscriptions_set_shutdown'
5
5
 
6
6
  module PgEventstore
7
7
  module CLI
8
+ # @!visibility private
8
9
  class TryUnlockSubscriptionsSet
9
10
  class << self
10
11
  def try_unlock(...)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module PgEventstore
4
4
  module CLI
5
+ # @!visibility private
5
6
  class WaitForSubscriptionsSetShutdown
6
7
  class << self
7
8
  def wait_for_shutdown(...)
@@ -55,10 +55,11 @@ module PgEventstore
55
55
  # Read events from the specific stream or from "all" stream.
56
56
  # @param stream [PgEventstore::Stream]
57
57
  # @param options [Hash] request options
58
- # @option options [String] :direction read direction - 'Forwards' or 'Backwards'
58
+ # @option options [String] :direction read direction. Allowed values are "Forwards", "Backwards", "asc", "desc",
59
+ # :asc, :desc
59
60
  # @option options [Integer] :from_revision a starting revision number. **Use this option when stream name is a
60
61
  # normal stream name**
61
- # @option options [Integer, Symbol] :from_position a starting global position number. **Use this option when reading
62
+ # @option options [Integer] :from_position a starting global position number. **Use this option when reading
62
63
  # from "all" stream**
63
64
  # @option options [Integer] :max_count max number of events to return in one response. Defaults to config.max_count
64
65
  # @option options [Boolean] :resolve_link_tos When using projections to create new events you
@@ -94,7 +95,7 @@ module PgEventstore
94
95
  # }
95
96
  # )
96
97
  #
97
- # # Filtering the a mix of context and event type
98
+ # # Filtering a mix of context and event type
98
99
  # PgEventstore.client.read(
99
100
  # PgEventstore::Stream.all_stream,
100
101
  # options: { filter: { streams: [{ context: 'User' }], event_types: ['MyAwesomeEvent'] } }
@@ -123,6 +124,23 @@ module PgEventstore
123
124
  call(stream, options: { max_count: config.max_count }.merge(options))
124
125
  end
125
126
 
127
+ # Takes a stream, determines a list of even types in it and returns most recent(or very first - depending on
128
+ # :direction option) events, one of each type. If :event_types filter is provided - uses it instead of automatic
129
+ # event types lookup logic. The result size is almost always less than or equal to event types list size, so passing
130
+ # :max_count option does not make any effect. In case if event of same type appears in different context/stream
131
+ # name - it will be counted as a different event, thus, may appear several times in the result.
132
+ # @see {#read} for the detailed docs
133
+ # @param stream [PgEventstore::Stream]
134
+ # @param options [Hash] request options
135
+ # @param middlewares [Array, nil]
136
+ # @return [Array<PgEventstore::Event>]
137
+ def read_grouped(stream, options: {}, middlewares: nil)
138
+ cmd_class = stream.all_stream? ? Commands::AllStreamReadGrouped : Commands::RegularStreamReadGrouped
139
+ cmd_class.
140
+ new(Queries.new(partitions: partition_queries, events: event_queries(middlewares(middlewares)))).
141
+ call(stream, options: options)
142
+ end
143
+
126
144
  # Links event from one stream into another stream. You can later access it by providing :resolve_link_tos option
127
145
  # when reading from a stream. Only existing events can be linked.
128
146
  # @param stream [PgEventstore::Stream]
@@ -149,7 +167,7 @@ module PgEventstore
149
167
  private
150
168
 
151
169
  # @param middlewares [Array, nil]
152
- # @return [Array<Object<#serialize, #deserialize>>]
170
+ # @return [Array<PgEventstore::Middleware>]
153
171
  def middlewares(middlewares = nil)
154
172
  return config.middlewares.values unless middlewares
155
173
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class AllStreamReadGrouped < AbstractCommand
7
+ # @param stream [PgEventstore::Stream]
8
+ # @param options [Hash] request options
9
+ # @option options [String] :direction read direction
10
+ # @option options [Integer, Symbol] :from_position. **Use this option when reading from "all" stream**
11
+ # @option options [Boolean] :resolve_link_tos
12
+ # @option options [Hash] :filter provide it to filter events
13
+ # @return [Array<PgEventstore::Event>]
14
+ # @raise [PgEventstore::StreamNotFoundError]
15
+ def call(stream, options: {})
16
+ event_types = QueryBuilders::PartitionsFiltering.extract_event_types_filter(options)
17
+ stream_filters = QueryBuilders::PartitionsFiltering.extract_streams_filter(options)
18
+ stream_ids_grouped = group_stream_ids(options)
19
+ options_by_event_type =
20
+ queries.partitions.partitions(stream_filters, event_types).flat_map do |partition|
21
+ stream_ids = stream_ids_grouped[[partition.context, partition.stream_name]]
22
+ next build_filter_options_for_streams(partition, stream_ids, options) if stream_ids
23
+
24
+ build_filter_options_for_partitions(partition, options)
25
+ end
26
+ queries.events.grouped_events(stream, options_by_event_type, **options.slice(:resolve_link_tos))
27
+ end
28
+
29
+ private
30
+
31
+ # @param options [Hash]
32
+ # @return [Hash]
33
+ def group_stream_ids(options)
34
+ event_stream_filters = QueryBuilders::EventsFiltering.extract_streams_filter(options)
35
+ event_stream_filters.each_with_object({}) do |attrs, res|
36
+ next unless attrs[:stream_id]
37
+
38
+ res[[attrs[:context], attrs[:stream_name]]] ||= []
39
+ res[[attrs[:context], attrs[:stream_name]]].push(attrs[:stream_id])
40
+ end
41
+ end
42
+
43
+ # @param partition [PgEventstore::Partition]
44
+ # @param stream_ids [Array<String>]
45
+ # @param options [Hash]
46
+ # @return [Array<Hash>]
47
+ def build_filter_options_for_streams(partition, stream_ids, options)
48
+ stream_ids.map do |stream_id|
49
+ filter = {
50
+ streams: [{ context: partition.context, stream_name: partition.stream_name, stream_id: stream_id }],
51
+ event_types: [partition.event_type]
52
+ }
53
+ options.merge(filter: filter, max_count: 1)
54
+ end
55
+ end
56
+
57
+ # @param partition [PgEventstore::Partition]
58
+ # @param options [Hash]
59
+ # @return [Hash]
60
+ def build_filter_options_for_partitions(partition, options)
61
+ filter = {
62
+ streams: [{ context: partition.context, stream_name: partition.stream_name }],
63
+ event_types: [partition.event_type]
64
+ }
65
+ options.merge(filter: filter, max_count: 1)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class RegularStreamReadGrouped < AbstractCommand
7
+ # @param stream [PgEventstore::Stream]
8
+ # @param options [Hash] request options
9
+ # @option options [String] :direction read direction
10
+ # @option options [Integer, Symbol] :from_revision. **Use this option when stream name is a normal stream name**
11
+ # @option options [Integer, Symbol] :from_position. **Use this option when reading from "all" stream**
12
+ # @option options [Boolean] :resolve_link_tos
13
+ # @option options [Hash] :filter provide it to filter events
14
+ # @return [Array<PgEventstore::Event>]
15
+ # @raise [PgEventstore::StreamNotFoundError]
16
+ def call(stream, options: {})
17
+ queries.events.stream_revision(stream) || raise(StreamNotFoundError, stream)
18
+
19
+ event_types = QueryBuilders::PartitionsFiltering.extract_event_types_filter(options)
20
+ stream_filters = QueryBuilders::PartitionsFiltering.extract_streams_filter(
21
+ filter: { streams: [{ context: stream.context, stream_name: stream.stream_name }] }
22
+ )
23
+ options_by_event_type = queries.partitions.partitions(stream_filters, event_types).map do |partition|
24
+ filter = { event_types: [partition.event_type] }
25
+ options.merge(filter: filter, max_count: 1)
26
+ end
27
+ queries.events.grouped_events(stream, options_by_event_type, **options.slice(:resolve_link_tos))
28
+ end
29
+ end
30
+ end
31
+ end
@@ -5,6 +5,8 @@ require_relative 'commands/event_modifiers/prepare_link_event'
5
5
  require_relative 'commands/event_modifiers/prepare_regular_event'
6
6
  require_relative 'commands/append'
7
7
  require_relative 'commands/read'
8
+ require_relative 'commands/regular_stream_read_grouped'
9
+ require_relative 'commands/all_stream_read_grouped'
8
10
  require_relative 'commands/regular_stream_read_paginated'
9
11
  require_relative 'commands/system_stream_read_paginated'
10
12
  require_relative 'commands/multiple'
@@ -67,9 +67,9 @@ module PgEventstore
67
67
  should_retry = true
68
68
  @pool.with do |conn|
69
69
  yield conn
70
- rescue PG::ConnectionBad
71
- # Recover connection after fork. We do it only once and without any delay. Recover is required by both
72
- # processes - child process and parent process
70
+ rescue PG::ConnectionBad, PG::UnableToSend
71
+ # Recover a connection after fork or when we lost a connection to PostgreSQL. We retry only once and without any
72
+ # delay.
73
73
  conn.sync_reset
74
74
  raise unless should_retry
75
75
  should_retry = false
@@ -77,6 +77,11 @@ module PgEventstore
77
77
  end
78
78
  end
79
79
 
80
+ # @return [void]
81
+ def shutdown
82
+ @pool.shutdown(&:close)
83
+ end
84
+
80
85
  private
81
86
 
82
87
  # @return [ConnectionPool]
@@ -2,6 +2,7 @@
2
2
 
3
3
  module PgEventstore
4
4
  module Extensions
5
+ # @!visibility private
5
6
  module CallbackHandlersExtension
6
7
  def self.included(klass)
7
8
  klass.extend(ClassMethods)
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ class Partition
5
+ include Extensions::OptionsExtension
6
+
7
+ # @!attribute id
8
+ # @return [Integer]
9
+ option(:id)
10
+ # @!attribute context
11
+ # @return [String]
12
+ option(:context)
13
+ # @!attribute stream_name
14
+ # @return [String, nil]
15
+ option(:stream_name)
16
+ # @!attribute event_type
17
+ # @return [String, nil]
18
+ option(:event_type)
19
+ # @!attribute table_name
20
+ # @return [String]
21
+ option(:table_name)
22
+ end
23
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgEventstore
4
+ # @!visibility private
4
5
  class PgConnection < PG::Connection
5
6
  def exec(sql)
6
7
  log(sql, [])
@@ -98,6 +98,24 @@ module PgEventstore
98
98
  end
99
99
  end
100
100
 
101
+ # @param stream [PgEventstore::Stream]
102
+ # @param options_by_event_type [Array<Hash>] a set of options per an event type
103
+ # @param options [Hash]
104
+ # @option options [Boolean] :resolve_link_tos
105
+ # @return [Array<PgEventstore::Event>]
106
+ def grouped_events(stream, options_by_event_type, **options)
107
+ builders = options_by_event_type.map do |filter|
108
+ QueryBuilders::EventsFiltering.events_filtering(stream, filter)
109
+ end
110
+ final_builder = SQLBuilder.union_builders(builders.map(&:to_sql_builder))
111
+
112
+ raw_events = connection.with do |conn|
113
+ conn.exec_params(*final_builder.to_exec_params)
114
+ end.to_a
115
+ raw_events = links_resolver.resolve(raw_events) if options[:resolve_link_tos]
116
+ deserializer.deserialize_many(raw_events)
117
+ end
118
+
101
119
  private
102
120
 
103
121
  # @param stream [PgEventstore::Stream]
@@ -176,6 +176,19 @@ module PgEventstore
176
176
  end.to_a
177
177
  end
178
178
 
179
+ # @param stream_filters [Array<Hash[Symbol, String]>]
180
+ # @param event_filters [Array<String>]
181
+ # @return [Array<PgEventstore::Partition>]
182
+ def partitions(stream_filters, event_filters)
183
+ partitions_filter = QueryBuilders::PartitionsFiltering.new
184
+ stream_filters.each { |attrs| partitions_filter.add_stream_attrs(**attrs) }
185
+ partitions_filter.add_event_types(event_filters)
186
+ partitions_filter.with_event_types
187
+ connection.with do |conn|
188
+ conn.exec_params(*partitions_filter.to_exec_params)
189
+ end.map(&method(:deserialize))
190
+ end
191
+
179
192
  # @param stream [PgEventstore::Stream]
180
193
  # @return [String]
181
194
  def context_partition_name(stream)
@@ -194,5 +207,13 @@ module PgEventstore
194
207
  def event_type_partition_name(stream, event_type)
195
208
  "event_types_#{Digest::MD5.hexdigest("#{stream.context}-#{stream.stream_name}-#{event_type}")[0..5]}"
196
209
  end
210
+
211
+ private
212
+
213
+ # @param attrs [Hash]
214
+ # @return [PgEventstore::Partition]
215
+ def deserialize(attrs)
216
+ Partition.new(**attrs.transform_keys(&:to_sym))
217
+ end
197
218
  end
198
219
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'sql_builder'
4
+ require_relative 'query_builders/basic_filtering'
4
5
  require_relative 'query_builders/events_filtering'
6
+ require_relative 'query_builders/partitions_filtering'
5
7
  require_relative 'queries/transaction_queries'
6
8
  require_relative 'queries/event_queries'
7
9
  require_relative 'queries/partition_queries'
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module QueryBuilders
5
+ # @!visibility private
6
+ class BasicFiltering
7
+ def initialize
8
+ @sql_builder = SQLBuilder.new.select("#{to_table_name}.*").from(to_table_name)
9
+ end
10
+
11
+ # @return [String]
12
+ def to_table_name
13
+ raise NotImplementedError
14
+ end
15
+
16
+ # @return [PgEventstore::SQLBuilder]
17
+ def to_sql_builder
18
+ @sql_builder
19
+ end
20
+
21
+ # @return [Array]
22
+ def to_exec_params
23
+ @sql_builder.to_exec_params
24
+ end
25
+ end
26
+ end
27
+ end