pg_eventstore 0.4.0 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 256d7be42cb993955eb3c387b27fa63bf9992852da2375c785f181b873fe7568
4
- data.tar.gz: 4101d8f0706f402a8e97bce4864c2ab2c61a587845e1051826d7d26417c199bd
3
+ metadata.gz: 6af4900fd9dc0524bcc7abcf7becd913c33fc50e1f0cb097c124ab48ee9cf3cd
4
+ data.tar.gz: c5626c8b58e6265b7deed496b6531881ea352049cbaea6f29b2281abd7a7d5b9
5
5
  SHA512:
6
- metadata.gz: 548512c6c8ec7161380848b1975313222173dd41db13d45c0216948bf769eb513d8593dc0e22263c4cc59c9210a7c90fab21f7a64b26a38217253422457582b4
7
- data.tar.gz: 777aecbd9e7ad84882fbf0c20847fc79e8495addac77a5887db3dface85b7dec7aa3679e91849272109f6f6745cdbfd143628d8dc53d78b73559ba9fad31722f
6
+ metadata.gz: 6c4c6425faea1168e3396b14c7cd66fb3c7f10097d2e2645ebf7dba6ba2b9800ddb6372bac3f6c8d39baf4c78a46bbfc907b352274b3839ef5eeb7acd8dbec02
7
+ data.tar.gz: '09a4f239984bffc88abaa5e08aa4c4fa31df1856aebcb4c8b5fcd6bdd5e77806d556584680cdc5702cfd5f720bc5f91ecf19bdc98bdf15c4a16f3024e5df0151'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.5.2] - 2024-02-06
4
+
5
+ - Improve speed of `PgEventstore::Stream#eql?` a bit
6
+
7
+ ## [0.5.1] - 2024-02-06
8
+
9
+ - Fix `PgEventstore::Stream` to be properly recognizable inside Hash
10
+
11
+ ## [0.5.0] - 2024-02-05
12
+
13
+ - Fix event class resolving when providing `resolve_link_tos: true` option
14
+ - Return correct stream revision of the `Event#stream` object of the appended event
15
+ - Implement events linking feature
16
+ - Implement paginated read
17
+ - Remove duplicated `idx_events_event_type_id` index
18
+
1
19
  ## [0.4.0] - 2024-01-29
2
20
 
3
21
  - Implement asynchronous subscriptions. Refer to the documentation for more info
data/CODE_OF_CONDUCT.md CHANGED
@@ -39,7 +39,7 @@ This Code of Conduct applies within all community spaces, and also applies when
39
39
 
40
40
  ## Enforcement
41
41
 
42
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at intale.a@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at ivan.dzyzenko@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
43
 
44
44
  All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
45
 
data/README.md CHANGED
@@ -52,6 +52,7 @@ Documentation chapters:
52
52
  - [Configuration](docs/configuration.md)
53
53
  - [Events and streams definitions](docs/events_and_streams.md)
54
54
  - [Appending events](docs/appending_events.md)
55
+ - [Linking events](docs/linking_events.md)
55
56
  - [Reading events](docs/reading_events.md)
56
57
  - [Subscriptions](docs/subscriptions.md)
57
58
  - [Writing middlewares](docs/writing_middleware.md)
