pg_eventstore 0.2.3 → 0.2.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd2524ab67ce1fa009de14a032cdc3d922051420bc2c02e933cb63ee245db02d
4
- data.tar.gz: f49867dab08825770aae44e3e1bec38a8144504e3bea2e6618ce47a0d6e8cb54
3
+ metadata.gz: bc158c2f99fee36514e1902199691dfb93e882ecdebe4738bbcf787fadc8f514
4
+ data.tar.gz: a70d00d4bdaef48223e35234f01d938a713f9306aa1bfb45bde99733b3654c39
5
5
  SHA512:
6
- metadata.gz: e9925413067ca47159cbd5902f2716bbd9e5fe1e0c63aa7397f077c95878bacc2da57cd65f32160a0e1d70d855e101d16fb6d5aa5b537c9630c4290449db8c16
7
- data.tar.gz: 65add6ce6e11ebe0634cec3949709ad3c8d42b105ba9325c5d6d45c45bf2339c0e47f9286dbb71edc8e1c99636aa449adc34926f4a6d57c695c50655609d5451
6
+ metadata.gz: bf38d241b001b4244d9fa915d2dfcabec7ac9456aa83832b8d850fe3fd34e48b0ef8b9ef5f0cca91b776124ec3333dea658220f3bd427309b3b1ae09a12714ae
7
+ data.tar.gz: af5b5e4394ee696790145852ac6a7761728e5cc7e7aca127e6cde112b857e5516b121ac2bb7bf2280e7e66c93cf0823e33bcbe1f258cadbff31d50f83d9972a8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.2.4] - 2023-12-20
2
+
3
+ Due to performance issues under certain circumstances, searching by event type was giving bad performance. I decided to extract `type` column from `events` table into separated table. **No breaking changes in public API though.**
4
+
5
+ **Warning** The migrations this version has, requires you to shut down applications that use `pg_eventstore` and only then run `rake pg_eventstore:migrate`.
6
+
1
7
  ## [0.2.3] - 2023-12-18
2
8
 
3
9
  - Fix performance when searching by event type only(under certain circumstances PosetgreSQL was picking wrong index).
@@ -0,0 +1,17 @@
1
+ CREATE TABLE public.event_types
2
+ (
3
+ id bigserial NOT NULL,
4
+ type character varying NOT NULL
5
+ );
6
+
7
+ ALTER TABLE ONLY public.events ADD COLUMN event_type_id bigint;
8
+
9
+ ALTER TABLE ONLY public.event_types
10
+ ADD CONSTRAINT event_types_pkey PRIMARY KEY (id);
11
+
12
+ ALTER TABLE ONLY public.events
13
+ ADD CONSTRAINT events_event_type_fk FOREIGN KEY (event_type_id)
14
+ REFERENCES public.event_types (id);
15
+
16
+ CREATE UNIQUE INDEX idx_event_types_type ON public.event_types USING btree (type);
17
+ CREATE INDEX idx_events_event_type_id ON public.events USING btree (event_type_id);
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ PgEventstore.connection.with do |conn|
4
+ types = conn.exec('select type from events group by type').to_a.map { |attrs| attrs['type'] }
5
+ types.each.with_index(1) do |type, index|
6
+ id = conn.exec_params('SELECT id FROM event_types WHERE type = $1', [type]).to_a.first['id']
7
+ id ||= conn.exec_params('INSERT INTO event_types (type) VALUES ($1) RETURNING *', [type]).to_a.first['id']
8
+ conn.exec_params('UPDATE events SET event_type_id = $1 WHERE type = $2 AND event_type_id IS NULL', [id, type])
9
+ puts "Processed #{index} types of #{types.size}"
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ CREATE INDEX idx_events_global_position ON public.events USING btree (global_position);
2
+
3
+ DROP INDEX idx_events_stream_id_and_type_and_revision;
4
+ DROP INDEX idx_events_type_and_stream_id_and_position;
5
+ DROP INDEX idx_events_global_position_including_type;
6
+ DROP INDEX idx_events_type_and_position;
@@ -0,0 +1 @@
1
+ ALTER TABLE public.events ALTER COLUMN event_type_id SET NOT NULL;
@@ -0,0 +1 @@
1
+ ALTER TABLE public.events ALTER COLUMN type DROP NOT NULL;
@@ -22,6 +22,7 @@ module PgEventstore
22
22
  # @param options [Hash]
23
23
  # @return [Array<PgEventstore::Event>]
24
24
  def stream_events(stream, options)
25
+ options = include_event_types_ids(options)
25
26
  exec_params = events_filtering(stream, options).to_exec_params
26
27
  pg_result = connection.with do |conn|
27
28
  conn.exec_params(*exec_params)
@@ -35,17 +36,18 @@ module PgEventstore
35
36
  def insert(stream, event)
36
37
  serializer.serialize(event)
37
38
 
