pg_eventstore 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +90 -0
- data/db/extensions.sql +2 -0
- data/db/indexes.sql +13 -0
- data/db/primary_and_foreign_keys.sql +11 -0
- data/db/tables.sql +21 -0
- data/docs/appending_events.md +170 -0
- data/docs/configuration.md +82 -0
- data/docs/events_and_streams.md +45 -0
- data/docs/multiple_commands.md +46 -0
- data/docs/reading_events.md +161 -0
- data/docs/writing_middleware.md +160 -0
- data/lib/pg_eventstore/abstract_command.rb +18 -0
- data/lib/pg_eventstore/client.rb +133 -0
- data/lib/pg_eventstore/commands/append.rb +61 -0
- data/lib/pg_eventstore/commands/multiple.rb +14 -0
- data/lib/pg_eventstore/commands/read.rb +24 -0
- data/lib/pg_eventstore/commands.rb +6 -0
- data/lib/pg_eventstore/config.rb +30 -0
- data/lib/pg_eventstore/connection.rb +97 -0
- data/lib/pg_eventstore/errors.rb +107 -0
- data/lib/pg_eventstore/event.rb +59 -0
- data/lib/pg_eventstore/event_class_resolver.rb +17 -0
- data/lib/pg_eventstore/event_serializer.rb +27 -0
- data/lib/pg_eventstore/extensions/options_extension.rb +103 -0
- data/lib/pg_eventstore/middleware.rb +15 -0
- data/lib/pg_eventstore/pg_result_deserializer.rb +48 -0
- data/lib/pg_eventstore/queries.rb +127 -0
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +187 -0
- data/lib/pg_eventstore/rspec/has_option_matcher.rb +90 -0
- data/lib/pg_eventstore/sql_builder.rb +126 -0
- data/lib/pg_eventstore/stream.rb +72 -0
- data/lib/pg_eventstore/tasks/setup.rake +37 -0
- data/lib/pg_eventstore/version.rb +5 -0
- data/lib/pg_eventstore.rb +85 -0
- data/pg_eventstore.gemspec +40 -0
- metadata +113 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pg'
|
4
|
+
require 'pg/basic_type_map_for_results'
|
5
|
+
require 'pg/basic_type_map_for_queries'
|
6
|
+
require 'connection_pool'
|
7
|
+
|
8
|
+
module PgEventstore
|
9
|
+
class Connection
|
10
|
+
# Starting from ruby v3.1 ConnectionPool closes connections after forking by default. For ruby v3 we need this patch
|
11
|
+
# to correctly reload the ConnectionPool. Otherwise the same connection will leak into another process which will
|
12
|
+
# result in disaster.
|
13
|
+
# @!visibility private
|
14
|
+
module Ruby30Patch
|
15
|
+
def initialize(**)
|
16
|
+
@current_pid = Process.pid
|
17
|
+
@mutext = Mutex.new
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def with(&blk)
|
22
|
+
reload_after_fork
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def reload_after_fork
|
29
|
+
return if @current_pid == Process.pid
|
30
|
+
|
31
|
+
@mutext.synchronize do
|
32
|
+
return if @current_pid == Process.pid
|
33
|
+
|
34
|
+
@pool.reload(&:close)
|
35
|
+
@current_pid = Process.pid
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :uri, :pool_size, :pool_timeout
|
41
|
+
private :uri, :pool_size, :pool_timeout
|
42
|
+
|
43
|
+
# @param uri [String] PostgreSQL connection URI.
|
44
|
+
# Example: "postgresql://postgres:postgres@localhost:5432/eventstore"
|
45
|
+
# @param pool_size [Integer] Connection pool size
|
46
|
+
# @param pool_timeout [Integer] Connection pool timeout in seconds
|
47
|
+
def initialize(uri:, pool_size: 5, pool_timeout: 5)
|
48
|
+
@uri = uri
|
49
|
+
@pool_size = pool_size
|
50
|
+
@pool_timeout = pool_timeout
|
51
|
+
init_pool
|
52
|
+
end
|
53
|
+
|
54
|
+
# A shorthand from ConnectionPool#with.
|
55
|
+
# @yieldparam connection [PG::Connection] PostgreSQL connection instance
|
56
|
+
# @return [Object] a value of a given block
|
57
|
+
def with(&blk)
|
58
|
+
should_retry = true
|
59
|
+
@pool.with do |conn|
|
60
|
+
yield conn
|
61
|
+
rescue PG::ConnectionBad
|
62
|
+
# Recover connection after fork. We do it only once and without any delay. Recover is required by both
|
63
|
+
# processes - child process and parent process
|
64
|
+
conn.sync_reset
|
65
|
+
raise unless should_retry
|
66
|
+
should_retry = false
|
67
|
+
retry
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# @return [ConnectionPool]
|
74
|
+
def init_pool
|
75
|
+
@pool ||= ConnectionPool.new(size: pool_size, timeout: pool_timeout) do
|
76
|
+
PG::Connection.new(uri).tap do |conn|
|
77
|
+
conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn, registry: pg_type_registry)
|
78
|
+
conn.type_map_for_queries = PG::BasicTypeMapForQueries.new(conn, registry: pg_type_registry)
|
79
|
+
# conn.trace($stdout) # logs
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [PG::BasicTypeRegistry]
|
85
|
+
def pg_type_registry
|
86
|
+
registry = PG::BasicTypeRegistry.new.register_default_types
|
87
|
+
# 0 means that the pg value format is a text(1 for binary)
|
88
|
+
registry.alias_type(0, 'uuid', 'text')
|
89
|
+
registry.register_type 0, 'timestamp', PG::TextEncoder::TimestampUtc, PG::TextDecoder::TimestampUtc
|
90
|
+
registry
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.1')
|
96
|
+
PgEventstore::Connection.prepend(PgEventstore::Connection::Ruby30Patch)
|
97
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
class Error < StandardError
|
5
|
+
# @return [Hash]
|
6
|
+
def as_json(*)
|
7
|
+
to_h.transform_keys(&:to_s)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @return [Hash]
|
11
|
+
def to_h
|
12
|
+
hash =
|
13
|
+
instance_variables.each_with_object({}) do |var, result|
|
14
|
+
key = var.to_s
|
15
|
+
key[0] = '' # remove @ sign
|
16
|
+
result[key.to_sym] = instance_variable_get(var)
|
17
|
+
end
|
18
|
+
hash[:message] = message
|
19
|
+
hash[:backtrace] = backtrace
|
20
|
+
hash
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class StreamNotFoundError < Error
|
25
|
+
attr_reader :stream
|
26
|
+
|
27
|
+
# @param stream [PgEventstore::Stream]
|
28
|
+
def initialize(stream)
|
29
|
+
@stream = stream
|
30
|
+
super("Stream #{stream.inspect} does not exist.")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class SystemStreamError < Error
|
35
|
+
attr_reader :stream
|
36
|
+
|
37
|
+
# @param stream [PgEventstore::Stream]
|
38
|
+
def initialize(stream)
|
39
|
+
@stream = stream
|
40
|
+
super("Stream #{stream.inspect} is a system stream and can't be used to append events.")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class WrongExpectedRevisionError < Error
|
45
|
+
attr_reader :revision, :expected_revision
|
46
|
+
|
47
|
+
# @param revision [Integer]
|
48
|
+
# @param expected_revision [Integer, Symbol]
|
49
|
+
def initialize(revision, expected_revision)
|
50
|
+
@revision = revision
|
51
|
+
@expected_revision = expected_revision
|
52
|
+
super(user_friendly_message)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# @return [String]
|
58
|
+
def user_friendly_message
|
59
|
+
return expected_stream_exists if revision == -1 && expected_revision == :stream_exists
|
60
|
+
return expected_no_stream if revision > -1 && expected_revision == :no_stream
|
61
|
+
return current_no_stream if revision == -1 && expected_revision.is_a?(Integer)
|
62
|
+
|
63
|
+
unmatched_stream_revision
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [String]
|
67
|
+
def expected_stream_exists
|
68
|
+
"Expected stream to exist, but it doesn't."
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [String]
|
72
|
+
def expected_no_stream
|
73
|
+
"Expected stream to be absent, but it actually exists."
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [String]
|
77
|
+
def current_no_stream
|
78
|
+
"Stream revision #{expected_revision} is expected, but stream does not exist."
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [String]
|
82
|
+
def unmatched_stream_revision
|
83
|
+
"Stream revision #{expected_revision} is expected, but actual stream revision is #{revision.inspect}."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class StreamDeletionError < Error
|
88
|
+
attr_reader :stream_name, :details
|
89
|
+
|
90
|
+
# @param stream_name [String]
|
91
|
+
# @param details [String]
|
92
|
+
def initialize(stream_name, details:)
|
93
|
+
@stream_name = stream_name
|
94
|
+
@details = details
|
95
|
+
super(user_friendly_message)
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [String]
|
99
|
+
def user_friendly_message
|
100
|
+
<<~TEXT.strip
|
101
|
+
Could not delete #{stream_name.inspect} stream. It seems that a stream with that \
|
102
|
+
name does not exist, has already been deleted or its state does not match the \
|
103
|
+
provided :expected_revision option. Please check #details for more info.
|
104
|
+
TEXT
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
class Event
|
5
|
+
include Extensions::OptionsExtension
|
6
|
+
|
7
|
+
LINK_TYPE = '$>'
|
8
|
+
|
9
|
+
# @!attribute id
|
10
|
+
# @return [String] UUIDv4 string
|
11
|
+
attribute(:id)
|
12
|
+
# @!attribute type
|
13
|
+
# @return [String] event type
|
14
|
+
attribute(:type) { self.class.name }
|
15
|
+
# @!attribute global_position
|
16
|
+
# @return [Integer] event's position in "all" stream
|
17
|
+
attribute(:global_position)
|
18
|
+
# @!attribute stream
|
19
|
+
# @return [PgEventstore::Stream, nil] event's stream
|
20
|
+
attribute(:stream)
|
21
|
+
# @!attribute stream_revision
|
22
|
+
# @return [Integer] a revision of an event inside event's stream
|
23
|
+
attribute(:stream_revision)
|
24
|
+
# @!attribute data
|
25
|
+
# @return [Hash] event's data
|
26
|
+
attribute(:data) { {} }
|
27
|
+
# @!attribute metadata
|
28
|
+
# @return [Hash] event's metadata
|
29
|
+
attribute(:metadata) { {} }
|
30
|
+
# @!attribute link_id
|
31
|
+
# @return [String, nil] UUIDv4 of an event the current event points to. If it is not nil, then the current
|
32
|
+
# event is a link
|
33
|
+
attribute(:link_id)
|
34
|
+
# @!attribute created_at
|
35
|
+
# @return [Time, nil] a timestamp an event was created at
|
36
|
+
attribute(:created_at)
|
37
|
+
|
38
|
+
# Implements comparison of `PgEventstore::Event`-s. Two events matches if all of their attributes matches
|
39
|
+
# @param other [Object, EventStoreClient::DeserializedEvent]
|
40
|
+
# @return [Boolean]
|
41
|
+
def ==(other)
|
42
|
+
return false unless other.is_a?(PgEventstore::Event)
|
43
|
+
|
44
|
+
attributes_hash == other.attributes_hash
|
45
|
+
end
|
46
|
+
|
47
|
+
# Detect whether an event is a link event
|
48
|
+
# @return [Boolean]
|
49
|
+
def link?
|
50
|
+
!link_id.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Detect whether an event is a system event
|
54
|
+
# @return [Boolean]
|
55
|
+
def system?
|
56
|
+
type.start_with?('$')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
class EventClassResolver
|
5
|
+
# @param event_type [String]
|
6
|
+
# @return [Class]
|
7
|
+
def call(event_type)
|
8
|
+
Object.const_get(event_type)
|
9
|
+
rescue NameError, TypeError
|
10
|
+
PgEventstore.logger&.debug(<<~TEXT.strip)
|
11
|
+
Unable to resolve class by `#{event_type}' event type. \
|
12
|
+
Picking #{Event} event class to instantiate the event.
|
13
|
+
TEXT
|
14
|
+
Event
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# @!visibility private
|
5
|
+
class EventSerializer
|
6
|
+
attr_reader :middlewares
|
7
|
+
|
8
|
+
# @param middlewares [Array<#deserialize, #serialize>]
|
9
|
+
def initialize(middlewares)
|
10
|
+
@middlewares = middlewares
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param event [PgEventstore::Event]
|
14
|
+
# @return [PgEventstore::Event]
|
15
|
+
def serialize(event)
|
16
|
+
@middlewares.each do |middleware|
|
17
|
+
middleware.serialize(event)
|
18
|
+
end
|
19
|
+
event
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [PgEventstore::EventSerializer]
|
23
|
+
def without_middlewares
|
24
|
+
self.class.new([])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module PgEventstore
|
6
|
+
module Extensions
|
7
|
+
# A very simple extension that implements a DSL for adding attr_accessors with default values,
|
8
|
+
# and assigning their values during object initialization.
|
9
|
+
# Example. Let's say you frequently do something like this:
|
10
|
+
# ```ruby
|
11
|
+
# class SomeClass
|
12
|
+
# attr_accessor :attr1, :attr2, :attr3, :attr4
|
13
|
+
#
|
14
|
+
# def initialize(opts = {})
|
15
|
+
# @attr1 = opts[:attr1] || 'Attr 1 value'
|
16
|
+
# @attr2 = opts[:attr2] || 'Attr 2 value'
|
17
|
+
# @attr3 = opts[:attr3] || do_some_calc
|
18
|
+
# @attr4 = opts[:attr4]
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# def do_some_calc
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# SomeClass.new(attr1: 'hihi', attr4: 'byebye')
|
26
|
+
# ```
|
27
|
+
#
|
28
|
+
# You can replace the code above using the OptionsExtension:
|
29
|
+
# ```ruby
|
30
|
+
# class SomeClass
|
31
|
+
# include PgEventstore::Extensions::OptionsExtension
|
32
|
+
#
|
33
|
+
# option(:attr1) { 'Attr 1 value' }
|
34
|
+
# option(:attr2) { 'Attr 2 value' }
|
35
|
+
# option(:attr3) { do_some_calc }
|
36
|
+
# option(:attr4)
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# SomeClass.new(attr1: 'hihi', attr4: 'byebye')
|
40
|
+
# ```
|
41
|
+
module OptionsExtension
|
42
|
+
module ClassMethods
|
43
|
+
# @param opt_name [Symbol] option name
|
44
|
+
# @param blk [Proc] provide define value using block. It will be later evaluated in the
|
45
|
+
# context of your object to determine the default value of the option
|
46
|
+
# @return [Symbol]
|
47
|
+
def option(opt_name, &blk)
|
48
|
+
self.options = (options + Set.new([opt_name])).freeze
|
49
|
+
warn_already_defined(opt_name)
|
50
|
+
warn_already_defined(:"#{opt_name}=")
|
51
|
+
attr_writer opt_name
|
52
|
+
|
53
|
+
define_method opt_name do
|
54
|
+
result = instance_variable_get(:"@#{opt_name}")
|
55
|
+
return result if instance_variable_defined?(:"@#{opt_name}")
|
56
|
+
|
57
|
+
instance_exec(&blk) if blk
|
58
|
+
end
|
59
|
+
end
|
60
|
+
alias attribute option
|
61
|
+
|
62
|
+
def inherited(klass)
|
63
|
+
super
|
64
|
+
klass.options = Set.new(options).freeze
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# @param method_name [Symbol]
|
70
|
+
# @return [void]
|
71
|
+
def warn_already_defined(method_name)
|
72
|
+
return unless instance_methods.include?(method_name)
|
73
|
+
|
74
|
+
puts "Warning: Redefining already defined method #{self}##{method_name}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.included(klass)
|
79
|
+
klass.singleton_class.attr_accessor(:options)
|
80
|
+
klass.options = Set.new.freeze
|
81
|
+
klass.extend(ClassMethods)
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize(**options)
|
85
|
+
self.class.options.each do |option|
|
86
|
+
# init default values of options
|
87
|
+
value = options.key?(option) ? options[option] : public_send(option)
|
88
|
+
public_send("#{option}=", value)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Construct a hash from options, where key is the option's name and the value is option's
|
93
|
+
# value
|
94
|
+
# @return [Hash]
|
95
|
+
def options_hash
|
96
|
+
self.class.options.each_with_object({}) do |option, res|
|
97
|
+
res[option] = public_send(option)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
alias attributes_hash options_hash
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Middleware
|
5
|
+
# @param event [PgEventstore::Event]
|
6
|
+
# @return [void]
|
7
|
+
def serialize(event)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param event [PgEventstore::Event]
|
11
|
+
# @return [void]
|
12
|
+
def deserialize(event)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# @!visibility private
|
5
|
+
class PgResultDeserializer
|
6
|
+
attr_reader :middlewares, :event_class_resolver
|
7
|
+
|
8
|
+
# @param middlewares [Array<Object<#deserialize, #serialize>>]
|
9
|
+
# @param event_class_resolver [#call]
|
10
|
+
def initialize(middlewares, event_class_resolver)
|
11
|
+
@middlewares = middlewares
|
12
|
+
@event_class_resolver = event_class_resolver
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param pg_result [PG::Result]
|
16
|
+
# @return [Array<PgEventstore::Event>]
|
17
|
+
def deserialize(pg_result)
|
18
|
+
pg_result.map(&method(:_deserialize))
|
19
|
+
end
|
20
|
+
alias deserialize_many deserialize
|
21
|
+
|
22
|
+
# @param pg_result [PG::Result]
|
23
|
+
# @return [PgEventstore::Event, nil]
|
24
|
+
def deserialize_one(pg_result)
|
25
|
+
return if pg_result.ntuples.zero?
|
26
|
+
|
27
|
+
_deserialize(pg_result.first)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [PgEventstore::PgResultDeserializer]
|
31
|
+
def without_middlewares
|
32
|
+
self.class.new([], event_class_resolver)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# @param attrs [Hash]
|
38
|
+
# @return [PgEventstore::Event]
|
39
|
+
def _deserialize(attrs)
|
40
|
+
event = event_class_resolver.call(attrs['type']).new(**attrs.transform_keys(&:to_sym))
|
41
|
+
middlewares.each do |middleware|
|
42
|
+
middleware.deserialize(event)
|
43
|
+
end
|
44
|
+
event.stream = PgEventstore::Stream.new(**attrs['stream'].transform_keys(&:to_sym)) if attrs.key?('stream')
|
45
|
+
event
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'query_builders/events_filtering_query'
|
4
|
+
|
5
|
+
module PgEventstore
|
6
|
+
# @!visibility private
|
7
|
+
class Queries
|
8
|
+
attr_reader :connection, :serializer, :deserializer
|
9
|
+
private :connection, :serializer, :deserializer
|
10
|
+
|
11
|
+
# @param connection [PgEventstore::Connection]
|
12
|
+
# @param serializer [PgEventstore::EventSerializer]
|
13
|
+
# @param deserializer [PgEventstore::PgResultDeserializer]
|
14
|
+
def initialize(connection, serializer, deserializer)
|
15
|
+
@connection = connection
|
16
|
+
@serializer = serializer
|
17
|
+
@deserializer = deserializer
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [void]
|
21
|
+
def transaction
|
22
|
+
connection.with do |conn|
|
23
|
+
# We are inside a transaction already - no need to start another one
|
24
|
+
if [PG::PQTRANS_ACTIVE, PG::PQTRANS_INTRANS].include?(conn.transaction_status)
|
25
|
+
next yield
|
26
|
+
end
|
27
|
+
|
28
|
+
conn.transaction do
|
29
|
+
conn.exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
|
30
|
+
yield
|
31
|
+
end
|
32
|
+
end
|
33
|
+
rescue PG::TRSerializationFailure, PG::TRDeadlockDetected => e
|
34
|
+
retry if e.connection.transaction_status == PG::PQTRANS_IDLE
|
35
|
+
raise
|
36
|
+
end
|
37
|
+
|
38
|
+
# Finds a stream in the database by the given Stream object
|
39
|
+
# @param stream [PgEventstore::Stream]
|
40
|
+
# @return [PgEventstore::Stream, nil] persisted stream
|
41
|
+
def find_stream(stream)
|
42
|
+
builder =
|
43
|
+
SQLBuilder.new.
|
44
|
+
from('streams').
|
45
|
+
where('streams.context = ? AND streams.stream_name = ? AND streams.stream_id = ?', *stream.to_a).
|
46
|
+
limit(1)
|
47
|
+
pg_result = connection.with do |conn|
|
48
|
+
conn.exec_params(*builder.to_exec_params)
|
49
|
+
end
|
50
|
+
PgEventstore::Stream.new(**pg_result.to_a.first.transform_keys(&:to_sym)) if pg_result.ntuples == 1
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param stream [PgEventstore::Stream]
|
54
|
+
# @return [PgEventstore::RawStream] persisted stream
|
55
|
+
def create_stream(stream)
|
56
|
+
create_sql = <<~SQL
|
57
|
+
INSERT INTO streams (context, stream_name, stream_id) VALUES ($1, $2, $3) RETURNING *
|
58
|
+
SQL
|
59
|
+
pg_result = connection.with do |conn|
|
60
|
+
conn.exec_params(create_sql, stream.to_a)
|
61
|
+
end
|
62
|
+
PgEventstore::Stream.new(**pg_result.to_a.first.transform_keys(&:to_sym))
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [PgEventstore::Stream] persisted stream
|
66
|
+
def find_or_create_stream(stream)
|
67
|
+
find_stream(stream) || create_stream(stream)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @see PgEventstore::Client#read for more info
|
71
|
+
# @param stream [PgEventstore::Stream]
|
72
|
+
# @param options [Hash]
|
73
|
+
# @return [Array<PgEventstore::Event>]
|
74
|
+
def stream_events(stream, options)
|
75
|
+
exec_params = events_filtering(stream, options).to_exec_params
|
76
|
+
pg_result = connection.with do |conn|
|
77
|
+
conn.exec_params(*exec_params)
|
78
|
+
end
|
79
|
+
deserializer.deserialize_many(pg_result)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @param stream [PgEventstore::Stream] persisted stream
|
83
|
+
# @param event [PgEventstore::Event]
|
84
|
+
# @return [PgEventstore::Event]
|
85
|
+
def insert(stream, event)
|
86
|
+
serializer.serialize(event)
|
87
|
+
|
88
|
+
attributes = event.options_hash.slice(:id, :type, :data, :metadata, :stream_revision, :link_id).compact
|
89
|
+
attributes[:stream_id] = stream.id
|
90
|
+
|
91
|
+
sql = <<~SQL
|
92
|
+
INSERT INTO events (#{attributes.keys.join(', ')})
|
93
|
+
VALUES (#{(1..attributes.values.size).map { |n| "$#{n}" }.join(', ')})
|
94
|
+
RETURNING *
|
95
|
+
SQL
|
96
|
+
|
97
|
+
pg_result = connection.with do |conn|
|
98
|
+
conn.exec_params(sql, attributes.values)
|
99
|
+
end
|
100
|
+
deserializer.without_middlewares.deserialize_one(pg_result).tap do |persisted_event|
|
101
|
+
persisted_event.stream = stream
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param stream [PgEventstore::Stream] persisted stream
|
106
|
+
# @return [void]
|
107
|
+
def update_stream_revision(stream, revision)
|
108
|
+
connection.with do |conn|
|
109
|
+
conn.exec_params(<<~SQL, [revision, stream.id])
|
110
|
+
UPDATE streams SET stream_revision = $1 WHERE id = $2
|
111
|
+
SQL
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
# @param stream [PgEventstore::Stream]
|
118
|
+
# @param options [Hash]
|
119
|
+
# @param offset [Integer]
|
120
|
+
# @return [PgEventstore::EventsFilteringQuery]
|
121
|
+
def events_filtering(stream, options, offset: 0)
|
122
|
+
return QueryBuilders::EventsFiltering.all_stream_filtering(options, offset: offset) if stream.all_stream?
|
123
|
+
|
124
|
+
QueryBuilders::EventsFiltering.specific_stream_filtering(stream, options, offset: offset)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|