@@ -0,0 +1 @@
1
+ DROP INDEX idx_events_event_type_id;
@@ -0,0 +1,96 @@
1
+ # Linking Events
2
+
3
+ ## Linking single event
4
+
5
+ You can create a link to an existing event. Next example demonstrates how you can link an existing event from another stream:
6
+
7
+ ```ruby
8
+ class SomethingHappened < PgEventstore::Event
9
+ end
10
+
11
+ event = SomethingHappened.new(
12
+ type: 'some-event', data: { title: 'Something happened' }
13
+ )
14
+
15
+ events_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeStream', stream_id: '1')
16
+ projection_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeProjection', stream_id: '1')
17
+ # Persist our event
18
+ event = PgEventstore.client.append_to_stream(events_stream, event)
19
+
20
+ # Link event into your projection
21
+ PgEventstore.client.link_to(projection_stream, event)
22
+ ```
23
+
24
+ The linked event can later be fetched by providing the `:resolve_link_tos` option when reading from the stream:
25
+
26
+ ```ruby
27
+ projection_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeProjection', stream_id: '1')
28
+ PgEventstore.client.read(projection_stream, options: { resolve_link_tos: true })
29
+ ```
30
+
31
+ If you don't provide the `:resolve_link_tos` option, the linked event will be returned instead of the original one.
32
+
33
+ ## Linking multiple events
34
+
35
+ You can provide an array of events to link to the target stream:
36
+
37
+ ```ruby
38
+ class SomethingHappened < PgEventstore::Event
39
+ end
40
+
41
+ events = 3.times.map { |i| SomethingHappened.new(type: 'some-event', data: { title: "Something happened-#{i}" }) }
42
+ events_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeStream', stream_id: '1')
43
+ projection_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeProjection', stream_id: '1')
44
+ events = PgEventstore.client.append_to_stream(events_stream, events)
45
+ # Link events
46
+ PgEventstore.client.link_to(projection_stream, events)
47
+ ```
48
+
49
+ ## Handling concurrency
50
+
51
+ Linking concurrency is implemented the same way as appending concurrency. You can check [**Handling concurrency**](appending_events.md#handling-concurrency) chapter of **Appending Events** section.
52
+
53
+ Example:
54
+
55
+ ```ruby
56
+ require 'securerandom'
57
+ class SomethingHappened < PgEventstore::Event
58
+ end
59
+
60
+ event1 = SomethingHappened.new
61
+ event2 = SomethingHappened.new
62
+
63
+ events_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeStream', stream_id: '1')
64
+ projection_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeProjection', stream_id: SecureRandom.uuid)
65
+
66
+ event1, event2 = PgEventstore.client.append_to_stream(events_stream, [event1, event2])
67
+
68
+ # Links first event
69
+ PgEventstore.client.link_to(projection_stream, event1, options: { expected_revision: :no_stream })
70
+ # Raises PgEventstore::WrongExpectedVersionError error because stream already exists
71
+ PgEventstore.client.link_to(projection_stream, event2, options: { expected_revision: :no_stream })
72
+ ```
73
+
74
+ ## Middlewares
75
+
76
+ If you would like to modify your link events before they get persisted - you should use the `:middlewares` argument which allows you to pass the list of middlewares you would like to use. **By default no middlewares will be applied to the link event despite on `config.middlewares` option**.
77
+
78
+ Let's say you have these registered middlewares:
79
+
80
+ ```ruby
81
+ PgEventstore.configure do |config|
82
+ config.middlewares = { foo: FooMiddleware.new, bar: BarMiddleware.new, baz: BazMiddleware.new }
83
+ end
84
+ ```
85
+
86
+ And you want to use `FooMiddleware` and `BazMiddleware`. You simply have to provide an array of corresponding middleware keys you would like to use:
87
+
88
+ ```ruby
89
+ events_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeStream', stream_id: '1')
90
+ projection_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeProjection', stream_id: '1')
91
+
92
+ event = PgEventstore.client.append_to_stream(events_stream, PgEventstore::Event.new)
93
+ PgEventstore.client.link_to(projection_stream, event, middlewares: %i[foo baz])
94
+ ```
95
+
96
+ See [Writing middleware](writing_middleware.md) chapter for info about what is middleware and how to implement it.
@@ -159,3 +159,59 @@ You can also mix filtering by stream's attributes and event types. The result wi
159
159
  ```ruby
160
160
  PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { filter: { streams: [{ context: 'MyAwesomeContext' }], event_types: %w[Foo Bar] } })
161
161
  ```
162
+
163
+
164
+ ## Pagination
165
+
166
+ You can use `#read_paginated` to iterate over all (filtered) events. It yields each batch of records that was found according to the filter options:
167
+
168
+ ```ruby
169
+ # Read from the specific stream
170
+ stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
171
+ PgEventstore.client.read_paginated(stream).each do |events|
172
+ events.each do |event|
173
+ # iterate through events
174
+ end
175
+ end
176
+
177
+ # Read from "all" stream
178
+ PgEventstore.client.read_paginated(PgEventstore::Stream.all_stream).each do |events|
179
+ events.each do |event|
180
+ # iterate through events
181
+ end
182
+ end
183
+ ```
184
+
185
+ Options are the same as for `#read` method. Several examples:
186
+
187
+ ```ruby
188
+ # Read "Foo" events only from the specific stream
189
+ stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
190
+ PgEventstore.client.read_paginated(stream, options: { filter: { event_types: ['Foo'] } }).each do |events|
191
+ events.each do |event|
192
+ # iterate through events
193
+ end
194
+ end
195
+
196
+ # Backwards read from "all" stream
197
+ PgEventstore.client.read_paginated(PgEventstore::Stream.all_stream, options: { direction: 'Backwards' }).each do |events|
198
+ events.each do |event|
199
+ # iterate through events
200
+ end
201
+ end
202
+
203
+ # Set batch size to 100
204
+ PgEventstore.client.read_paginated(PgEventstore::Stream.all_stream, options: { max_count: 100 }).each do |events|
205
+ events.each do |event|
206
+ # iterate through events
207
+ end
208
+ end
209
+
210
+ # Reading from projection stream
211
+ projection_stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'MyAwesomeProjection', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
212
+ PgEventstore.client.read_paginated(projection_stream, options: { resolve_link_tos: true }).each do |events|
213
+ events.each do |event|
214
+ # iterate through events
215
+ end
216
+ end
217
+ ```
@@ -109,6 +109,36 @@ module PgEventstore
109
109
  call(stream, options: { max_count: config.max_count }.merge(options))
110
110
  end
111
111
 
112
+ # @see {#read} for available params
113
+ # @return [Enumerator] enumerator will yield PgEventstore::Event
114
+ def read_paginated(stream, options: {}, middlewares: nil)
115
+ cmd_class = stream.system? ? Commands::SystemStreamReadPaginated : Commands::RegularStreamReadPaginated
116
+ cmd_class.
117
+ new(Queries.new(streams: stream_queries, events: event_queries(middlewares(middlewares)))).
118
+ call(stream, options: { max_count: config.max_count }.merge(options))
119
+ end
120
+
121
+ # Links event from one stream into another stream. You can later access it by providing :resolve_link_tos option
122
+ # when reading from a stream. Only existing events can be linked.
123
+ # @param stream [PgEventstore::Stream]
124
+ # @param events_or_event [PgEventstore::Event, Array<PgEventstore::Event>]
125
+ # @param options [Hash]
126
+ # @option options [Integer] :expected_revision provide your own revision number
127
+ # @option options [Symbol] :expected_revision provide one of next values: :any, :no_stream or :stream_exists
128
+ # @param middlewares [Array] provide a list of middleware names to use. Defaults to empty array, meaning no
129
+ # middlewares will be applied to the "link" event
130
+ # @return [PgEventstore::Event, Array<PgEventstore::Event>] persisted event(s)
131
+ # @raise [PgEventstore::WrongExpectedRevisionError]
132
+ def link_to(stream, events_or_event, options: {}, middlewares: [])
133
+ result =
134
+ Commands::LinkTo.new(
135
+ Queries.new(
136
+ streams: stream_queries, events: event_queries(middlewares(middlewares)), transactions: transaction_queries
137
+ )
138
+ ).call(stream, *events_or_event, options: options)
139
+ events_or_event.is_a?(Array) ? result : result.first
140
+ end
141
+
112
142
  private
113
143
 
114
144
  # @param middlewares [Array, nil]
@@ -9,9 +9,10 @@ module PgEventstore
9
9
  # @param options [Hash]
10
10
  # @option options [Integer] :expected_revision provide your own revision number
11
11
  # @option options [Symbol] :expected_revision provide one of next values: :any, :no_stream or :stream_exists
12
+ # @param event_modifier [#call]
12
13
  # @return [Array<PgEventstore::Event>] persisted events
13
14
  # @raise [PgEventstore::WrongExpectedRevisionError]
14
- def call(stream, *events, options: {})
15
+ def call(stream, *events, options: {}, event_modifier: EventModifiers::PrepareRegularEvent)
15
16
  raise SystemStreamError, stream if stream.system?
16
17
 
17
18
  queries.transactions.transaction do
@@ -19,7 +20,7 @@ module PgEventstore
19
20
  revision = stream.stream_revision
20
21
  assert_expected_revision!(revision, options[:expected_revision]) if options[:expected_revision]
21
22
  events.map.with_index(1) do |event, index|
22
- queries.events.insert(stream, prepared_event(event, revision + index))
23
+ queries.events.insert(stream, event_modifier.call(event, revision + index))
23
24
  end.tap do
24
25
  queries.streams.update_stream_revision(stream, revision + events.size)
25
26
  end
@@ -28,15 +29,6 @@ module PgEventstore
28
29
 
29
30
  private
30
31
 
31
- # @param event [PgEventstore::Event]
32
- # @param revision [Integer]
33
- # @return [PgEventstore::Event]
34
- def prepared_event(event, revision)
35
- event.class.new(
36
- id: event.id, data: event.data, metadata: event.metadata, type: event.type, stream_revision: revision
37
- )
38
- end
39
-
40
32
  # @param revision [Integer]
41
33
  # @param expected_revision [Symbol, Integer]
42
34
  # @raise [PgEventstore::WrongExpectedRevisionError] in case if revision does not satisfy expected revision
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ module EventModifiers
6
+ # Defines how to transform regular event into a link event
7
+ # @!visibility private
8
+ class PrepareLinkEvent
9
+ class << self
10
+ # @param event [PgEventstore::Event]
11
+ # @param revision [Integer]
12
+ # @return [PgEventstore::Event]
13
+ def call(event, revision)
14
+ Event.new(link_id: event.id, type: Event::LINK_TYPE, stream_revision: revision).tap do |e|
15
+ %i[link_id type stream_revision].each { |attr| e.readonly!(attr) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ module EventModifiers
6
+ # Defines how to transform regular event before appending it to the stream
7
+ # @!visibility private
8
+ class PrepareRegularEvent
9
+ class << self
10
+ # @param event [PgEventstore::Event]
11
+ # @param revision [Integer]
12
+ # @return [PgEventstore::Event]
13
+ def call(event, revision)
14
+ event.class.new(
15
+ id: event.id, data: event.data, metadata: event.metadata, type: event.type, stream_revision: revision
16
+ ).tap do |e|
17
+ %i[link_id stream_revision].each { |attr| e.readonly!(attr) }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class LinkTo < AbstractCommand
7
+ # @param stream [PgEventstore::Stream]
8
+ # @param events [Array<PgEventstore::Event>]
9
+ # @param options [Hash]
10
+ # @option options [Integer] :expected_revision provide your own revision number
11
+ # @option options [Symbol] :expected_revision provide one of next values: :any, :no_stream or :stream_exists
12
+ # @return [Array<PgEventstore::Event>] persisted events
13
+ # @raise [PgEventstore::WrongExpectedRevisionError]
14
+ # @raise [PgEventstore::NotPersistedEventError]
15
+ def call(stream, *events, options: {})
16
+ events.each(&method(:check_id_presence))
17
+ append_cmd = Append.new(queries)
18
+ append_cmd.call(stream, *events, options: options, event_modifier: EventModifiers::PrepareLinkEvent)
19
+ end
20
+
21
+ private
22
+
23
+ # Checks if Event#id is present. An event must have the #id value in order to be linked.
24
+ # @param event [PgEventstore::Event]
25
+ # @return [void]
26
+ def check_id_presence(event)
27
+ return unless event.id.nil?
28
+
29
+ raise NotPersistedEventError, event
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class RegularStreamReadPaginated < AbstractCommand
7
+ # @see PgEventstore::Commands::Read for docs
8
+ def call(stream, options: {})
9
+ revision = calc_initial_revision(stream, options)
10
+ Enumerator.new do |yielder|
11
+ loop do
12
+ events = read_cmd.call(stream, options: options.merge(from_revision: revision))
13
+ yielder << events if events.any?
14
+ raise StopIteration if end_reached?(events, options[:max_count])
15
+
16
+ revision = calc_next_revision(events, revision, options[:direction])
17
+ raise StopIteration if revision.negative?
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # @param stream [PgEventstore::Stream]
25
+ # @param options [Hash]
26
+ # @return [Integer]
27
+ def calc_initial_revision(stream, options)
28
+ return options[:from_revision] if options[:from_revision]
29
+ return 0 if forwards?(options[:direction])
30
+
31
+ read_cmd.call(stream, options: options.merge(max_count: 1)).first.stream_revision
32
+ end
33
+
34
+ # @param events [Array<PgEventstore::Event>]
35
+ # @param max_count [Integer]
36
+ # @return [Boolean]
37
+ def end_reached?(events, max_count)
38
+ events.size < max_count
39
+ end
40
+
41
+ # @param events [Array<PgEventstore::Event>]
42
+ # @param revision [Integer]
43
+ # @param direction [String, Symbol, nil]
44
+ # @return [Integer]
45
+ def calc_next_revision(events, revision, direction)
46
+ return revision + events.size if forwards?(direction)
47
+
48
+ revision - events.size
49
+ end
50
+
51
+ # @param direction [String, Symbol, nil]
52
+ # @return [Boolean]
53
+ def forwards?(direction)
54
+ QueryBuilders::EventsFiltering::SQL_DIRECTIONS[direction] == QueryBuilders::EventsFiltering::SQL_DIRECTIONS[:asc]
55
+ end
56
+
57
+ # @return [PgEventstore::Commands::Read]
58
+ def read_cmd
59
+ @read_cmd ||= Read.new(queries)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Commands
5
+ # @!visibility private
6
+ class SystemStreamReadPaginated < AbstractCommand
7
+ # @see PgEventstore::Commands::Read for docs
8
+ def call(stream, options: {})
9
+ position = calc_initial_position(stream, options)
10
+ Enumerator.new do |yielder|
11
+ loop do
12
+ events = read_cmd.call(stream, options: options.merge(from_position: position))
13
+ yielder << events if events.any?
14
+ raise StopIteration if end_reached?(events, options[:max_count])
15
+
16
+ position = calc_next_position(events, options[:direction])
17
+ raise StopIteration if position <= 0
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # @param stream [PgEventstore::Stream]
25
+ # @param options [Hash]
26
+ # @return [Integer]
27
+ def calc_initial_position(stream, options)
28
+ return options[:from_position] if options[:from_position]
29
+ return 1 if forwards?(options[:direction])
30
+
31
+ read_cmd.call(stream, options: options.merge(max_count: 1)).first.global_position
32
+ end
33
+
34
+ # @param events [Array<PgEventstore::Event>]
35
+ # @param max_count [Integer]
36
+ # @return [Boolean]
37
+ def end_reached?(events, max_count)
38
+ events.size < max_count
39
+ end
40
+
41
+ # @param events [Array<PgEventstore::Event>]
42
+ # @param direction [String, Symbol, nil]
43
+ # @return [Integer]
44
+ def calc_next_position(events, direction)
45
+ return events.last.global_position + 1 if forwards?(direction)
46
+
47
+ events.last.global_position - 1
48
+ end
49
+
50
+ # @param direction [String, Symbol, nil]
51
+ # @return [Boolean]
52
+ def forwards?(direction)
53
+ QueryBuilders::EventsFiltering::SQL_DIRECTIONS[direction] == QueryBuilders::EventsFiltering::SQL_DIRECTIONS[:asc]
54
+ end
55
+
56
+ # @return [PgEventstore::Commands::Read]
57
+ def read_cmd
58
+ @read_cmd ||= Read.new(queries)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'abstract_command'
4
+ require_relative 'commands/event_modifiers/prepare_link_event'
5
+ require_relative 'commands/event_modifiers/prepare_regular_event'
4
6
  require_relative 'commands/append'
5
7
  require_relative 'commands/read'
8
+ require_relative 'commands/regular_stream_read_paginated'
9
+ require_relative 'commands/system_stream_read_paginated'
6
10
  require_relative 'commands/multiple'
11
+ require_relative 'commands/link_to'
@@ -137,8 +137,8 @@ module PgEventstore
137
137
 
138
138
  # @return [String]
139
139
  def user_friendly_message
140
- <<~TEXT
141
- Could not lock Subscription from #{set.inspect} set with #{name.inspect} name. It is already locked by \
140
+ <<~TEXT.strip
141
+ Could not lock subscription from #{set.inspect} set with #{name.inspect} name. It is already locked by \
142
142
  #{lock_id.inspect} set.
143
143
  TEXT
144
144
  end
@@ -161,10 +161,27 @@ module PgEventstore
161
161
 
162
162
  # @return [String]
163
163
  def user_friendly_message
164
- <<~TEXT
165
- Failed to unlock Subscription from #{set.inspect} set with #{name.inspect} name by \
164
+ <<~TEXT.strip
165
+ Failed to unlock subscription from #{set.inspect} set with #{name.inspect} name by \
166
166
  #{expected_locked_by.inspect} lock id. It is currently locked by #{actual_locked_by.inspect} lock id.
167
167
  TEXT
168
168
  end
169
169
  end
170
+
171
+ class NotPersistedEventError < Error
172
+ attr_reader :event
173
+
174
+ # @param event [PgEventstore::Event]
175
+ def initialize(event)
176
+ @event = event
177
+ super(user_friendly_message)
178
+ end
179
+
180
+ # @return [String]
181
+ def user_friendly_message
182
+ <<~TEXT.strip
183
+ Event#id must be present, got #{event.id.inspect} instead.
184
+ TEXT
185
+ end
186
+ end
170
187
  end
@@ -12,18 +12,9 @@ module PgEventstore
12
12
  @event_class_resolver = event_class_resolver
13
13
  end
14
14
 
15
- # @param pg_result [PG::Result]
16
- # @return [Array<PgEventstore::Event>]
17
- def deserialize_pg_result(pg_result)
18
- pg_result.map(&method(:deserialize))
19
- end
20
-
21
- # @param pg_result [PG::Result]
22
- # @return [PgEventstore::Event, nil]
23
- def deserialize_one_pg_result(pg_result)
24
- return if pg_result.ntuples.zero?
25
-
26
- deserialize(pg_result.first)
15
+ # @param raw_events [Array<Hash>]
16
+ def deserialize_many(raw_events)
17
+ raw_events.map(&method(:deserialize))
27
18
  end
28
19
 
29
20
  # @param attrs [Hash]
@@ -50,7 +50,11 @@ module PgEventstore
50
50
  self.options = (options + Set.new([opt_name])).freeze
51
51
  warn_already_defined(opt_name)
52
52
  warn_already_defined(:"#{opt_name}=")
53
- 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
54
58
 
55
59
  define_method opt_name do
56
60
  result = instance_variable_get(:"@#{opt_name}")
@@ -77,6 +81,8 @@ module PgEventstore
77
81
  end
78
82
  end
79
83
 
84
+ ReadonlyAttributeError = Class.new(StandardError)
85
+
80
86
  def self.included(klass)
81
87
  klass.singleton_class.attr_accessor(:options)
82
88
  klass.options = Set.new.freeze
@@ -84,11 +90,8 @@ module PgEventstore
84
90
  end
85
91
 
86
92
  def initialize(**options)
87
- self.class.options.each do |option|
88
- # init default values of options
89
- value = options.key?(option) ? options[option] : public_send(option)
90
- public_send("#{option}=", value)
91
- end
93
+ @readonly = Set.new
94
+ init_default_values(options)
92
95
  end
93
96
 
94
97
  # Construct a hash from options, where key is the option's name and the value is option's
@@ -100,6 +103,41 @@ module PgEventstore
100
103
  end
101
104
  end
102
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
103
141
  end
104
142
  end
105
143
  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
@@ -22,10 +22,11 @@ module PgEventstore
22
22
  def stream_events(stream, options)
23
23
  options = event_type_queries.include_event_types_ids(options)
24
24
  exec_params = events_filtering(stream, options).to_exec_params
25
- pg_result = connection.with do |conn|
25
+ raw_events = connection.with do |conn|
26
26
  conn.exec_params(*exec_params)
27
- end
28
- deserializer.deserialize_pg_result(pg_result)
27
+ end.to_a
28
+ preloader.preload_related_objects(raw_events)
29
+ deserializer.deserialize_many(raw_events)
29
30
  end
30
31
 
31
32
  # @param stream [PgEventstore::Stream] persisted stream
@@ -44,10 +45,10 @@ module PgEventstore
44
45
  RETURNING *, $#{attributes.values.size + 1} as type
45
46
  SQL
46
47
 
47
- pg_result = connection.with do |conn|
48
+ raw_event = connection.with do |conn|
48
49
  conn.exec_params(sql, [*attributes.values, event.type])
49
- end
50
- deserializer.without_middlewares.deserialize_one_pg_result(pg_result).tap do |persisted_event|
50
+ end.to_a.first
51
+ deserializer.without_middlewares.deserialize(raw_event).tap do |persisted_event|
51
52
  persisted_event.stream = stream
52
53
  end
53
54
  end
@@ -56,17 +57,21 @@ module PgEventstore
56
57
 
57
58
  # @param stream [PgEventstore::Stream]
58
59
  # @param options [Hash]
59
- # @param offset [Integer]
60
60
  # @return [PgEventstore::EventsFilteringQuery]
61
- def events_filtering(stream, options, offset: 0)
62
- 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?
63
63
 
64
- QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options, offset: offset)
64
+ QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options)
65
65
  end
