pg_eventstore 0.9.0 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/db/migrations/{3_create_events.sql → 1_create_events.sql} +11 -14
- data/db/migrations/5_partitions.sql +16 -0
- data/docs/events_and_streams.md +5 -3
- data/docs/how_it_works.md +80 -0
- data/lib/pg_eventstore/client.rb +11 -7
- data/lib/pg_eventstore/commands/append.rb +17 -8
- data/lib/pg_eventstore/commands/link_to.rb +3 -3
- data/lib/pg_eventstore/commands/read.rb +1 -1
- data/lib/pg_eventstore/errors.rb +17 -6
- data/lib/pg_eventstore/event_deserializer.rb +3 -1
- data/lib/pg_eventstore/queries/event_queries.rb +61 -28
- data/lib/pg_eventstore/queries/partition_queries.rb +184 -0
- data/lib/pg_eventstore/queries/subscription_queries.rb +2 -10
- data/lib/pg_eventstore/queries/transaction_queries.rb +13 -0
- data/lib/pg_eventstore/queries.rb +4 -6
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +10 -18
- data/lib/pg_eventstore/rspec/test_helpers.rb +16 -1
- data/lib/pg_eventstore/sql_builder.rb +34 -4
- data/lib/pg_eventstore/stream.rb +3 -8
- data/lib/pg_eventstore/version.rb +1 -1
- metadata +9 -11
- data/db/migrations/1_create_streams.sql +0 -13
- data/db/migrations/2_create_event_types.sql +0 -10
- data/lib/pg_eventstore/queries/event_type_queries.rb +0 -74
- data/lib/pg_eventstore/queries/preloader.rb +0 -37
- data/lib/pg_eventstore/queries/stream_queries.rb +0 -77
- /data/db/migrations/{4_create_subscriptions.sql → 2_create_subscriptions.sql} +0 -0
- /data/db/migrations/{5_create_subscription_commands.sql → 3_create_subscription_commands.sql} +0 -0
- /data/db/migrations/{6_create_subscriptions_set_commands.sql → 4_create_subscriptions_set_commands.sql} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9426880dd6354875d896f5bf37abeac180c4f04b26905db0efc035fec316e6b9
|
4
|
+
data.tar.gz: 355a6c370451fff9e391aa0ecb1c6abf105c6fc537d99f6b7371359067dd2ee4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fc96e847b892e375115a08985678328d166742e5f6be3ee967f67dc808c80166a58f010555567b05fbac172b31c7d98276d1e898de6889d5d53d3ccf0ab2be47
|
7
|
+
data.tar.gz: cba82b0c20d4104cd6878fd2471e5c4d0c70e60991f573cd5aa1a845e7e5b443cf9f15448b81754888a84883c6bfbaf932e452c13fd7d5d229b03185741f1dbd
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.10.1] - 2024-03-12
|
4
|
+
|
5
|
+
- Handle edge case when creating partitions
|
6
|
+
|
7
|
+
## [0.10.0] - 2024-03-12
|
8
|
+
|
9
|
+
- Reimplement db structure
|
10
|
+
- Optimize `#append_to_stream` method - it now produces one `INSERT` query when publishing multiple events
|
11
|
+
|
3
12
|
## [0.9.0] - 2024-02-23
|
4
13
|
|
5
14
|
- Use POSIX locale for streams and event types
|
@@ -1,27 +1,24 @@
|
|
1
1
|
CREATE TABLE public.events
|
2
2
|
(
|
3
3
|
id uuid DEFAULT public.gen_random_uuid() NOT NULL,
|
4
|
-
|
4
|
+
context character varying COLLATE "POSIX" NOT NULL,
|
5
|
+
stream_name character varying COLLATE "POSIX" NOT NULL,
|
6
|
+
stream_id character varying COLLATE "POSIX" NOT NULL,
|
5
7
|
global_position bigserial NOT NULL,
|
6
8
|
stream_revision integer NOT NULL,
|
7
9
|
data jsonb DEFAULT '{}'::jsonb NOT NULL,
|
8
10
|
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
9
11
|
link_id uuid,
|
10
12
|
created_at timestamp without time zone DEFAULT now() NOT NULL,
|
11
|
-
|
12
|
-
);
|
13
|
+
type character varying COLLATE "POSIX" NOT NULL
|
14
|
+
) PARTITION BY LIST (context);
|
13
15
|
|
14
16
|
ALTER TABLE ONLY public.events
|
15
|
-
ADD CONSTRAINT events_pkey PRIMARY KEY (
|
17
|
+
ADD CONSTRAINT events_pkey PRIMARY KEY (context, stream_name, type, global_position);
|
16
18
|
|
17
|
-
CREATE INDEX
|
18
|
-
CREATE INDEX
|
19
|
-
CREATE INDEX idx_events_link_id ON public.events USING btree (link_id);
|
20
|
-
CREATE INDEX idx_events_stream_id_and_revision ON public.events USING btree (stream_id, stream_revision);
|
19
|
+
CREATE INDEX idx_events_stream_id_and_stream_revision ON public.events USING btree (stream_id, stream_revision);
|
20
|
+
CREATE INDEX idx_events_stream_id_and_global_position ON public.events USING btree (stream_id, global_position);
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
ADD CONSTRAINT events_event_type_fk FOREIGN KEY (event_type_id) REFERENCES public.event_types (id);
|
26
|
-
ALTER TABLE ONLY public.events
|
27
|
-
ADD CONSTRAINT events_link_fk FOREIGN KEY (link_id) REFERENCES public.events (id) ON DELETE CASCADE;
|
22
|
+
CREATE INDEX idx_events_id ON public.events USING btree (id);
|
23
|
+
CREATE INDEX idx_events_link_id ON public.events USING btree (link_id);
|
24
|
+
CREATE INDEX idx_events_global_position ON public.events USING btree (global_position);
|
@@ -0,0 +1,16 @@
|
|
1
|
+
CREATE TABLE public.partitions
|
2
|
+
(
|
3
|
+
id bigserial NOT NULL,
|
4
|
+
context character varying COLLATE "POSIX" NOT NULL,
|
5
|
+
stream_name character varying COLLATE "POSIX",
|
6
|
+
event_type character varying COLLATE "POSIX",
|
7
|
+
table_name character varying COLLATE "POSIX" NOT NULL
|
8
|
+
);
|
9
|
+
|
10
|
+
ALTER TABLE ONLY public.partitions
|
11
|
+
ADD CONSTRAINT partitions_pkey PRIMARY KEY (id);
|
12
|
+
|
13
|
+
CREATE UNIQUE INDEX idx_partitions_by_context ON public.partitions USING btree (context) WHERE stream_name IS NULL AND event_type IS NULL;
|
14
|
+
CREATE UNIQUE INDEX idx_partitions_by_context_and_stream_name ON public.partitions USING btree (context, stream_name) WHERE event_type IS NULL;
|
15
|
+
CREATE UNIQUE INDEX idx_partitions_by_context_and_stream_name_and_event_type ON public.partitions USING btree (context, stream_name, event_type);
|
16
|
+
CREATE UNIQUE INDEX idx_partitions_by_partition_table_name ON public.partitions USING btree (table_name);
|
data/docs/events_and_streams.md
CHANGED
@@ -22,7 +22,7 @@
|
|
22
22
|
Example:
|
23
23
|
|
24
24
|
```ruby
|
25
|
-
PgEventstore::Event.new(data: { 'foo' => 'bar' })
|
25
|
+
PgEventstore::Event.new(data: { 'foo' => 'bar' }, type: 'FooChanged')
|
26
26
|
```
|
27
27
|
|
28
28
|
## Stream object
|
@@ -32,8 +32,6 @@ To be able to manipulate a stream, you have to compute a stream's object first.
|
|
32
32
|
- `context` - String(required). A Bounded Context, read more [here](https://martinfowler.com/bliki/BoundedContext.html). Values which start from `$` sign are reserved by `pg_eventstore`. Such contexts can't be used to append events.
|
33
33
|
- `stream_name` - String(required). A stream name.
|
34
34
|
- `stream_id` - String(required). A stream id.
|
35
|
-
- `id` - Integer(optional, read only). Internal id. It is set when a stream is returned from the database as part of the deserialization process. Manually assigning this attribute has no effect.
|
36
|
-
- `stream_revision` - Integer(optional, read only). Current stream's revision. You can rely on this value when setting the `:expected_revision` option when appending events to a stream. It is set when a stream is returned from the database a part of the deserialization process. Manually assigning this attribute has no effect.
|
37
35
|
|
38
36
|
Example:
|
39
37
|
|
@@ -43,3 +41,7 @@ PgEventstore::Stream.new(context: 'Sales', stream_name: 'Customer', stream_id: '
|
|
43
41
|
```
|
44
42
|
|
45
43
|
There is a special stream, called the "all" stream. You can get this object by calling the`PgEventstore::Stream.all_stream` method. Read more about the "all" stream in the `Reading from the "all" stream` section of [Reading events](reading_events.md) chapter.
|
44
|
+
|
45
|
+
## Important note
|
46
|
+
|
47
|
+
Because the database is designed for Eventsourcing, some limitations should be met - a combination of `Event#type`, `Stream#context` and `Stream#stream_name` must have low cardinality(low unique values number). This means you should pre-defined values there. Otherwise it may lead to the performance degradation. See [How it works](how_it_works.md) chapter for the details.
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# How it works
|
2
|
+
|
3
|
+
## Database architecture
|
4
|
+
|
5
|
+
The database is designed specifically for Eventsourcing using Domain-Driven Design. `events` table is partitioned in next way:
|
6
|
+
|
7
|
+
- For each `Stream#context` there is a subpartition of `events` table. Those tables have `contexts_` prefix.
|
8
|
+
- For each `Stream#stream_name` there is a subpartition of `contexts_` table. Those tables have `stream_names_` prefix.
|
9
|
+
- For each `Event#type` there is a subpartition of `stream_names_` table. Those tables have `event_types_` prefix.
|
10
|
+
|
11
|
+
To implement partitions - Declarative Partitioning is used. Partitioning means that you should not have any random values in the combination of `Stream#context`, `Stream#stream_name` and `Event#type`. A combination of those values must have low cardinality(low distinct values number) and must be pre-defined in your application. Otherwise it will lead to the performance degradation. More about PostgreSQL partitions is [here](https://www.postgresql.org/docs/current/ddl-partitioning.html),
|
12
|
+
|
13
|
+
So, let's say you want to publish next event:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
stream = PgEventstore::Stream.new(context: 'SomeCtx', stream_name: 'SomeStream', stream_id: '1')
|
17
|
+
event = PgEventstore::Event.new(type: 'SomethingChanged', data: { foo: :bar })
|
18
|
+
PgEventstore.client.append_to_stream(stream, event)
|
19
|
+
```
|
20
|
+
|
21
|
+
To actually create `events` record next partitions will be created:
|
22
|
+
|
23
|
+
- `contexts_81820a` table which is a subpartition of `events` table. It is needed to handle all events which comes to `"SomeCtx"` context
|
24
|
+
- `stream_names_ecb803` table which is a subpartition of `contexts_81820a` table. It is needed to handle all events which comes to `"SomeStream"` stream name of `"SomeCtx"` context
|
25
|
+
- `event_types_aeadd5` table which is a subpartition of `stream_names_ecb803` table. It is needed to handle all events which have `"SomethingChanged"` event type of `"SomeStream"` stream name of `"SomeCtx"` context
|
26
|
+
|
27
|
+
You can check all partitions and associated with them contexts, stream names and event types by querying `partitions` table. Example(based on the publish sample above):
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
PgEventstore.connection.with do |conn|
|
31
|
+
conn.exec('select * from partitions')
|
32
|
+
end.to_a
|
33
|
+
# =>
|
34
|
+
# [{"id"=>1, "context"=>"SomeCtx", "stream_name"=>nil, "event_type"=>nil, "table_name"=>"contexts_81820a"},
|
35
|
+
# {"id"=>2, "context"=>"SomeCtx", "stream_name"=>"SomeStream", "event_type"=>nil, "table_name"=>"stream_names_ecb803"},
|
36
|
+
# {"id"=>3, "context"=>"SomeCtx", "stream_name"=>"SomeStream", "event_type"=>"SomethingChanged", "table_name"=>"event_types_aeadd5"}]
|
37
|
+
```
|
38
|
+
|
39
|
+
## Appending events and multiple commands
|
40
|
+
|
41
|
+
You may want to get familiar with [Appending events](appending_events.md) and [multiple commands](multiple_commands.md) first.
|
42
|
+
|
43
|
+
`pg_eventstore` internally uses `Serializable` transaction isolation level(more about different transaction isolation levels in PostgreSQL is [here](https://www.postgresql.org/docs/current/transaction-iso.html)). On practice this means that any transaction may fail with serialization error, and the common approach is to restart this transaction. For ruby this means re-execution of the block of code. Which is why there is a warning regarding potential block re-execution when using `#multiple`. However current implementation allows to limit 99% of retries to the manipulations with one stream. For example, when two parallel processes changing the same stream. If different streams are being changed at the same time - it is less likely it would perform retry.
|
44
|
+
|
45
|
+
Examples:
|
46
|
+
|
47
|
+
- if "process 1" and "process 2" perform the append command at the same time - one of the append commands will be retried:
|
48
|
+
```ruby
|
49
|
+
# process 1
|
50
|
+
stream = PgEventstore::Stream.new(context: 'MyCtx', stream_name: 'MyStream', stream_id: '1')
|
51
|
+
event = PgEventstore::Event.new(type: 'SomethingChanged', data: { foo: :bar })
|
52
|
+
PgEventstore.client.append_to_stream(stream, event)
|
53
|
+
|
54
|
+
# process 2
|
55
|
+
stream = PgEventstore::Stream.new(context: 'MyCtx', stream_name: 'MyStream', stream_id: '1')
|
56
|
+
event = PgEventstore::Event.new(type: 'SomethingElseChanged', data: { baz: :bar })
|
57
|
+
PgEventstore.client.append_to_stream(stream, event)
|
58
|
+
```
|
59
|
+
|
60
|
+
- if "process 1" performs multiple commands at the same time "process 2" performs append command which involves the same stream from "process 1" - either block of `#multiple` or `#append_to_stream` will be retried:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
# process 1
|
64
|
+
stream1 = PgEventstore::Stream.new(context: 'MyCtx', stream_name: 'MyStream1', stream_id: '1')
|
65
|
+
stream2 = PgEventstore::Stream.new(context: 'MyCtx', stream_name: 'MyStream2', stream_id: '1')
|
66
|
+
event = PgEventstore::Event.new(type: 'SomethingChanged', data: { foo: :bar })
|
67
|
+
PgEventstore.client.multiple do
|
68
|
+
PgEventstore.client.append_to_stream(stream1, event)
|
69
|
+
PgEventstore.client.append_to_stream(stream2, event)
|
70
|
+
end
|
71
|
+
|
72
|
+
# process 2
|
73
|
+
stream2 = PgEventstore::Stream.new(context: 'MyCtx', stream_name: 'MyStream2', stream_id: '1')
|
74
|
+
event = PgEventstore::Event.new(type: 'SomethingChanged', data: { foo: :bar })
|
75
|
+
PgEventstore.client.append_to_stream(stream2, event)
|
76
|
+
```
|
77
|
+
|
78
|
+
Retries also concern your potential implementation of [middlewares](writing_middleware.md). For example, `YourAwesomeMiddleware#serialize` can be executed several times when append the event. This is especially important when you involve your microservices here - they can receive the same payload several times.
|
79
|
+
|
80
|
+
Conclusion. When developing using `pg_eventstore` - always keep in mind that some parts of your implementation can be executed several times before successfully publishing an event, or event when reading events(`#deserializa` middleware method) if you perform reading withing `#multiple` block.
|
data/lib/pg_eventstore/client.rb
CHANGED
@@ -29,7 +29,9 @@ module PgEventstore
|
|
29
29
|
result =
|
30
30
|
Commands::Append.new(
|
31
31
|
Queries.new(
|
32
|
-
|
32
|
+
partitions: partition_queries,
|
33
|
+
events: event_queries(middlewares(middlewares)),
|
34
|
+
transactions: transaction_queries
|
33
35
|
)
|
34
36
|
).call(stream, *events_or_event, options: options)
|
35
37
|
events_or_event.is_a?(Array) ? result : result.first
|
@@ -105,7 +107,7 @@ module PgEventstore
|
|
105
107
|
# @raise [PgEventstore::StreamNotFoundError]
|
106
108
|
def read(stream, options: {}, middlewares: nil)
|
107
109
|
Commands::Read.
|
108
|
-
new(Queries.new(
|
110
|
+
new(Queries.new(partitions: partition_queries, events: event_queries(middlewares(middlewares)))).
|
109
111
|
call(stream, options: { max_count: config.max_count }.merge(options))
|
110
112
|
end
|
111
113
|
|
@@ -114,7 +116,7 @@ module PgEventstore
|
|
114
116
|
def read_paginated(stream, options: {}, middlewares: nil)
|
115
117
|
cmd_class = stream.system? ? Commands::SystemStreamReadPaginated : Commands::RegularStreamReadPaginated
|
116
118
|
cmd_class.
|
117
|
-
new(Queries.new(
|
119
|
+
new(Queries.new(partitions: partition_queries, events: event_queries(middlewares(middlewares)))).
|
118
120
|
call(stream, options: { max_count: config.max_count }.merge(options))
|
119
121
|
end
|
120
122
|
|
@@ -133,7 +135,9 @@ module PgEventstore
|
|
133
135
|
result =
|
134
136
|
Commands::LinkTo.new(
|
135
137
|
Queries.new(
|
136
|
-
|
138
|
+
partitions: partition_queries,
|
139
|
+
events: event_queries(middlewares(middlewares)),
|
140
|
+
transactions: transaction_queries
|
137
141
|
)
|
138
142
|
).call(stream, *events_or_event, options: options)
|
139
143
|
events_or_event.is_a?(Array) ? result : result.first
|
@@ -154,9 +158,9 @@ module PgEventstore
|
|
154
158
|
PgEventstore.connection(config.name)
|
155
159
|
end
|
156
160
|
|
157
|
-
# @return [PgEventstore::
|
158
|
-
def
|
159
|
-
|
161
|
+
# @return [PgEventstore::PartitionQueries]
|
162
|
+
def partition_queries
|
163
|
+
PartitionQueries.new(connection)
|
160
164
|
end
|
161
165
|
|
162
166
|
# @return [PgEventstore::TransactionQueries]
|
@@ -16,19 +16,28 @@ module PgEventstore
|
|
16
16
|
raise SystemStreamError, stream if stream.system?
|
17
17
|
|
18
18
|
queries.transactions.transaction do
|
19
|
-
|
20
|
-
revision = stream.stream_revision
|
19
|
+
revision = queries.events.stream_revision(stream) || Stream::NON_EXISTING_STREAM_REVISION
|
21
20
|
assert_expected_revision!(revision, options[:expected_revision], stream) if options[:expected_revision]
|
22
|
-
events.map.with_index(1) do |event, index|
|
23
|
-
|
24
|
-
end.tap do
|
25
|
-
queries.streams.update_stream_revision(stream, revision + events.size)
|
21
|
+
formatted_events = events.map.with_index(1) do |event, index|
|
22
|
+
event_modifier.call(event, revision + index)
|
26
23
|
end
|
24
|
+
create_partitions(stream, formatted_events)
|
25
|
+
queries.events.insert(stream, formatted_events)
|
27
26
|
end
|
28
27
|
end
|
29
28
|
|
30
29
|
private
|
31
30
|
|
31
|
+
# @param stream [PgEventstore::Stream]
|
32
|
+
# @param events [Array<PgEventstore::Event>]
|
33
|
+
# @return [void]
|
34
|
+
def create_partitions(stream, events)
|
35
|
+
missing_event_types = events.map(&:type).map(&:to_s).uniq.select do |event_type|
|
36
|
+
queries.partitions.partition_required?(stream, event_type)
|
37
|
+
end
|
38
|
+
raise MissingPartitions.new(stream, missing_event_types) if missing_event_types.any?
|
39
|
+
end
|
40
|
+
|
32
41
|
# @param revision [Integer]
|
33
42
|
# @param expected_revision [Symbol, Integer]
|
34
43
|
# @param stream [PgEventstore::Stream]
|
@@ -46,12 +55,12 @@ module PgEventstore
|
|
46
55
|
end
|
47
56
|
|
48
57
|
in [Integer, Symbol]
|
49
|
-
if revision == Stream::
|
58
|
+
if revision == Stream::NON_EXISTING_STREAM_REVISION && expected_revision == :stream_exists
|
50
59
|
raise WrongExpectedRevisionError.new(
|
51
60
|
revision: revision, expected_revision: expected_revision, stream: stream
|
52
61
|
)
|
53
62
|
end
|
54
|
-
if revision > Stream::
|
63
|
+
if revision > Stream::NON_EXISTING_STREAM_REVISION && expected_revision == :no_stream
|
55
64
|
raise WrongExpectedRevisionError.new(
|
56
65
|
revision: revision, expected_revision: expected_revision, stream: stream
|
57
66
|
)
|
@@ -13,7 +13,7 @@ module PgEventstore
|
|
13
13
|
# @raise [PgEventstore::WrongExpectedRevisionError]
|
14
14
|
# @raise [PgEventstore::NotPersistedEventError]
|
15
15
|
def call(stream, *events, options: {})
|
16
|
-
events.each(&method(:
|
16
|
+
events.each(&method(:check_event_presence))
|
17
17
|
append_cmd = Append.new(queries)
|
18
18
|
append_cmd.call(stream, *events, options: options, event_modifier: EventModifiers::PrepareLinkEvent)
|
19
19
|
end
|
@@ -23,8 +23,8 @@ module PgEventstore
|
|
23
23
|
# Checks if Event#id is present. An event must have the #id value in order to be linked.
|
24
24
|
# @param event [PgEventstore::Event]
|
25
25
|
# @return [void]
|
26
|
-
def
|
27
|
-
return
|
26
|
+
def check_event_presence(event)
|
27
|
+
return if queries.events.event_exists?(event.id)
|
28
28
|
|
29
29
|
raise NotPersistedEventError, event
|
30
30
|
end
|
@@ -15,7 +15,7 @@ module PgEventstore
|
|
15
15
|
# @return [Array<PgEventstore::Event>]
|
16
16
|
# @raise [PgEventstore::StreamNotFoundError]
|
17
17
|
def call(stream, options: {})
|
18
|
-
|
18
|
+
queries.events.stream_revision(stream) || raise(StreamNotFoundError, stream) unless stream.all_stream?
|
19
19
|
|
20
20
|
queries.events.stream_events(stream, options)
|
21
21
|
end
|
data/lib/pg_eventstore/errors.rb
CHANGED
@@ -58,9 +58,11 @@ module PgEventstore
|
|
58
58
|
|
59
59
|
# @return [String]
|
60
60
|
def user_friendly_message
|
61
|
-
|
62
|
-
|
63
|
-
|
61
|
+
if revision == Stream::NON_EXISTING_STREAM_REVISION && expected_revision == :stream_exists
|
62
|
+
return expected_stream_exists
|
63
|
+
end
|
64
|
+
return expected_no_stream if revision > Stream::NON_EXISTING_STREAM_REVISION && expected_revision == :no_stream
|
65
|
+
return current_no_stream if revision == Stream::NON_EXISTING_STREAM_REVISION && expected_revision.is_a?(Integer)
|
64
66
|
|
65
67
|
unmatched_stream_revision
|
66
68
|
end
|
@@ -189,9 +191,18 @@ module PgEventstore
|
|
189
191
|
|
190
192
|
# @return [String]
|
191
193
|
def user_friendly_message
|
192
|
-
|
193
|
-
|
194
|
-
|
194
|
+
"Event with #id #{event.id.inspect} must be present, but it could not be found."
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class MissingPartitions < Error
|
199
|
+
attr_reader :stream, :event_types
|
200
|
+
|
201
|
+
# @param stream [PgEventstore::Stream]
|
202
|
+
# @param event_types [Array<String>]
|
203
|
+
def initialize(stream, event_types)
|
204
|
+
@stream = stream
|
205
|
+
@event_types = event_types
|
195
206
|
end
|
196
207
|
end
|
197
208
|
end
|
@@ -24,7 +24,9 @@ module PgEventstore
|
|
24
24
|
middlewares.each do |middleware|
|
25
25
|
middleware.deserialize(event)
|
26
26
|
end
|
27
|
-
event.stream = PgEventstore::Stream.new(
|
27
|
+
event.stream = PgEventstore::Stream.new(
|
28
|
+
**attrs.slice('context', 'stream_name', 'stream_id').transform_keys(&:to_sym)
|
29
|
+
)
|
28
30
|
event
|
29
31
|
end
|
30
32
|
|
@@ -15,46 +15,89 @@ module PgEventstore
|
|
15
15
|
@deserializer = deserializer
|
16
16
|
end
|
17
17
|
|
18
|
+
# @param id [String, nil]
|
19
|
+
# @return [Boolean]
|
20
|
+
def event_exists?(id)
|
21
|
+
return false if id.nil?
|
22
|
+
|
23
|
+
sql_builder = SQLBuilder.new.select('1 as exists').from('events').where('id = ?', id).limit(1)
|
24
|
+
connection.with do |conn|
|
25
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
26
|
+
end.to_a.dig(0, 'exists') == 1
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param stream [PgEventstore::Stream]
|
30
|
+
# @return [Integer, nil]
|
31
|
+
def stream_revision(stream)
|
32
|
+
sql_builder = SQLBuilder.new.from('events').select('stream_revision').
|
33
|
+
where('context = ? and stream_name = ? and stream_id = ?', *stream.to_a).
|
34
|
+
order('stream_revision DESC').
|
35
|
+
limit(1)
|
36
|
+
connection.with do |conn|
|
37
|
+
conn.exec_params(*sql_builder.to_exec_params)
|
38
|
+
end.to_a.dig(0, 'stream_revision')
|
39
|
+
end
|
40
|
+
|
18
41
|
# @see PgEventstore::Client#read for more info
|
19
42
|
# @param stream [PgEventstore::Stream]
|
20
43
|
# @param options [Hash]
|
21
44
|
# @return [Array<PgEventstore::Event>]
|
22
45
|
def stream_events(stream, options)
|
23
|
-
options = event_type_queries.include_event_types_ids(options)
|
24
46
|
exec_params = events_filtering(stream, options).to_exec_params
|
25
47
|
raw_events = connection.with do |conn|
|
26
48
|
conn.exec_params(*exec_params)
|
27
49
|
end.to_a
|
28
|
-
preloader.preload_related_objects(raw_events)
|
29
50
|
deserializer.deserialize_many(raw_events)
|
30
51
|
end
|
31
52
|
|
32
|
-
# @param stream [PgEventstore::Stream]
|
33
|
-
# @param
|
53
|
+
# @param stream [PgEventstore::Stream]
|
54
|
+
# @param events [Array<PgEventstore::Event>]
|
34
55
|
# @return [PgEventstore::Event]
|
35
|
-
def insert(stream,
|
36
|
-
|
37
|
-
|
38
|
-
attributes = event.options_hash.slice(:id, :data, :metadata, :stream_revision, :link_id).compact
|
39
|
-
attributes[:stream_id] = stream.id
|
40
|
-
attributes[:event_type_id] = event_type_queries.find_or_create_type(event.type)
|
56
|
+
def insert(stream, events)
|
57
|
+
sql_rows_for_insert, values = prepared_statements(stream, events)
|
58
|
+
columns = %w[id data metadata stream_revision link_id type context stream_name stream_id]
|
41
59
|
|
42
60
|
sql = <<~SQL
|
43
|
-
INSERT INTO events (#{
|
44
|
-
VALUES
|
45
|
-
RETURNING
|
61
|
+
INSERT INTO events (#{columns.join(', ')})
|
62
|
+
VALUES #{sql_rows_for_insert.join(", ")}
|
63
|
+
RETURNING *
|
46
64
|
SQL
|
47
65
|
|
48
|
-
|
49
|
-
conn.exec_params(sql,
|
50
|
-
end.
|
51
|
-
|
52
|
-
persisted_event.stream = stream
|
66
|
+
connection.with do |conn|
|
67
|
+
conn.exec_params(sql, values)
|
68
|
+
end.map do |raw_event|
|
69
|
+
deserializer.without_middlewares.deserialize(raw_event)
|
53
70
|
end
|
54
71
|
end
|
55
72
|
|
56
73
|
private
|
57
74
|
|
75
|
+
# @param stream [PgEventstore::Stream]
|
76
|
+
# @param events [Array<PgEventstore::Event>]
|
77
|
+
# @return [Array<Array<String>, Array<Object>>]
|
78
|
+
def prepared_statements(stream, events)
|
79
|
+
positional_counter = 1
|
80
|
+
values = []
|
81
|
+
sql_rows_for_insert = events.map do |event|
|
82
|
+
event = serializer.serialize(event)
|
83
|
+
attributes = event.options_hash.slice(:id, :data, :metadata, :stream_revision, :link_id, :type)
|
84
|
+
|
85
|
+
attributes = attributes.merge(stream.to_hash)
|
86
|
+
prepared = attributes.values.map do |value|
|
87
|
+
if value.nil?
|
88
|
+
'DEFAULT'
|
89
|
+
else
|
90
|
+
"$#{positional_counter}".tap do
|
91
|
+
values.push(value)
|
92
|
+
positional_counter += 1
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
"(#{prepared.join(',')})"
|
97
|
+
end
|
98
|
+
[sql_rows_for_insert, values]
|
99
|
+
end
|
100
|
+
|
58
101
|
# @param stream [PgEventstore::Stream]
|
59
102
|
# @param options [Hash]
|
60
103
|
# @return [PgEventstore::EventsFilteringQuery]
|
@@ -63,15 +106,5 @@ module PgEventstore
|
|
63
106
|
|
64
107
|
QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options)
|
65
108
|
end
|
66
|
-
|
67
|
-
# @return [PgEventstore::EventTypeQueries]
|
68
|
-
def event_type_queries
|
69
|
-
EventTypeQueries.new(connection)
|
70
|
-
end
|
71
|
-
|
72
|
-
# @return [PgEventstore::Preloader]
|
73
|
-
def preloader
|
74
|
-
Preloader.new(connection)
|
75
|
-
end
|
76
109
|
end
|
77
110
|
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# @!visibility private
|
5
|
+
class PartitionQueries
|
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 stream [PgEventstore::Stream]
|
15
|
+
# @return [Hash] partition attributes
|
16
|
+
def create_context_partition(stream)
|
17
|
+
attributes = { context: stream.context, table_name: context_partition_name(stream) }
|
18
|
+
|
19
|
+
loop do
|
20
|
+
break unless partition_name_taken?(attributes[:table_name])
|
21
|
+
|
22
|
+
attributes[:table_name] = attributes[:table_name].next
|
23
|
+
end
|
24
|
+
|
25
|
+
partition_sql = <<~SQL
|
26
|
+
INSERT INTO partitions (#{attributes.keys.join(', ')})
|
27
|
+
VALUES (#{Utils.positional_vars(attributes.values)}) RETURNING *
|
28
|
+
SQL
|
29
|
+
partition = connection.with do |conn|
|
30
|
+
conn.exec_params(partition_sql, [*attributes.values])
|
31
|
+
end.to_a.first
|
32
|
+
connection.with do |conn|
|
33
|
+
conn.exec(<<~SQL)
|
34
|
+
CREATE TABLE #{attributes[:table_name]} PARTITION OF events
|
35
|
+
FOR VALUES IN('#{conn.escape_string(stream.context)}') PARTITION BY LIST (stream_name)
|
36
|
+
SQL
|
37
|
+
end
|
38
|
+
partition
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param stream [PgEventstore::Stream]
|
42
|
+
# @param context_partition_name [String]
|
43
|
+
# @return [Hash] partition attributes
|
44
|
+
def create_stream_name_partition(stream, context_partition_name)
|
45
|
+
attributes = {
|
46
|
+
context: stream.context, stream_name: stream.stream_name, table_name: stream_name_partition_name(stream)
|
47
|
+
}
|
48
|
+
|
49
|
+
loop do
|
50
|
+
break unless partition_name_taken?(attributes[:table_name])
|
51
|
+
|
52
|
+
attributes[:table_name] = attributes[:table_name].next
|
53
|
+
end
|
54
|
+
|
55
|
+
partition_sql = <<~SQL
|
56
|
+
INSERT INTO partitions (#{attributes.keys.join(', ')})
|
57
|
+
VALUES (#{Utils.positional_vars(attributes.values)}) RETURNING *
|
58
|
+
SQL
|
59
|
+
partition = connection.with do |conn|
|
60
|
+
conn.exec_params(partition_sql, [*attributes.values])
|
61
|
+
end.to_a.first
|
62
|
+
connection.with do |conn|
|
63
|
+
conn.exec(<<~SQL)
|
64
|
+
CREATE TABLE #{attributes[:table_name]} PARTITION OF #{context_partition_name}
|
65
|
+
FOR VALUES IN('#{conn.escape_string(stream.stream_name)}') PARTITION BY LIST (type)
|
66
|
+
SQL
|
67
|
+
end
|
68
|
+
partition
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param stream [PgEventstore::Stream]
|
72
|
+
# @param stream_name_partition_name [String]
|
73
|
+
# @return [Hash] partition attributes
|
74
|
+
def create_event_type_partition(stream, event_type, stream_name_partition_name)
|
75
|
+
attributes = {
|
76
|
+
context: stream.context, stream_name: stream.stream_name, event_type: event_type,
|
77
|
+
table_name: event_type_partition_name(stream, event_type)
|
78
|
+
}
|
79
|
+
|
80
|
+
loop do
|
81
|
+
break unless partition_name_taken?(attributes[:table_name])
|
82
|
+
|
83
|
+
attributes[:table_name] = attributes[:table_name].next
|
84
|
+
end
|
85
|
+
|
86
|
+
partition_sql = <<~SQL
|
87
|
+
INSERT INTO partitions (#{attributes.keys.join(', ')})
|
88
|
+
VALUES (#{Utils.positional_vars(attributes.values)}) RETURNING *
|
89
|
+
SQL
|
90
|
+
partition = connection.with do |conn|
|
91
|
+
conn.exec_params(partition_sql, [*attributes.values])
|
92
|
+
end.to_a.first
|
93
|
+
connection.with do |conn|
|
94
|
+
conn.exec(<<~SQL)
|
95
|
+
CREATE TABLE #{attributes[:table_name]} PARTITION OF #{stream_name_partition_name}
|
96
|
+
FOR VALUES IN('#{conn.escape_string(event_type)}')
|
97
|
+
SQL
|
98
|
+
end
|
99
|
+
partition
|
100
|
+
end
|
101
|
+
|
102
|
+
# @param stream [PgEventstore::Stream]
|
103
|
+
# @param event_type [String]
|
104
|
+
# @return [Boolean]
|
105
|
+
def partition_required?(stream, event_type)
|
106
|
+
event_type_partition(stream, event_type).nil?
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param stream [PgEventstore::Stream]
|
110
|
+
# @param event_type [String]
|
111
|
+
# @return [void]
|
112
|
+
def create_partitions(stream, event_type)
|
113
|
+
return unless partition_required?(stream, event_type)
|
114
|
+
|
115
|
+
context_partition = context_partition(stream) || create_context_partition(stream)
|
116
|
+
stream_name_partition = stream_name_partition(stream) ||
|
117
|
+
create_stream_name_partition(stream, context_partition['table_name'])
|
118
|
+
|
119
|
+
create_event_type_partition(stream, event_type, stream_name_partition['table_name'])
|
120
|
+
end
|
121
|
+
|
122
|
+
# @param stream [PgEventstore::Stream]
|
123
|
+
# @return [Hash, nil] partition attributes
|
124
|
+
def context_partition(stream)
|
125
|
+
connection.with do |conn|
|
126
|
+
conn.exec_params(
|
127
|
+
'select * from partitions where context = $1 and stream_name is null and event_type is null',
|
128
|
+
[stream.context]
|
129
|
+
)
|
130
|
+
end.first
|
131
|
+
end
|
132
|
+
|
133
|
+
# @param stream [PgEventstore::Stream]
|
134
|
+
# @return [Hash, nil] partition attributes
|
135
|
+
def stream_name_partition(stream)
|
136
|
+
connection.with do |conn|
|
137
|
+
conn.exec_params(
|
138
|
+
<<~SQL,
|
139
|
+
select * from partitions where context = $1 and stream_name = $2 and event_type is null
|
140
|
+
SQL
|
141
|
+
[stream.context, stream.stream_name]
|
142
|
+
)
|
143
|
+
end.first
|
144
|
+
end
|
145
|
+
|
146
|
+
# @param stream [PgEventstore::Stream]
|
147
|
+
# @param event_type [String]
|
148
|
+
# @return [Hash, nil] partition attributes
|
149
|
+
def event_type_partition(stream, event_type)
|
150
|
+
connection.with do |conn|
|
151
|
+
conn.exec_params(
|
152
|
+
<<~SQL,
|
153
|
+
select * from partitions where context = $1 and stream_name = $2 and event_type = $3
|
154
|
+
SQL
|
155
|
+
[stream.context, stream.stream_name, event_type]
|
156
|
+
)
|
157
|
+
end.first
|
158
|
+
end
|
159
|
+
|
160
|
+
# @param table_name [String]
|
161
|
+
# @return [Boolean]
|
162
|
+
def partition_name_taken?(table_name)
|
163
|
+
connection.with do |conn|
|
164
|
+
conn.exec_params('select 1 as exists from partitions where table_name = $1', [table_name])
|
165
|
+
end.to_a.dig(0, 'exists') == 1
|
166
|
+
end
|
167
|
+
|
168
|
+
# @param stream [PgEventstore::Stream]
|
169
|
+
# @return [String]
|
170
|
+
def context_partition_name(stream)
|
171
|
+
"contexts_#{Digest::MD5.hexdigest(stream.context)[0..5]}"
|
172
|
+
end
|
173
|
+
|
174
|
+
# @param stream [PgEventstore::Stream]
|
175
|
+
# @return [String]
|
176
|
+
def stream_name_partition_name(stream)
|
177
|
+
"stream_names_#{Digest::MD5.hexdigest("#{stream.context}-#{stream.stream_name}")[0..5]}"
|
178
|
+
end
|
179
|
+
|
180
|
+
def event_type_partition_name(stream, event_type)
|
181
|
+
"event_types_#{Digest::MD5.hexdigest("#{stream.context}-#{stream.stream_name}-#{event_type}")[0..5]}"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -79,10 +79,9 @@ 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
|
-
|
82
|
+
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)
|
86
85
|
end
|
87
86
|
|
88
87
|
# @param id [Integer] subscription's id
|
@@ -128,9 +127,7 @@ module PgEventstore
|
|
128
127
|
# @param options [Hash] query options
|
129
128
|
# @return [PgEventstore::SQLBuilder]
|
130
129
|
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
|
130
|
+
builder = PgEventstore::QueryBuilders::EventsFiltering.subscriptions_events_filtering(options).to_sql_builder
|
134
131
|
builder.select("#{id} as runner_id")
|
135
132
|
end
|
136
133
|
|
@@ -152,11 +149,6 @@ module PgEventstore
|
|
152
149
|
EventTypeQueries.new(connection)
|
153
150
|
end
|
154
151
|
|
155
|
-
# @return [PgEventstore::Preloader]
|
156
|
-
def preloader
|
157
|
-
Preloader.new(connection)
|
158
|
-
end
|
159
|
-
|
160
152
|
# @param hash [Hash]
|
161
153
|
# @return [Hash]
|
162
154
|
def deserialize(hash)
|
@@ -36,6 +36,19 @@ module PgEventstore
|
|
36
36
|
end
|
37
37
|
rescue PG::TRSerializationFailure, PG::TRDeadlockDetected
|
38
38
|
retry
|
39
|
+
rescue MissingPartitions => error
|
40
|
+
error.event_types.each do |event_type|
|
41
|
+
transaction do
|
42
|
+
partition_queries.create_partitions(error.stream, event_type)
|
43
|
+
end
|
44
|
+
rescue PG::UniqueViolation
|
45
|
+
retry
|
46
|
+
end
|
47
|
+
retry
|
48
|
+
end
|
49
|
+
|
50
|
+
def partition_queries
|
51
|
+
PartitionQueries.new(connection)
|
39
52
|
end
|
40
53
|
end
|
41
54
|
end
|
@@ -4,13 +4,11 @@ require_relative 'sql_builder'
|
|
4
4
|
require_relative 'query_builders/events_filtering_query'
|
5
5
|
require_relative 'queries/transaction_queries'
|
6
6
|
require_relative 'queries/event_queries'
|
7
|
-
require_relative 'queries/
|
8
|
-
require_relative 'queries/event_type_queries'
|
7
|
+
require_relative 'queries/partition_queries'
|
9
8
|
require_relative 'queries/subscription_queries'
|
10
9
|
require_relative 'queries/subscriptions_set_queries'
|
11
10
|
require_relative 'queries/subscription_command_queries'
|
12
11
|
require_relative 'queries/subscriptions_set_command_queries'
|
13
|
-
require_relative 'queries/preloader'
|
14
12
|
|
15
13
|
module PgEventstore
|
16
14
|
# @!visibility private
|
@@ -20,9 +18,9 @@ module PgEventstore
|
|
20
18
|
# @!attribute events
|
21
19
|
# @return [PgEventstore::EventQueries, nil]
|
22
20
|
attribute(:events)
|
23
|
-
# @!attribute
|
24
|
-
# @return [PgEventstore::
|
25
|
-
attribute(:
|
21
|
+
# @!attribute partitions
|
22
|
+
# @return [PgEventstore::PartitionQueries, nil]
|
23
|
+
attribute(:partitions)
|
26
24
|
# @!attribute transactions
|
27
25
|
# @return [PgEventstore::TransactionQueries, nil]
|
28
26
|
attribute(:transactions)
|
@@ -28,7 +28,7 @@ module PgEventstore
|
|
28
28
|
# @return [PgEventstore::QueryBuilders::EventsFiltering]
|
29
29
|
def all_stream_filtering(options)
|
30
30
|
event_filter = new
|
31
|
-
options in { filter: {
|
31
|
+
options in { filter: { event_types: Array => event_type_ids } }
|
32
32
|
event_filter.add_event_types(event_type_ids)
|
33
33
|
event_filter.add_limit(options[:max_count])
|
34
34
|
event_filter.resolve_links(options[:resolve_link_tos])
|
@@ -44,11 +44,11 @@ module PgEventstore
|
|
44
44
|
# @return [PgEventstore::QueryBuilders::EventsFiltering]
|
45
45
|
def specific_stream_filtering(stream, options)
|
46
46
|
event_filter = new
|
47
|
-
options in { filter: {
|
47
|
+
options in { filter: { event_types: Array => event_type_ids } }
|
48
48
|
event_filter.add_event_types(event_type_ids)
|
49
49
|
event_filter.add_limit(options[:max_count])
|
50
50
|
event_filter.resolve_links(options[:resolve_link_tos])
|
51
|
-
event_filter.
|
51
|
+
event_filter.add_stream_attrs(**stream.to_hash)
|
52
52
|
event_filter.add_revision(options[:from_revision], options[:direction])
|
53
53
|
event_filter.add_stream_direction(options[:direction])
|
54
54
|
event_filter
|
@@ -60,8 +60,6 @@ module PgEventstore
|
|
60
60
|
SQLBuilder.new.
|
61
61
|
select('events.*').
|
62
62
|
from('events').
|
63
|
-
join('JOIN streams ON streams.id = events.stream_id').
|
64
|
-
join('JOIN event_types ON event_types.id = events.event_type_id').
|
65
63
|
limit(DEFAULT_LIMIT)
|
66
64
|
end
|
67
65
|
|
@@ -75,27 +73,21 @@ module PgEventstore
|
|
75
73
|
|
76
74
|
stream_attrs.compact!
|
77
75
|
sql = stream_attrs.map do |attr, _|
|
78
|
-
"
|
76
|
+
"events.#{attr} = ?"
|
79
77
|
end.join(" AND ")
|
80
78
|
@sql_builder.where_or(sql, *stream_attrs.values)
|
81
79
|
end
|
82
80
|
|
83
|
-
# @param
|
81
|
+
# @param event_types [Array<String>, nil]
|
84
82
|
# @return [void]
|
85
|
-
def
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
# @param event_type_ids [Array<Integer>, nil]
|
90
|
-
# @return [void]
|
91
|
-
def add_event_types(event_type_ids)
|
92
|
-
return if event_type_ids.nil?
|
93
|
-
return if event_type_ids.empty?
|
83
|
+
def add_event_types(event_types)
|
84
|
+
return if event_types.nil?
|
85
|
+
return if event_types.empty?
|
94
86
|
|
95
|
-
sql =
|
87
|
+
sql = event_types.size.times.map do
|
96
88
|
"?"
|
97
89
|
end.join(", ")
|
98
|
-
@sql_builder.where("events.
|
90
|
+
@sql_builder.where("events.type IN (#{sql})", *event_types)
|
99
91
|
end
|
100
92
|
|
101
93
|
# @param revision [Integer, nil]
|
@@ -4,10 +4,25 @@ module PgEventstore
|
|
4
4
|
module TestHelpers
|
5
5
|
class << self
|
6
6
|
def clean_up_db
|
7
|
+
clean_up_data
|
8
|
+
clean_up_partitions
|
9
|
+
end
|
10
|
+
|
11
|
+
def clean_up_partitions
|
12
|
+
PgEventstore.connection.with do |conn|
|
13
|
+
# Dropping parent partition also drops all child partitions
|
14
|
+
conn.exec("select tablename from pg_tables where tablename like 'contexts_%'").each do |attrs|
|
15
|
+
conn.exec("drop table #{attrs['tablename']}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def clean_up_data
|
7
21
|
tables_to_purge = PgEventstore.connection.with do |conn|
|
8
22
|
conn.exec(<<~SQL)
|
9
23
|
SELECT tablename
|
10
|
-
FROM pg_catalog.pg_tables
|
24
|
+
FROM pg_catalog.pg_tables
|
25
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema') AND tablename != 'migrations'
|
11
26
|
SQL
|
12
27
|
end.map { |attrs| attrs['tablename'] }
|
13
28
|
tables_to_purge.each do |table_name|
|
@@ -9,6 +9,7 @@ module PgEventstore
|
|
9
9
|
@from_value = nil
|
10
10
|
@where_values = { 'AND' => [], 'OR' => [] }
|
11
11
|
@join_values = []
|
12
|
+
@group_values = []
|
12
13
|
@order_values = []
|
13
14
|
@limit_value = nil
|
14
15
|
@offset_value = nil
|
@@ -68,6 +69,11 @@ module PgEventstore
|
|
68
69
|
self
|
69
70
|
end
|
70
71
|
|
72
|
+
def remove_order
|
73
|
+
@order_values.clear
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
71
77
|
# @param limit [Integer]
|
72
78
|
# @return self
|
73
79
|
def limit(limit)
|
@@ -75,6 +81,11 @@ module PgEventstore
|
|
75
81
|
self
|
76
82
|
end
|
77
83
|
|
84
|
+
def remove_limit
|
85
|
+
@limit_value = nil
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
78
89
|
# @param offset [Integer]
|
79
90
|
# @return self
|
80
91
|
def offset(offset)
|
@@ -89,10 +100,22 @@ module PgEventstore
|
|
89
100
|
self
|
90
101
|
end
|
91
102
|
|
92
|
-
|
93
|
-
|
103
|
+
# @param sql [String]
|
104
|
+
# @return self
|
105
|
+
def group(sql)
|
106
|
+
@group_values.push(sql)
|
107
|
+
self
|
108
|
+
end
|
94
109
|
|
95
|
-
|
110
|
+
def remove_group
|
111
|
+
@group_values.clear
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_exec_params
|
116
|
+
@positional_values.clear
|
117
|
+
@positional_values_size = 0
|
118
|
+
_to_exec_params
|
96
119
|
end
|
97
120
|
|
98
121
|
protected
|
@@ -106,6 +129,12 @@ module PgEventstore
|
|
106
129
|
@positional_values_size = val
|
107
130
|
end
|
108
131
|
|
132
|
+
def _to_exec_params
|
133
|
+
return [single_query_sql, @positional_values] if @union_values.empty?
|
134
|
+
|
135
|
+
[union_query_sql, @positional_values]
|
136
|
+
end
|
137
|
+
|
109
138
|
private
|
110
139
|
|
111
140
|
# @return [String]
|
@@ -114,6 +143,7 @@ module PgEventstore
|
|
114
143
|
sql = "SELECT #{select_sql} FROM #{@from_value}"
|
115
144
|
sql += " #{join_sql}" unless @join_values.empty?
|
116
145
|
sql += " WHERE #{where_sql}" unless where_sql.empty?
|
146
|
+
sql += " GROUP BY #{@group_values.join(', ')}" unless @group_values.empty?
|
117
147
|
sql += " ORDER BY #{order_sql}" unless @order_values.empty?
|
118
148
|
sql += " LIMIT #{@limit_value}" if @limit_value
|
119
149
|
sql += " OFFSET #{@offset_value}" if @offset_value
|
@@ -126,7 +156,7 @@ module PgEventstore
|
|
126
156
|
union_parts = ["(#{sql})"]
|
127
157
|
union_parts += @union_values.map do |builder|
|
128
158
|
builder.positional_values_size = @positional_values_size
|
129
|
-
builder_sql, values = builder.
|
159
|
+
builder_sql, values = builder._to_exec_params
|
130
160
|
@positional_values.push(*values)
|
131
161
|
@positional_values_size += values.size
|
132
162
|
"(#{builder_sql})"
|
data/lib/pg_eventstore/stream.rb
CHANGED
@@ -5,7 +5,7 @@ require 'digest/md5'
|
|
5
5
|
module PgEventstore
|
6
6
|
class Stream
|
7
7
|
SYSTEM_STREAM_PREFIX = '$'
|
8
|
-
|
8
|
+
NON_EXISTING_STREAM_REVISION = -1
|
9
9
|
|
10
10
|
class << self
|
11
11
|
# Produces "all" stream instance. "all" stream does not represent any specific stream. Instead, it indicates that
|
@@ -18,20 +18,15 @@ module PgEventstore
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
attr_reader :context, :stream_name, :stream_id
|
22
|
-
attr_accessor :stream_revision
|
21
|
+
attr_reader :context, :stream_name, :stream_id
|
23
22
|
|
24
23
|
# @param context [String]
|
25
24
|
# @param stream_name [String]
|
26
25
|
# @param stream_id [String]
|
27
|
-
|
28
|
-
# @param stream_revision [Integer, nil] current stream revision, read only
|
29
|
-
def initialize(context:, stream_name:, stream_id:, id: nil, stream_revision: nil)
|
26
|
+
def initialize(context:, stream_name:, stream_id:)
|
30
27
|
@context = context
|
31
28
|
@stream_name = stream_name
|
32
29
|
@stream_id = stream_id
|
33
|
-
@id = id
|
34
|
-
@stream_revision = stream_revision
|
35
30
|
end
|
36
31
|
|
37
32
|
# @return [Boolean]
|
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
|
+
version: 0.10.1
|
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-
|
11
|
+
date: 2024-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pg
|
@@ -50,15 +50,15 @@ files:
|
|
50
50
|
- LICENSE.txt
|
51
51
|
- README.md
|
52
52
|
- db/migrations/0_create_extensions.sql
|
53
|
-
- db/migrations/
|
54
|
-
- db/migrations/
|
55
|
-
- db/migrations/
|
56
|
-
- db/migrations/
|
57
|
-
- db/migrations/
|
58
|
-
- db/migrations/6_create_subscriptions_set_commands.sql
|
53
|
+
- db/migrations/1_create_events.sql
|
54
|
+
- db/migrations/2_create_subscriptions.sql
|
55
|
+
- db/migrations/3_create_subscription_commands.sql
|
56
|
+
- db/migrations/4_create_subscriptions_set_commands.sql
|
57
|
+
- db/migrations/5_partitions.sql
|
59
58
|
- docs/appending_events.md
|
60
59
|
- docs/configuration.md
|
61
60
|
- docs/events_and_streams.md
|
61
|
+
- docs/how_it_works.md
|
62
62
|
- docs/linking_events.md
|
63
63
|
- docs/multiple_commands.md
|
64
64
|
- docs/reading_events.md
|
@@ -91,9 +91,7 @@ files:
|
|
91
91
|
- lib/pg_eventstore/pg_connection.rb
|
92
92
|
- lib/pg_eventstore/queries.rb
|
93
93
|
- lib/pg_eventstore/queries/event_queries.rb
|
94
|
-
- lib/pg_eventstore/queries/
|
95
|
-
- lib/pg_eventstore/queries/preloader.rb
|
96
|
-
- lib/pg_eventstore/queries/stream_queries.rb
|
94
|
+
- lib/pg_eventstore/queries/partition_queries.rb
|
97
95
|
- lib/pg_eventstore/queries/subscription_command_queries.rb
|
98
96
|
- lib/pg_eventstore/queries/subscription_queries.rb
|
99
97
|
- lib/pg_eventstore/queries/subscriptions_set_command_queries.rb
|
@@ -1,13 +0,0 @@
|
|
1
|
-
CREATE TABLE public.streams
|
2
|
-
(
|
3
|
-
id bigserial NOT NULL,
|
4
|
-
context character varying COLLATE "POSIX" NOT NULL,
|
5
|
-
stream_name character varying COLLATE "POSIX" NOT NULL,
|
6
|
-
stream_id character varying COLLATE "POSIX" NOT NULL,
|
7
|
-
stream_revision integer DEFAULT '-1'::integer NOT NULL
|
8
|
-
);
|
9
|
-
|
10
|
-
ALTER TABLE ONLY public.streams
|
11
|
-
ADD CONSTRAINT streams_pkey PRIMARY KEY (id);
|
12
|
-
|
13
|
-
CREATE UNIQUE INDEX idx_streams_context_and_stream_name_and_stream_id ON public.streams USING btree (context, stream_name, stream_id);
|
@@ -1,10 +0,0 @@
|
|
1
|
-
CREATE TABLE public.event_types
|
2
|
-
(
|
3
|
-
id bigserial NOT NULL,
|
4
|
-
type character varying COLLATE "POSIX" NOT NULL
|
5
|
-
);
|
6
|
-
|
7
|
-
ALTER TABLE ONLY public.event_types
|
8
|
-
ADD CONSTRAINT event_types_pkey PRIMARY KEY (id);
|
9
|
-
|
10
|
-
CREATE UNIQUE INDEX idx_event_types_type ON public.event_types USING btree (type);
|
@@ -1,74 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module PgEventstore
|
4
|
-
# @!visibility private
|
5
|
-
class EventTypeQueries
|
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 type [String]
|
15
|
-
# @return [Integer] event type's id
|
16
|
-
def find_or_create_type(type)
|
17
|
-
find_type(type) || create_type(type)
|
18
|
-
end
|
19
|
-
|
20
|
-
# @param type [String]
|
21
|
-
# @return [Integer, nil] event type's id
|
22
|
-
def find_type(type)
|
23
|
-
connection.with do |conn|
|
24
|
-
conn.exec_params('SELECT id FROM event_types WHERE type = $1', [type])
|
25
|
-
end.to_a.dig(0, 'id')
|
26
|
-
end
|
27
|
-
|
28
|
-
# @param type [String]
|
29
|
-
# @return [Integer] event type's id
|
30
|
-
def create_type(type)
|
31
|
-
connection.with do |conn|
|
32
|
-
conn.exec_params('INSERT INTO event_types (type) VALUES ($1) RETURNING id', [type])
|
33
|
-
end.to_a.dig(0, 'id')
|
34
|
-
end
|
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
|
-
|
47
|
-
# @param types [Array<String>]
|
48
|
-
# @return [Array<Integer, nil>]
|
49
|
-
def find_event_types(types)
|
50
|
-
connection.with do |conn|
|
51
|
-
conn.exec_params(<<~SQL, [types])
|
52
|
-
SELECT event_types.id, types.type
|
53
|
-
FROM event_types
|
54
|
-
RIGHT JOIN (
|
55
|
-
SELECT unnest($1::varchar[]) as type
|
56
|
-
) types ON types.type = event_types.type
|
57
|
-
SQL
|
58
|
-
end.to_a.map { |attrs| attrs['id'] }
|
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
|
73
|
-
end
|
74
|
-
end
|
@@ -1,37 +0,0 @@
|
|
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
|
@@ -1,77 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module PgEventstore
|
4
|
-
# @!visibility private
|
5
|
-
class StreamQueries
|
6
|
-
attr_reader :connection
|
7
|
-
private :connection
|
8
|
-
|
9
|
-
# @param connection [PgEventstore::Connection]
|
10
|
-
def initialize(connection)
|
11
|
-
@connection = connection
|
12
|
-
end
|
13
|
-
|
14
|
-
# Finds a stream in the database by the given Stream object
|
15
|
-
# @param stream [PgEventstore::Stream]
|
16
|
-
# @return [PgEventstore::Stream, nil] persisted stream
|
17
|
-
def find_stream(stream)
|
18
|
-
builder =
|
19
|
-
SQLBuilder.new.
|
20
|
-
from('streams').
|
21
|
-
where('streams.context = ? AND streams.stream_name = ? AND streams.stream_id = ?', *stream.to_a).
|
22
|
-
limit(1)
|
23
|
-
pg_result = connection.with do |conn|
|
24
|
-
conn.exec_params(*builder.to_exec_params)
|
25
|
-
end
|
26
|
-
deserialize(pg_result) if pg_result.ntuples == 1
|
27
|
-
end
|
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
|
-
|
40
|
-
# @param stream [PgEventstore::Stream]
|
41
|
-
# @return [PgEventstore::RawStream] persisted stream
|
42
|
-
def create_stream(stream)
|
43
|
-
create_sql = <<~SQL
|
44
|
-
INSERT INTO streams (context, stream_name, stream_id) VALUES ($1, $2, $3) RETURNING *
|
45
|
-
SQL
|
46
|
-
pg_result = connection.with do |conn|
|
47
|
-
conn.exec_params(create_sql, stream.to_a)
|
48
|
-
end
|
49
|
-
deserialize(pg_result)
|
50
|
-
end
|
51
|
-
|
52
|
-
# @return [PgEventstore::Stream] persisted stream
|
53
|
-
def find_or_create_stream(stream)
|
54
|
-
find_stream(stream) || create_stream(stream)
|
55
|
-
end
|
56
|
-
|
57
|
-
# @param stream [PgEventstore::Stream] persisted stream
|
58
|
-
# @return [PgEventstore::Stream]
|
59
|
-
def update_stream_revision(stream, revision)
|
60
|
-
connection.with do |conn|
|
61
|
-
conn.exec_params(<<~SQL, [revision, stream.id])
|
62
|
-
UPDATE streams SET stream_revision = $1 WHERE id = $2
|
63
|
-
SQL
|
64
|
-
end
|
65
|
-
stream.stream_revision = revision
|
66
|
-
stream
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
# @param pg_result [PG::Result]
|
72
|
-
# @return [PgEventstore::Stream, nil]
|
73
|
-
def deserialize(pg_result)
|
74
|
-
PgEventstore::Stream.new(**pg_result.to_a.first.transform_keys(&:to_sym))
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
File without changes
|
/data/db/migrations/{5_create_subscription_commands.sql → 3_create_subscription_commands.sql}
RENAMED
File without changes
|
File without changes
|