event_sourcery-postgres 0.9.1 → 1.0.1

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: 824d15b46de51809119f1adec5708cab3e6e252dabeedc5257cda2b1c934a7bf
4
- data.tar.gz: deea46e08d13e42d46d37ac7379ccc9782a87e5137710004ce1c922ad283f40d
3
+ metadata.gz: d0257d9a0f70fe078196251fc6de2944278cb2ddf7dda70e2125ce1217d62af6
4
+ data.tar.gz: 4ff91be144b82ec02988d7993a7094618e002ffce96ec7dfde1cde054fb15668
5
5
  SHA512:
6
- metadata.gz: a9cf1a6d3aad1270378fa68855ca339a3a0bca8aa13c8c06c82da634b340a93822d73254da780fbc6ef7420f9901836ebd81d68cfe0c8c506584b1be3c50c028
7
- data.tar.gz: 520be11bd4e97b6ecd07fcc995c613c4978dcb97509569d15fda3d28206acd41696db718d54bc54861f72a38e59bdaf61a18369aa09c60cc748991700cb5dc90
6
+ metadata.gz: 5cd37ea3284ab95e970e704bbabdb14c2160dd3a7612a48927fc99e15a72b074d8c704c0239a75256a158bf468df93c110dcaa0d0ce46da4f871ceb1fd0390c9
7
+ data.tar.gz: d9e5f9281bcc91bb364e1dd607065199b97f144118841183807ce78a3a7a0ab4997fc6e12a625d0f15aaf3b15076d769a03b56362e47201906539ed29d8fefb4
data/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
+
10
+ [Unreleased]: https://github.com/envato/event_sourcery-postgres/compare/v1.0.1...HEAD
11
+
12
+ ## [1.0.1] - 2026-01-17
13
+
14
+ ### Changed
15
+
16
+ - Resolve issues as identified by RuboCop ([#85]).
17
+
18
+ [1.0.0]: https://github.com/envato/event_sourcery-postgres/compare/v1.0.0...v1.0.1
19
+ [#85]: https://github.com/envato/event_sourcery-postgres/pull/85
20
+
21
+ ## [1.0.0] - 2025-12-28
22
+
23
+ ### Changed
24
+
25
+ - Resolve issues as identified by RuboCop ([#78], [#82], [#83]).
26
+ - Minor fixups in gem metadata ([#79]).
27
+ - Remove support for older Ruby versions: Ruby 2.6 or greater is now required ([#80]).
28
+
29
+ [1.0.0]: https://github.com/envato/event_sourcery-postgres/compare/v0.9.1...v1.0.0
30
+ [#78]: https://github.com/envato/event_sourcery-postgres/pull/78
31
+ [#79]: https://github.com/envato/event_sourcery-postgres/pull/79
32
+ [#80]: https://github.com/envato/event_sourcery-postgres/pull/80
33
+ [#82]: https://github.com/envato/event_sourcery-postgres/pull/82
34
+ [#83]: https://github.com/envato/event_sourcery-postgres/pull/83
35
+
9
36
  ## [0.9.1] - 2022-01-20
10
37
 
11
38
  ### Changed
@@ -117,7 +144,6 @@ or when the loop stops
117
144
  - Postgres related configuration is through `EventSourcery::Postgres.configure`
118
145
  instead of `EventSourcery.configure`.
119
146
 
120
- [Unreleased]: https://github.com/envato/event_sourcery-postgres/compare/v0.9.1...HEAD
121
147
  [0.9.1]: https://github.com/envato/event_sourcery-postgres/compare/v0.9.0...v0.9.1
122
148
  [0.9.0]: https://github.com/envato/event_sourcery-postgres/compare/v0.8.1...v0.9.0
123
149
  [0.8.1]: https://github.com/envato/event_sourcery-postgres/compare/v0.8.0...v0.8.1
data/README.md CHANGED
@@ -1,12 +1,10 @@
1
1
  # EventSourcery::Postgres
2
2
 
3
- [![Build Status](https://github.com/envato/event_sourcery-postgres/workflows/tests/badge.svg?branch=main)](https://github.com/envato/event_sourcery-postgres/actions?query=workflow%3Atests+branch%3Amain)
3
+ [![Build Status](https://github.com/envato/event_sourcery-postgres/actions/workflows/test.yml/badge.svg)](https://github.com/envato/event_sourcery-postgres/actions/workflows/test.yml)
4
4
 
5
5
  ## Development Status
6
6
 
7
- EventSourcery is currently being used in production by multiple apps but we
8
- haven't finalized the API yet and things are still moving rapidly. Until we
9
- release a 1.0 things may change without first being deprecated.
7
+ EventSourcery::Postgres is in production use at [Envato](http://envato.com).
10
8
 
11
9
  ## Installation
12
10
 
@@ -1,5 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'event_sourcery/postgres/version'
5
6
 
@@ -12,27 +13,32 @@ Gem::Specification.new do |spec|
12
13
 
13
14
  spec.summary = 'Postgres event store for use with EventSourcery'
14
15
  spec.homepage = 'https://github.com/envato/event_sourcery-postgres'
16
+ spec.license = 'MIT'
17
+
15
18
  spec.metadata = {
16
- 'bug_tracker_uri' => 'https://github.com/envato/event_sourcery-postgres/issues',
17
- 'changelog_uri' => 'https://github.com/envato/event_sourcery-postgres/blob/HEAD/CHANGELOG.md',
18
- 'source_code_uri' => 'https://github.com/envato/event_sourcery-postgres',
19
- }
19
+ 'allowed_push_host' => 'https://rubygems.org',
20
+ 'bug_tracker_uri' => "#{spec.homepage}/issues",
21
+ 'changelog_uri' => "#{spec.homepage}/blob/HEAD/CHANGELOG.md",
22
+ 'documentation_uri' => "https://www.rubydoc.info/gems/#{spec.name}/#{spec.version}",
23
+ 'source_code_uri' => "#{spec.homepage}/tree/v#{spec.version}"
24
+ }
20
25
 
21
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
26
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
22
27
  f.match(%r{^(\.|bin/|Gemfile|Rakefile|script/|spec/)})
23
28
  end
24
29
  spec.bindir = 'exe'
25
30
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
31
  spec.require_paths = ['lib']
27
32
 
28
- spec.required_ruby_version = '>= 2.2.0'
33
+ spec.required_ruby_version = '>= 2.6.0'
29
34
 
30
- spec.add_dependency 'sequel', '>= 4.38'
31
- spec.add_dependency 'pg'
32
35
  spec.add_dependency 'event_sourcery', '>= 0.14.0'
36
+ spec.add_dependency 'pg'
37
+ spec.add_dependency 'sequel', '>= 4.38'
38
+ spec.add_development_dependency 'benchmark-ips'
33
39
  spec.add_development_dependency 'bundler'
40
+ spec.add_development_dependency 'pry'
34
41
  spec.add_development_dependency 'rake', '~> 13'
35
42
  spec.add_development_dependency 'rspec', '~> 3.0'
36
- spec.add_development_dependency 'pry'
37
- spec.add_development_dependency 'benchmark-ips'
43
+ spec.add_development_dependency 'rubocop', '~> 1'
38
44
  end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EventSourcery
2
4
  module Postgres
5
+ # Configuration settings for the PostgreSQL event store and projections.
3
6
  class Config
4
7
  attr_accessor :lock_table_to_guarantee_linear_sequence_id_growth,
5
8
  :write_events_function_name,
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EventSourcery
2
4
  module Postgres
5
+ # PostgreSQL implementation of an event store for persisting and retrieving domain events.
3
6
  class EventStore
4
7
  include EventSourcery::EventStore::EachByRange
5
8
 
@@ -32,6 +35,7 @@ module EventSourcery
32
35
  events = Array(event_or_events)
33
36
  aggregate_ids = events.map(&:aggregate_id).uniq
34
37
  raise AtomicWriteToMultipleAggregatesNotSupported unless aggregate_ids.count == 1
38
+
35
39
  sql = write_events_sql(aggregate_ids.first, events, expected_version)
36
40
  @db_connection.run(sql)
37
41
  log_events_saved(events)
@@ -40,9 +44,9 @@ module EventSourcery
40
44
  rescue Sequel::DatabaseError => e
41
45
  if e.message =~ /Concurrency conflict/
42
46
  raise ConcurrencyError, "expected version was not #{expected_version}. Error: #{e.message}"
43
- else
44
- raise
45
47
  end
48
+
49
+ raise
46
50
  end
47
51
 
48
52
  # Get the next set of events from the given event id. You can
@@ -55,10 +59,11 @@ module EventSourcery
55
59
  #
56
60
  # @return [Array] array of found events
57
61
  def get_next_from(id, event_types: nil, limit: 1000)
58
- query = events_table.
59
- order(:id).
60
- where(Sequel.lit('id >= ?', id)).
61
- limit(limit)
62
+ query =
63
+ events_table
64
+ .order(:id)
65
+ .where(Sequel.lit('id >= ?', id))
66
+ .limit(limit)
62
67
  query = query.where(type: event_types) if event_types
63
68
  query.map { |event_row| build_event(event_row) }
64
69
  end
@@ -96,7 +101,7 @@ module EventSourcery
96
101
  # @param event_types the event_types to subscribe to, default all.
97
102
  # @param after_listen the after listen call back block. default nil.
98
103
  # @param subscription_master the subscription master block
99
- def subscribe(from_id:, event_types: nil, after_listen: nil, subscription_master:, &block)
104
+ def subscribe(from_id:, subscription_master:, event_types: nil, after_listen: nil, &block)
100
105
  poll_waiter = OptimisedEventPollWaiter.new(db_connection: @db_connection, after_listen: after_listen)
101
106
  args = {
102
107
  poll_waiter: poll_waiter,
@@ -157,9 +162,10 @@ module EventSourcery
157
162
 
158
163
  def to_sql_literal(value)
159
164
  return 'null' unless value
160
- wrapped_value = if Time === value
165
+
166
+ wrapped_value = if value.is_a?(Time)
161
167
  value.iso8601(6)
162
- elsif Hash === value
168
+ elsif value.is_a?(Hash)
163
169
  Sequel.pg_json(value)
164
170
  else
165
171
  value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EventSourcery
2
4
  module Postgres
3
5
  # Optimise poll interval with Postgres listen/notify
@@ -11,7 +13,7 @@ module EventSourcery
11
13
  @after_listen = after_listen
12
14
  end
13
15
 
14
- def poll(after_listen: proc { }, &block)
16
+ def poll(after_listen: proc {}, &block)
15
17
  @events_queue.callback = proc do
16
18
  ensure_listen_thread_alive!
17
19
  block.call
@@ -52,7 +54,7 @@ module EventSourcery
52
54
  after_listen_callback = if after_listen
53
55
  proc do
54
56
  after_listen.call
55
- @after_listen.call if @after_listen
57
+ @after_listen&.call
56
58
  end
57
59
  else
58
60
  @after_listen
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EventSourcery
2
4
  module Postgres
5
+ # Mixin providing projection capabilities for processing events into read models.
3
6
  module Projector
4
7
  def self.included(base)
5
8
  base.include(EventProcessing::EventStreamProcessor)
@@ -15,6 +18,7 @@ module EventSourcery
15
18
  end
16
19
  end
17
20
 
21
+ # Instance methods for projector event processing and tracking.
18
22
  module InstanceMethods
19
23
  def initialize(tracker: EventSourcery::Postgres.config.event_tracker,
20
24
  db_connection: EventSourcery::Postgres.config.projections_database,
@@ -1,17 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EventSourcery
2
4
  module Postgres
5
+ # Queue that invokes a callback at regular intervals when no items are available.
3
6
  class QueueWithIntervalCallback < ::Queue
4
7
  attr_accessor :callback
5
8
 
6
- def initialize(callback: proc {}, callback_interval: EventSourcery::Postgres.config.callback_interval_if_no_new_events, poll_interval: 0.1)
9
+ def initialize(
10
+ callback: proc {},
11
+ callback_interval: EventSourcery::Postgres.config.callback_interval_if_no_new_events,
12
+ poll_interval: 0.1
13
+ )
7
14
  @callback = callback
8
15
  @callback_interval = callback_interval
9
16
  @poll_interval = poll_interval
10
17
  super()
11
18
  end
12
19
 
13
- def pop(non_block_without_callback = false)
20
+ def pop(non_block_without_callback = false) # rubocop:disable Style/OptionalBooleanParameter
14
21
  return super if non_block_without_callback
22
+
15
23
  pop_with_interval_callback
16
24
  end
17
25
 
@@ -21,6 +29,7 @@ module EventSourcery
21
29
  time = Time.now
22
30
  loop do
23
31
  return pop(true) unless empty?
32
+
24
33
  if @callback_interval && Time.now > time + @callback_interval
25
34
  @callback.call
26
35
  time = Time.now
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EventSourcery
2
4
  module Postgres
5
+ # Mixin providing reactor capabilities for processing events and emitting new events in response.
3
6
  module Reactor
4
7
  UndeclaredEventEmissionError = Class.new(StandardError)
5
8
 
@@ -10,6 +13,7 @@ module EventSourcery
10
13
  base.include(InstanceMethods)
11
14
  end
12
15
 
16
+ # Class methods for declaring and querying emitted event types.
13
17
  module ClassMethods
14
18
  # Assign the types of events this reactor can emit.
15
19
  #
@@ -20,7 +24,7 @@ module EventSourcery
20
24
 
21
25
  # @return [Array] an array of the types of events this reactor can emit
22
26
  def emit_events
23
- @emits_event_types ||= []
27
+ @emits_event_types ||= [] # rubocop:disable Naming/MemoizedInstanceVariableName
24
28
  end
25
29
 
26
30
  # This will tell you if this reactor emits any type of event.
@@ -39,6 +43,7 @@ module EventSourcery
39
43
  end
40
44
  end
41
45
 
46
+ # Instance methods for reactor initialisation and event emission.
42
47
  module InstanceMethods
43
48
  def initialize(tracker: EventSourcery::Postgres.config.event_tracker,
44
49
  db_connection: EventSourcery::Postgres.config.projections_database,
@@ -48,11 +53,10 @@ module EventSourcery
48
53
  @event_source = event_source
49
54
  @event_sink = event_sink
50
55
  @db_connection = db_connection
51
- if self.class.emits_events?
52
- if event_sink.nil? || event_source.nil?
53
- raise ArgumentError, 'An event sink and source is required for processors that emit events'
54
- end
55
- end
56
+ return unless self.class.emits_events?
57
+ return unless event_sink.nil? || event_source.nil?
58
+
59
+ raise ArgumentError, 'An event sink and source is required for processors that emit events'
56
60
  end
57
61
  end
58
62
 
@@ -61,19 +65,20 @@ module EventSourcery
61
65
  attr_reader :event_sink, :event_source
62
66
 
63
67
  def emit_event(event_or_hash, &block)
64
- event = if Event === event_or_hash
68
+ event = if event_or_hash.is_a?(Event)
65
69
  event_or_hash
66
70
  else
67
71
  Event.new(event_or_hash)
68
72
  end
69
73
  raise UndeclaredEventEmissionError unless self.class.emits_event?(event.class)
74
+
70
75
  event = event.with(causation_id: _event.uuid, correlation_id: _event.correlation_id)
71
76
  invoke_action_and_emit_event(event, block)
72
- EventSourcery.logger.debug { "[#{self.processor_name}] Emitted event: #{event.inspect}" }
77
+ EventSourcery.logger.debug { "[#{processor_name}] Emitted event: #{event.inspect}" }
73
78
  end
74
79
 
75
80
  def invoke_action_and_emit_event(event, action)
76
- action.call(event.body) if action
81
+ action&.call(event.body)
77
82
  event_sink.sink(event)
78
83
  end
79
84
  end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EventSourcery
2
4
  module Postgres
5
+ # Schema management for creating event store and projector tables in PostgreSQL.
3
6
  module Schema
4
7
  module_function
5
8
 
@@ -15,7 +18,12 @@ module EventSourcery
15
18
  write_events_function_name: EventSourcery::Postgres.config.write_events_function_name)
16
19
  create_events(db: db, table_name: events_table_name)
17
20
  create_aggregates(db: db, table_name: aggregates_table_name)
18
- create_or_update_functions(db: db, events_table_name: events_table_name, function_name: write_events_function_name, aggregates_table_name: aggregates_table_name)
21
+ create_or_update_functions(
22
+ db: db,
23
+ events_table_name: events_table_name,
24
+ function_name: write_events_function_name,
25
+ aggregates_table_name: aggregates_table_name
26
+ )
19
27
  end
20
28
 
21
29
  # Create the events table. Needs the database and the table name.
@@ -35,8 +43,9 @@ module EventSourcery
35
43
  column :version, :bigint, null: false
36
44
  column :correlation_id, :uuid
37
45
  column :causation_id, :uuid
38
- column :created_at, :'timestamp without time zone', null: false, default: Sequel.lit("(now() at time zone 'utc')")
39
- index [:aggregate_id, :version], unique: true
46
+ column :created_at, :'timestamp without time zone', null: false,
47
+ default: Sequel.lit("(now() at time zone 'utc')")
48
+ index %i[aggregate_id version], unique: true
40
49
  index :uuid, unique: true
41
50
  index :type
42
51
  index :correlation_id
@@ -70,96 +79,96 @@ module EventSourcery
70
79
  function_name: EventSourcery::Postgres.config.write_events_function_name,
71
80
  events_table_name: EventSourcery::Postgres.config.events_table_name,
72
81
  aggregates_table_name: EventSourcery::Postgres.config.aggregates_table_name)
73
- db.run <<-SQL
74
- create or replace function #{function_name}(_aggregateId uuid,
75
- _eventTypes varchar[],
76
- _expectedVersion int,
77
- _bodies json[],
78
- _createdAtTimes timestamp without time zone[],
79
- _eventUUIDs uuid[],
80
- _correlationIds uuid[],
81
- _causationIds uuid[],
82
- _lockTable boolean) returns void as $$
83
- declare
84
- currentVersion int;
85
- body json;
86
- eventVersion int;
87
- eventId text;
88
- index int;
89
- newVersion int;
90
- numEvents int;
91
- createdAt timestamp without time zone;
92
- begin
93
- numEvents := array_length(_bodies, 1);
94
- select version into currentVersion from #{aggregates_table_name} where aggregate_id = _aggregateId;
95
- if not found then
96
- -- when we have no existing version for this aggregate
97
- if _expectedVersion = 0 or _expectedVersion is null then
98
- -- set the version to 1 if expected version is null or 0
99
- insert into #{aggregates_table_name}(aggregate_id, version) values(_aggregateId, numEvents);
100
- currentVersion := 0;
101
- else
102
- raise 'Concurrency conflict. Current version: 0, expected version: %', _expectedVersion;
103
- end if;
104
- else
105
- if _expectedVersion is null then
106
- -- automatically increment the version
107
- update #{aggregates_table_name} set version = version + numEvents where aggregate_id = _aggregateId returning version into newVersion;
108
- currentVersion := newVersion - numEvents;
109
- else
110
- -- increment the version if it's at our expected version
111
- update #{aggregates_table_name} set version = version + numEvents where aggregate_id = _aggregateId and version = _expectedVersion;
112
- if not found then
113
- -- version was not at expected_version, raise an error.
114
- -- currentVersion may not equal what it did in the database when the
115
- -- above update statement is executed (it may have been incremented by another
116
- -- process)
117
- raise 'Concurrency conflict. Last known current version: %, expected version: %', currentVersion, _expectedVersion;
118
- end if;
119
- end if;
120
- end if;
121
- index := 1;
122
- eventVersion := currentVersion + 1;
123
- if _lockTable then
124
- -- Ensure this transaction is the only one writing events to guarantee
125
- -- linear growth of sequence IDs.
126
- -- Any value that won't conflict with other advisory locks will work.
127
- -- The Postgres tracker currently obtains an advisory lock using it's
128
- -- integer row ID, so values 1 to the number of ESP's in the system would
129
- -- be taken if the tracker is running in the same database as your
130
- -- projections.
131
- perform pg_advisory_xact_lock(-1);
132
- end if;
133
- foreach body IN ARRAY(_bodies)
134
- loop
135
- if _createdAtTimes[index] is not null then
136
- createdAt := _createdAtTimes[index];
137
- else
138
- createdAt := now() at time zone 'utc';
139
- end if;
82
+ db.run <<~SQL
83
+ create or replace function #{function_name}(_aggregateId uuid,
84
+ _eventTypes varchar[],
85
+ _expectedVersion int,
86
+ _bodies json[],
87
+ _createdAtTimes timestamp without time zone[],
88
+ _eventUUIDs uuid[],
89
+ _correlationIds uuid[],
90
+ _causationIds uuid[],
91
+ _lockTable boolean) returns void as $$
92
+ declare
93
+ currentVersion int;
94
+ body json;
95
+ eventVersion int;
96
+ eventId text;
97
+ index int;
98
+ newVersion int;
99
+ numEvents int;
100
+ createdAt timestamp without time zone;
101
+ begin
102
+ numEvents := array_length(_bodies, 1);
103
+ select version into currentVersion from #{aggregates_table_name} where aggregate_id = _aggregateId;
104
+ if not found then
105
+ -- when we have no existing version for this aggregate
106
+ if _expectedVersion = 0 or _expectedVersion is null then
107
+ -- set the version to 1 if expected version is null or 0
108
+ insert into #{aggregates_table_name}(aggregate_id, version) values(_aggregateId, numEvents);
109
+ currentVersion := 0;
110
+ else
111
+ raise 'Concurrency conflict. Current version: 0, expected version: %', _expectedVersion;
112
+ end if;
113
+ else
114
+ if _expectedVersion is null then
115
+ -- automatically increment the version
116
+ update #{aggregates_table_name} set version = version + numEvents where aggregate_id = _aggregateId returning version into newVersion;
117
+ currentVersion := newVersion - numEvents;
118
+ else
119
+ -- increment the version if it's at our expected version
120
+ update #{aggregates_table_name} set version = version + numEvents where aggregate_id = _aggregateId and version = _expectedVersion;
121
+ if not found then
122
+ -- version was not at expected_version, raise an error.
123
+ -- currentVersion may not equal what it did in the database when the
124
+ -- above update statement is executed (it may have been incremented by another
125
+ -- process)
126
+ raise 'Concurrency conflict. Last known current version: %, expected version: %', currentVersion, _expectedVersion;
127
+ end if;
128
+ end if;
129
+ end if;
130
+ index := 1;
131
+ eventVersion := currentVersion + 1;
132
+ if _lockTable then
133
+ -- Ensure this transaction is the only one writing events to guarantee
134
+ -- linear growth of sequence IDs.
135
+ -- Any value that won't conflict with other advisory locks will work.
136
+ -- The Postgres tracker currently obtains an advisory lock using it's
137
+ -- integer row ID, so values 1 to the number of ESP's in the system would
138
+ -- be taken if the tracker is running in the same database as your
139
+ -- projections.
140
+ perform pg_advisory_xact_lock(-1);
141
+ end if;
142
+ foreach body IN ARRAY(_bodies)
143
+ loop
144
+ if _createdAtTimes[index] is not null then
145
+ createdAt := _createdAtTimes[index];
146
+ else
147
+ createdAt := now() at time zone 'utc';
148
+ end if;
140
149
 
141
- insert into #{events_table_name}
142
- (uuid, aggregate_id, type, body, version, correlation_id, causation_id, created_at)
143
- values
144
- (
145
- _eventUUIDs[index],
146
- _aggregateId,
147
- _eventTypes[index],
148
- body,
149
- eventVersion,
150
- _correlationIds[index],
151
- _causationIds[index],
152
- createdAt
153
- )
154
- returning id into eventId;
150
+ insert into #{events_table_name}
151
+ (uuid, aggregate_id, type, body, version, correlation_id, causation_id, created_at)
152
+ values
153
+ (
154
+ _eventUUIDs[index],
155
+ _aggregateId,
156
+ _eventTypes[index],
157
+ body,
158
+ eventVersion,
159
+ _correlationIds[index],
160
+ _causationIds[index],
161
+ createdAt
162
+ )
163
+ returning id into eventId;
155
164
 
156
- eventVersion := eventVersion + 1;
157
- index := index + 1;
158
- end loop;
159
- perform pg_notify('new_event', eventId);
160
- end;
161
- $$ language plpgsql;
162
- SQL
165
+ eventVersion := eventVersion + 1;
166
+ index := index + 1;
167
+ end loop;
168
+ perform pg_notify('new_event', eventId);
169
+ end;
170
+ $$ language plpgsql;
171
+ SQL
163
172
  end
164
173
 
165
174
  # Create the projector tracker table. Needs the database and the table name.
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EventSourcery
2
4
  module Postgres
5
+ # Mixin providing table management capabilities for projectors and reactors.
3
6
  module TableOwner
4
7
  DefaultTableError = Class.new(StandardError)
5
8
  NoSuchTableError = Class.new(StandardError)
@@ -8,6 +11,7 @@ module EventSourcery
8
11
  base.extend(ClassMethods)
9
12
  end
10
13
 
14
+ # Class methods for defining and managing database tables.
11
15
  module ClassMethods
12
16
  # Hash of the tables and their corresponding blocks.
13
17
  #
@@ -36,11 +40,9 @@ module EventSourcery
36
40
 
37
41
  # Reset by dropping each table.
38
42
  def reset
39
- self.class.tables.keys.each do |table_name|
43
+ self.class.tables.each_key do |table_name|
40
44
  prefixed_name = table_name_prefixed(table_name)
41
- if @db_connection.table_exists?(prefixed_name)
42
- @db_connection.drop_table(prefixed_name, cascade: true)
43
- end
45
+ @db_connection.drop_table(prefixed_name, cascade: true) if @db_connection.table_exists?(prefixed_name)
44
46
  end
45
47
  super if defined?(super)
46
48
  setup
@@ -49,7 +51,7 @@ module EventSourcery
49
51
  # This will truncate all the tables and reset the tracker back to 0,
50
52
  # done as a transaction.
51
53
  def truncate
52
- self.class.tables.each do |table_name, _|
54
+ self.class.tables.each_key do |table_name|
53
55
  @db_connection.transaction do
54
56
  prefixed_name = table_name_prefixed(table_name)
55
57
  @db_connection[prefixed_name].truncate
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EventSourcery
2
4
  module Postgres
3
5
  # This will set up a persisted event id tracker for processors.
4
6
  class Tracker
5
-
6
7
  def initialize(db_connection = EventSourcery::Postgres.config.projections_database,
7
8
  table_name: EventSourcery::Postgres.config.tracker_table_name,
8
9
  obtain_processor_lock: true)
@@ -19,16 +20,14 @@ module EventSourcery
19
20
  def setup(processor_name = nil)
20
21
  create_table_if_not_exists if EventSourcery::Postgres.config.auto_create_projector_tracker
21
22
 
22
- unless tracker_table_exists?
23
- raise UnableToLockProcessorError, 'Projector tracker table does not exist'
24
- end
23
+ raise UnableToLockProcessorError, 'Projector tracker table does not exist' unless tracker_table_exists?
25
24
 
26
- if processor_name
27
- create_track_entry_if_not_exists(processor_name)
28
- if @obtain_processor_lock
29
- obtain_global_lock_on_processor(processor_name)
30
- end
31
- end
25
+ return unless processor_name
26
+
27
+ create_track_entry_if_not_exists(processor_name)
28
+ return unless @obtain_processor_lock
29
+
30
+ obtain_global_lock_on_processor(processor_name)
32
31
  end
33
32
 
34
33
  # This will updated the tracker table to the given event id value
@@ -37,9 +36,9 @@ module EventSourcery
37
36
  # @param processor_name the name of the processor to update
38
37
  # @param event_id the event id number to update to
39
38
  def processed_event(processor_name, event_id)
40
- table.
41
- where(name: processor_name.to_s).
42
- update(last_processed_event_id: event_id)
39
+ table
40
+ .where(name: processor_name.to_s)
41
+ .update(last_processed_event_id: event_id)
43
42
  true
44
43
  end
45
44
 
@@ -82,17 +81,18 @@ module EventSourcery
82
81
  private
83
82
 
84
83
  def obtain_global_lock_on_processor(processor_name)
85
- lock_obtained = @db_connection.fetch("select pg_try_advisory_lock(#{@track_entry_id})").to_a.first[:pg_try_advisory_lock]
86
- if lock_obtained == false
87
- raise UnableToLockProcessorError, "Unable to get a lock on #{processor_name} #{@track_entry_id}"
88
- end
84
+ lock_obtained = @db_connection.fetch("select pg_try_advisory_lock(#{@track_entry_id})")
85
+ .to_a.first[:pg_try_advisory_lock]
86
+ return unless lock_obtained == false
87
+
88
+ raise UnableToLockProcessorError, "Unable to get a lock on #{processor_name} #{@track_entry_id}"
89
89
  end
90
90
 
91
91
  def create_table_if_not_exists
92
- unless tracker_table_exists?
93
- EventSourcery.logger.info { "Projector tracker missing - attempting to create 'projector_tracker' table" }
94
- EventSourcery::Postgres::Schema.create_projector_tracker(db: @db_connection, table_name: @table_name)
95
- end
92
+ return if tracker_table_exists?
93
+
94
+ EventSourcery.logger.info { "Projector tracker missing - attempting to create 'projector_tracker' table" }
95
+ EventSourcery::Postgres::Schema.create_projector_tracker(db: @db_connection, table_name: @table_name)
96
96
  end
97
97
 
98
98
  def create_track_entry_if_not_exists(processor_name)
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EventSourcery
2
4
  module Postgres
3
- VERSION = '0.9.1'.freeze
5
+ VERSION = '1.0.1'
4
6
  end
5
7
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sequel'
2
4
 
3
5
  Sequel.default_timezone = :utc
@@ -15,6 +17,7 @@ require 'event_sourcery/postgres/reactor'
15
17
  require 'event_sourcery/postgres/tracker'
16
18
 
17
19
  module EventSourcery
20
+ # PostgreSQL adapter for EventSourcery providing event store and projection capabilities.
18
21
  module Postgres
19
22
  def self.configure
20
23
  yield config
metadata CHANGED
@@ -1,29 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: event_sourcery-postgres
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Envato
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2022-01-20 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: sequel
13
+ name: event_sourcery
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '4.38'
18
+ version: 0.14.0
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '4.38'
25
+ version: 0.14.0
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: pg
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -39,19 +38,33 @@ dependencies:
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
41
40
  - !ruby/object:Gem::Dependency
42
- name: event_sourcery
41
+ name: sequel
43
42
  requirement: !ruby/object:Gem::Requirement
44
43
  requirements:
45
44
  - - ">="
46
45
  - !ruby/object:Gem::Version
47
- version: 0.14.0
46
+ version: '4.38'
48
47
  type: :runtime
49
48
  prerelease: false
50
49
  version_requirements: !ruby/object:Gem::Requirement
51
50
  requirements:
52
51
  - - ">="
53
52
  - !ruby/object:Gem::Version
54
- version: 0.14.0
53
+ version: '4.38'
54
+ - !ruby/object:Gem::Dependency
55
+ name: benchmark-ips
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
55
68
  - !ruby/object:Gem::Dependency
56
69
  name: bundler
57
70
  requirement: !ruby/object:Gem::Requirement
@@ -67,62 +80,61 @@ dependencies:
67
80
  - !ruby/object:Gem::Version
68
81
  version: '0'
69
82
  - !ruby/object:Gem::Dependency
70
- name: rake
83
+ name: pry
71
84
  requirement: !ruby/object:Gem::Requirement
72
85
  requirements:
73
- - - "~>"
86
+ - - ">="
74
87
  - !ruby/object:Gem::Version
75
- version: '13'
88
+ version: '0'
76
89
  type: :development
77
90
  prerelease: false
78
91
  version_requirements: !ruby/object:Gem::Requirement
79
92
  requirements:
80
- - - "~>"
93
+ - - ">="
81
94
  - !ruby/object:Gem::Version
82
- version: '13'
95
+ version: '0'
83
96
  - !ruby/object:Gem::Dependency
84
- name: rspec
97
+ name: rake
85
98
  requirement: !ruby/object:Gem::Requirement
86
99
  requirements:
87
100
  - - "~>"
88
101
  - !ruby/object:Gem::Version
89
- version: '3.0'
102
+ version: '13'
90
103
  type: :development
91
104
  prerelease: false
92
105
  version_requirements: !ruby/object:Gem::Requirement
93
106
  requirements:
94
107
  - - "~>"
95
108
  - !ruby/object:Gem::Version
96
- version: '3.0'
109
+ version: '13'
97
110
  - !ruby/object:Gem::Dependency
98
- name: pry
111
+ name: rspec
99
112
  requirement: !ruby/object:Gem::Requirement
100
113
  requirements:
101
- - - ">="
114
+ - - "~>"
102
115
  - !ruby/object:Gem::Version
103
- version: '0'
116
+ version: '3.0'
104
117
  type: :development
105
118
  prerelease: false
106
119
  version_requirements: !ruby/object:Gem::Requirement
107
120
  requirements:
108
- - - ">="
121
+ - - "~>"
109
122
  - !ruby/object:Gem::Version
110
- version: '0'
123
+ version: '3.0'
111
124
  - !ruby/object:Gem::Dependency
112
- name: benchmark-ips
125
+ name: rubocop
113
126
  requirement: !ruby/object:Gem::Requirement
114
127
  requirements:
115
- - - ">="
128
+ - - "~>"
116
129
  - !ruby/object:Gem::Version
117
- version: '0'
130
+ version: '1'
118
131
  type: :development
119
132
  prerelease: false
120
133
  version_requirements: !ruby/object:Gem::Requirement
121
134
  requirements:
122
- - - ">="
135
+ - - "~>"
123
136
  - !ruby/object:Gem::Version
124
- version: '0'
125
- description:
137
+ version: '1'
126
138
  email:
127
139
  - rubygems@envato.com
128
140
  executables: []
@@ -146,12 +158,14 @@ files:
146
158
  - lib/event_sourcery/postgres/tracker.rb
147
159
  - lib/event_sourcery/postgres/version.rb
148
160
  homepage: https://github.com/envato/event_sourcery-postgres
149
- licenses: []
161
+ licenses:
162
+ - MIT
150
163
  metadata:
164
+ allowed_push_host: https://rubygems.org
151
165
  bug_tracker_uri: https://github.com/envato/event_sourcery-postgres/issues
152
166
  changelog_uri: https://github.com/envato/event_sourcery-postgres/blob/HEAD/CHANGELOG.md
153
- source_code_uri: https://github.com/envato/event_sourcery-postgres
154
- post_install_message:
167
+ documentation_uri: https://www.rubydoc.info/gems/event_sourcery-postgres/1.0.1
168
+ source_code_uri: https://github.com/envato/event_sourcery-postgres/tree/v1.0.1
155
169
  rdoc_options: []
156
170
  require_paths:
157
171
  - lib
@@ -159,15 +173,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
159
173
  requirements:
160
174
  - - ">="
161
175
  - !ruby/object:Gem::Version
162
- version: 2.2.0
176
+ version: 2.6.0
163
177
  required_rubygems_version: !ruby/object:Gem::Requirement
164
178
  requirements:
165
179
  - - ">="
166
180
  - !ruby/object:Gem::Version
167
181
  version: '0'
168
182
  requirements: []
169
- rubygems_version: 3.2.22
170
- signing_key:
183
+ rubygems_version: 4.0.4
171
184
  specification_version: 4
172
185
  summary: Postgres event store for use with EventSourcery
173
186
  test_files: []