66
66
 
67
67
  # @return [PgEventstore::EventTypeQueries]
68
68
  def event_type_queries
69
69
  EventTypeQueries.new(connection)
70
70
  end
71
+
72
+ # @return [PgEventstore::Preloader]
73
+ def preloader
74
+ Preloader.new(connection)
75
+ end
71
76
  end
72
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)
@@ -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
@@ -79,9 +79,10 @@ module PgEventstore
79
79
  return [] if query_options.empty?
80
80
 
81
81
  final_builder = union_builders(query_options.map { |id, opts| query_builder(id, opts) })
82
- connection.with do |conn|
82
+ raw_events = connection.with do |conn|
83
83
  conn.exec_params(*final_builder.to_exec_params)
84
84
  end.to_a
85
+ preloader.preload_related_objects(raw_events)
85
86
  end
86
87
 
87
88
  # @param id [Integer] subscription's id
@@ -151,6 +152,11 @@ module PgEventstore
151
152
  EventTypeQueries.new(connection)
152
153
  end
153
154
 
155
+ # @return [PgEventstore::Preloader]
156
+ def preloader
157
+ Preloader.new(connection)
158
+ end
159
+
154
160
  # @param hash [Hash]
155
161
  # @return [Hash]
156
162
  def deserialize(hash)