38
- attributes = event.options_hash.slice(:id, :type, :data, :metadata, :stream_revision, :link_id).compact
39
+ attributes = event.options_hash.slice(:id, :data, :metadata, :stream_revision, :link_id).compact
39
40
  attributes[:stream_id] = stream.id
41
+ attributes[:event_type_id] = event_type_queries.find_or_create_type(event.type)
40
42
 
41
43
  sql = <<~SQL
42
44
  INSERT INTO events (#{attributes.keys.join(', ')})
43
- VALUES (#{(1..attributes.values.size).map { |n| "$#{n}" }.join(', ')})
44
- RETURNING *
45
+ VALUES (#{positional_vars(attributes.values)})
46
+ RETURNING *, $#{attributes.values.size + 1} as type
45
47
  SQL
46
48
 
47
49
  pg_result = connection.with do |conn|
48
- conn.exec_params(sql, attributes.values)
50
+ conn.exec_params(sql, [*attributes.values, event.type])
49
51
  end
50
52
  deserializer.without_middlewares.deserialize_one(pg_result).tap do |persisted_event|
51
53
  persisted_event.stream = stream
@@ -63,5 +65,29 @@ module PgEventstore
63
65
 
64
66
  QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options, offset: offset)
65
67
  end
68
+
69
+ # Replaces filter by event type strings with filter by event type ids
70
+ # @param options [Hash]
71
+ # @return [Hash]
72
+ def include_event_types_ids(options)
73
+ options in { filter: { event_types: Array => event_types } }
74
+ return options unless event_types
75
+
76
+ filter = options[:filter].dup
77
+ filter[:event_type_ids] = event_type_queries.find_event_types(event_types).uniq
78
+ filter.delete(:event_types)
79
+ options.merge(filter: filter)
80
+ end
81
+
82
+ # @param array [Array]
83
+ # @return [String] positional variables, based on array size. Example: "$1, $2, $3"
84
+ def positional_vars(array)
85
+ array.size.times.map { |t| "$#{t + 1}" }.join(', ')
86
+ end
87
+
88
+ # @return [PgEventstore::EventTypeQueries]
89
+ def event_type_queries
90
+ EventTypeQueries.new(connection)
91
+ end
66
92
  end
67
93
  end
@@ -0,0 +1,50 @@
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 types [Array<String>]
37
+ # @return [Array<Integer, nil>]
38
+ def find_event_types(types)
39
+ connection.with do |conn|
40
+ conn.exec_params(<<~SQL, [types])
41
+ SELECT event_types.id, types.type
42
+ FROM event_types
43
+ RIGHT JOIN (
44
+ SELECT unnest($1::varchar[]) type
45
+ ) types ON types.type = event_types.type
46
+ SQL
47
+ end.to_a.map { |attrs| attrs['id'] }
48
+ end
49
+ end
50
+ end
@@ -19,14 +19,23 @@ module PgEventstore
19
19
  next yield
20
20
  end
21
21
 
22
- conn.transaction do
23
- conn.exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
22
+ pg_transaction(conn) do
24
23
  yield
25
24
  end
26
25
  end
27
- rescue PG::TRSerializationFailure, PG::TRDeadlockDetected => e
28
- retry if [PG::PQTRANS_IDLE, PG::PQTRANS_UNKNOWN].include?(e.connection.transaction_status)
29
- raise
26
+ end
27
+
28
+ private
29
+
30
+ # @param pg_connection [PG::Connection]
31
+ # @return [void]
32
+ def pg_transaction(pg_connection)
33
+ pg_connection.transaction do
34
+ pg_connection.exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
35
+ yield
36
+ end
37
+ rescue PG::TRSerializationFailure, PG::TRDeadlockDetected
38
+ retry
30
39
  end
31
40
  end
32
41
  end
@@ -3,6 +3,7 @@
3
3
  require_relative 'queries/transaction_queries'
4
4
  require_relative 'queries/event_queries'
5
5
  require_relative 'queries/stream_queries'
6
+ require_relative 'queries/event_type_queries'
6
7
 
7
8
  module PgEventstore
8
9
  # @!visibility private
@@ -23,8 +23,8 @@ module PgEventstore
23
23
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
24
24
  def all_stream_filtering(options, offset: 0)
25
25
  event_filter = new
26
- options in { filter: { event_types: Array => event_types } }
27
- event_filter.add_event_types(event_types)
26
+ options in { filter: { event_type_ids: Array => event_type_ids } }
27
+ event_filter.add_event_types(event_type_ids)
28
28
  event_filter.add_limit(options[:max_count])
29
29
  event_filter.add_offset(offset)
30
30
  event_filter.resolve_links(options[:resolve_link_tos])
@@ -41,8 +41,8 @@ module PgEventstore
41
41
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
42
42
  def specific_stream_filtering(stream, options, offset: 0)
43
43
  event_filter = new
44
- options in { filter: { event_types: Array => event_types } }
45
- event_filter.add_event_types(event_types)
44
+ options in { filter: { event_type_ids: Array => event_type_ids } }
45
+ event_filter.add_event_types(event_type_ids)
46
46
  event_filter.add_limit(options[:max_count])
47
47
  event_filter.add_offset(offset)
48
48
  event_filter.resolve_links(options[:resolve_link_tos])
@@ -58,8 +58,10 @@ module PgEventstore
58
58
  SQLBuilder.new.
59
59
  select('events.*').
60
60
  select('row_to_json(streams.*) as stream').
61
+ select('event_types.type as type').
61
62
  from('events').
62
63
  join('JOIN streams ON streams.id = events.stream_id').
64
+ join('JOIN event_types ON event_types.id = events.event_type_id').
63
65
  limit(DEFAULT_LIMIT).
64
66
  offset(DEFAULT_OFFSET)
65
67
  end
@@ -85,16 +87,16 @@ module PgEventstore
85
87
  @sql_builder.where("streams.id = ?", stream.id)
86
88
  end
87
89
 
88
- # @param event_types [Array, nil]
90
+ # @param event_type_ids [Array<Integer>, nil]
89
91
  # @return [void]
90
- def add_event_types(event_types)
91
- return if event_types.nil?
92
- return if event_types.empty?
93
-
94
- sql = event_types.size.times.map do
95
- "events.type = ?"
96
- end.join(" OR ")
97
- @sql_builder.where(sql, *event_types)
92
+ def add_event_types(event_type_ids)
93
+ return if event_type_ids.nil?
94
+ return if event_type_ids.empty?
95
+
96
+ sql = event_type_ids.size.times.map do
97
+ "?"
98
+ end.join(", ")
99
+ @sql_builder.where("event_types.id IN (#{sql})", *event_type_ids)
98
100
  end
99
101
 
100
102
  # @param revision [Integer, nil]
@@ -31,12 +31,16 @@ namespace :pg_eventstore do
31
31
  latest_migration =
32
32
  conn.exec('SELECT number FROM migrations ORDER BY number DESC LIMIT 1').to_a.dig(0, 'number') || -1
33
33
 
34
- Dir["#{migration_files_root}/*.sql"].each do |f_name|
35
- number = File.basename(f_name).split('_')[0].to_i
36
- next if latest_migration >= number
34
+ Dir.chdir migration_files_root do
35
+ Dir["*.{sql,rb}"].each do |f_name|
36
+ number = File.basename(f_name).split('_')[0].to_i
37
+ next if latest_migration >= number
37
38
 
38
- conn.transaction do
39
- conn.exec(File.read(f_name))
39
+ if File.extname(f_name) == '.rb'
40
+ load f_name
41
+ else
42
+ conn.exec(File.read(f_name))
43
+ end
40
44
  conn.exec_params('INSERT INTO migrations (number) VALUES ($1)', [number])
41
45
  end
42
46
  end
@@ -53,6 +57,7 @@ namespace :pg_eventstore do
53
57
  conn.exec <<~SQL
54
58
  DROP TABLE IF EXISTS public.events;
55
59
  DROP TABLE IF EXISTS public.streams;
60
+ DROP TABLE IF EXISTS public.event_types;
56
61
  DROP TABLE IF EXISTS public.migrations;
57
62
  DROP EXTENSION IF EXISTS "uuid-ossp";
58
63
  DROP EXTENSION IF EXISTS pgcrypto;
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgEventstore
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.4"
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.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Dzyzenko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-12-18 00:00:00.000000000 Z
11
+ date: 2023-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -56,6 +56,11 @@ files:
56
56
  - db/migrations/0_improve_all_stream_indexes.sql
57
57
  - db/migrations/1_improve_specific_stream_indexes.sql
58
58
  - db/migrations/2_adjust_global_position_index.sql
59
+ - db/migrations/3_extract_type_into_separate_table.sql
60
+ - db/migrations/4_populate_event_types.rb
61
+ - db/migrations/5_adjust_indexes.sql
62
+ - db/migrations/6_change_events_event_type_id_null_constraint.sql
63
+ - db/migrations/7_change_events_type_constraint.sql
59
64
  - docs/appending_events.md
60
65
  - docs/configuration.md
61
66
  - docs/events_and_streams.md
@@ -80,6 +85,7 @@ files:
80
85
  - lib/pg_eventstore/pg_result_deserializer.rb
81
86
  - lib/pg_eventstore/queries.rb
82
87
  - lib/pg_eventstore/queries/event_queries.rb
88
+ - lib/pg_eventstore/queries/event_type_queries.rb
83
89
  - lib/pg_eventstore/queries/stream_queries.rb
84
90
  - lib/pg_eventstore/queries/transaction_queries.rb
85
91
  - lib/pg_eventstore/query_builders/events_filtering_query.rb