pg_eventstore 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|