@@ -10,6 +10,7 @@ require_relative 'queries/subscription_queries'
10
10
  require_relative 'queries/subscriptions_set_queries'
11
11
  require_relative 'queries/subscription_command_queries'
12
12
  require_relative 'queries/subscriptions_set_command_queries'
13
+ require_relative 'queries/preloader'
13
14
 
14
15
  module PgEventstore
15
16
  # @!visibility private
@@ -4,7 +4,6 @@ module PgEventstore
4
4
  module QueryBuilders
5
5
  # @!visibility private
6
6
  class EventsFiltering
7
- DEFAULT_OFFSET = 0
8
7
  DEFAULT_LIMIT = 1_000
9
8
  SQL_DIRECTIONS = {
10
9
  'asc' => 'ASC',
@@ -26,14 +25,12 @@ module PgEventstore
26
25
  end
27
26
 
28
27
  # @param options [Hash]
29
- # @param offset [Integer]
30
28
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
31
- def all_stream_filtering(options, offset: 0)
29
+ def all_stream_filtering(options)
32
30
  event_filter = new
33
31
  options in { filter: { event_type_ids: Array => event_type_ids } }
34
32
  event_filter.add_event_types(event_type_ids)
35
33
  event_filter.add_limit(options[:max_count])
36
- event_filter.add_offset(offset)
37
34
  event_filter.resolve_links(options[:resolve_link_tos])
38
35
  options in { filter: { streams: Array => streams } }
39
36
  streams&.each { |attrs| event_filter.add_stream_attrs(**attrs) }
@@ -44,14 +41,12 @@ module PgEventstore
44
41
 
45
42
  # @param stream [PgEventstore::Stream]
46
43
  # @param options [Hash]
47
- # @param offset [Integer]
48
44
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
49
- def specific_stream_filtering(stream, options, offset: 0)
45
+ def specific_stream_filtering(stream, options)
50
46
  event_filter = new
51
47
  options in { filter: { event_type_ids: Array => event_type_ids } }
52
48
  event_filter.add_event_types(event_type_ids)
53
49
  event_filter.add_limit(options[:max_count])
54
- event_filter.add_offset(offset)
55
50
  event_filter.resolve_links(options[:resolve_link_tos])
56
51
  event_filter.add_stream(stream)
57
52
  event_filter.add_revision(options[:from_revision], options[:direction])
@@ -64,13 +59,10 @@ module PgEventstore
64
59
  @sql_builder =
65
60
  SQLBuilder.new.
66
61
  select('events.*').
67
- select('row_to_json(streams.*) as stream').
68
- select('event_types.type as type').
69
62
  from('events').
70
63
  join('JOIN streams ON streams.id = events.stream_id').
71
64
  join('JOIN event_types ON event_types.id = events.event_type_id').
72
- limit(DEFAULT_LIMIT).
73
- offset(DEFAULT_OFFSET)
65
+ limit(DEFAULT_LIMIT)
74
66
  end
75
67
 
76
68
  # @param context [String, nil]
@@ -144,14 +136,6 @@ module PgEventstore
144
136
  @sql_builder.limit(limit)
145
137
  end
146
138
 
147
- # @param offset [Integer, nil]
148
- # @return [void]
149
- def add_offset(offset)
150
- return unless offset
151
-
152
- @sql_builder.offset(offset)
153
- end
154
-
155
139
  # @param should_resolve [Boolean]
156
140
  # @return [void]
157
141
  def resolve_links(should_resolve)
@@ -160,7 +144,6 @@ module PgEventstore
160
144
  @sql_builder.
161
145
  unselect.
162
146
  select("(COALESCE(original_events.*, events.*)).*").
163
- select('row_to_json(streams.*) as stream').
164
147
  join("LEFT JOIN events original_events ON original_events.id = events.link_id")
165
148
  end
166
149
 
@@ -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]
@@ -63,6 +64,19 @@ module PgEventstore
63
64
  deconstruct_keys(nil)
