pg_eventstore 0.2.3 → 0.2.4

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: 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