64
65
  end
65
66
 
67
+ # @return [Integer]
68
+ def hash
69
+ to_hash.hash
70
+ end
71
+
72
+ # @param another [Object]
73
+ # @return [Boolean]
74
+ def eql?(another)
75
+ return false unless another.is_a?(Stream)
76
+
77
+ hash == another.hash
78
+ end
79
+
66
80
  def ==(other_stream)
67
81
  return false unless other_stream.is_a?(Stream)
68
82
 
@@ -59,6 +59,10 @@ namespace :pg_eventstore do
59
59
  DROP TABLE IF EXISTS public.streams;
60
60
  DROP TABLE IF EXISTS public.event_types;
61
61
  DROP TABLE IF EXISTS public.migrations;
62
+ DROP TABLE IF EXISTS public.subscriptions_set;
63
+ DROP TABLE IF EXISTS public.subscriptions;
64
+ DROP TABLE IF EXISTS public.subscription_commands;
65
+ DROP TABLE IF EXISTS public.subscriptions_set_commands;
62
66
  DROP EXTENSION IF EXISTS "uuid-ossp";
63
67
  DROP EXTENSION IF EXISTS pgcrypto;
64
68
  SQL
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgEventstore
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_eventstore
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Dzyzenko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-29 00:00:00.000000000 Z
11
+ date: 2024-02-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -57,6 +57,7 @@ files:
57
57
  - db/migrations/10_create_subscription_commands.sql
58
58
  - db/migrations/11_create_subscriptions_set_commands.sql
59
59
  - db/migrations/12_improve_events_indexes.sql
60
+ - db/migrations/13_remove_duplicated_index.sql
60
61
  - db/migrations/1_improve_specific_stream_indexes.sql
61
62
  - db/migrations/2_adjust_global_position_index.sql
62
63
  - db/migrations/3_extract_type_into_separate_table.sql
@@ -69,6 +70,7 @@ files:
69
70
  - docs/appending_events.md
70
71
  - docs/configuration.md
71
72
  - docs/events_and_streams.md
73
+ - docs/linking_events.md
72
74
  - docs/multiple_commands.md
73
75
  - docs/reading_events.md
74
76
  - docs/subscriptions.md
@@ -79,8 +81,13 @@ files:
79
81
  - lib/pg_eventstore/client.rb
80
82
  - lib/pg_eventstore/commands.rb
81
83
  - lib/pg_eventstore/commands/append.rb
84
+ - lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb
85
+ - lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb
86
+ - lib/pg_eventstore/commands/link_to.rb
82
87
  - lib/pg_eventstore/commands/multiple.rb
83
88
  - lib/pg_eventstore/commands/read.rb
89
+ - lib/pg_eventstore/commands/regular_stream_read_paginated.rb
90
+ - lib/pg_eventstore/commands/system_stream_read_paginated.rb
84
91
  - lib/pg_eventstore/config.rb
85
92
  - lib/pg_eventstore/connection.rb
86
93
  - lib/pg_eventstore/errors.rb
@@ -96,6 +103,7 @@ files:
96
103
  - lib/pg_eventstore/queries.rb
97
104
  - lib/pg_eventstore/queries/event_queries.rb
98
105
  - lib/pg_eventstore/queries/event_type_queries.rb
106
+ - lib/pg_eventstore/queries/preloader.rb
99
107
  - lib/pg_eventstore/queries/stream_queries.rb
100
108
  - lib/pg_eventstore/queries/subscription_command_queries.rb
101
109
  - lib/pg_eventstore/queries/subscription_queries.rb