mammoth 0.7.2 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f6df928a12cd055b0c90ed4cd90ae18dca55dcfe43d2c990679543c0be5bcb8
4
- data.tar.gz: e87e0265edd19a481ff0a49921e8116f86d005618d585b6529e8a99c1505795a
3
+ metadata.gz: 6bca055c346ee0a0b651861f60fca15910708ea772b1cfb661cbc8a46040a1e0
4
+ data.tar.gz: 03d9953cec8a9d82859cbfe446736b4cdc216d46e36244bb5fca4abe455fb0ef
5
5
  SHA512:
6
- metadata.gz: 4e12848d7f5590461f9e02a34e9b8917719e49347f86e489e927fb88df26964e7e7363fca444224cc1826f62ce0ecfea70880e2d1aae55ea766d5dbdde231282
7
- data.tar.gz: f23a605b1bfbb1280abcd473d658393516c57a90a4c571ee2dd10eaeb84415f131b021471cf913c9f073a209e23e077c709c234829d8533b5379169f1869d4a0
6
+ metadata.gz: 3394c678ad2135d34d9d5429599d33d02b8390d7d366821f71356fb0ed50bee71892594901372b0083646f4b8f2332e80977933651102c2e72c3a9309770bee2
7
+ data.tar.gz: 3b67803444f90aa6951f6c554d70b0d7b6e88d7b020a905edbda6357a3f54a7eacbd5b3087a66fd1c002f740596cdcf9515f5ae565e6eb7f00cf66ecba648791
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.8.0
6
+
7
+ ### Changed
8
+
9
+ - Delegated incremental PostgreSQL transaction buffering and
10
+ `CDC::Core::TransactionEnvelope` construction to `pgoutput-source-adapter`.
11
+ - `Mammoth::Sources::Postgres` now streams decoded events and transport WAL
12
+ positions through the adapter's `each_normalized` API instead of maintaining
13
+ private transaction state or envelope lookalikes.
14
+
15
+ ### Quality
16
+
17
+ - Added boundary and source integration coverage for streaming adapter
18
+ delegation, exact core envelope preservation, and WAL position forwarding.
19
+ - Updated API signatures, examples, benchmark descriptions, and architecture
20
+ documentation for the corrected source-adapter boundary.
21
+
5
22
  ## 0.7.2
6
23
 
7
24
  ### Changed
data/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/kanutocd/mammoth/main/docs/assets/logo/mammoth-horizontal.svg" alt="Mammoth" width="520">
3
+ </p>
4
+
1
5
  # Mammoth
2
6
 
3
7
  [![Gem Version](https://badge.fury.io/rb/mammoth.svg)](https://badge.fury.io/rb/mammoth)
@@ -5,7 +9,7 @@
5
9
  [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%204.0-ruby.svg)](https://www.ruby-lang.org/en/)
6
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
11
 
8
- 🦣 Mammoth is a self-hosted PostgreSQL event relay focused on reliable delivery
12
+ Mammoth is a self-hosted PostgreSQL event relay focused on reliable delivery
9
13
  of database change events.
10
14
 
11
15
  ```text
@@ -20,7 +24,7 @@ Mammoth
20
24
  Webhook fanout
21
25
  ```
22
26
 
23
- 🦣 Mammoth is intentionally boring infrastructure. It uses YAML configuration,
27
+ Mammoth is intentionally boring infrastructure. It uses YAML configuration,
24
28
  JSON Schema validation, local SQLite operational state, and the CDC Ecosystem's
25
29
  shared vocabulary so operators can inspect, recover, and reason about delivery.
26
30
 
@@ -37,7 +41,7 @@ https://kanutocd.github.io/mammoth/Mammoth.html
37
41
 
38
42
  ## OSS MVP
39
43
 
40
- 🦣 Mammoth OSS includes:
44
+ Mammoth OSS includes:
41
45
 
42
46
  - CLI foundation
43
47
  - YAML configuration loading
@@ -69,6 +73,12 @@ Mammoth does not own pgoutput protocol parsing, value decoding, source
69
73
  normalization, ordering policy, or runtime execution. Those belong to the
70
74
  upstream CDC Ecosystem components.
71
75
 
76
+ For the live PostgreSQL stream, `pgoutput-source-adapter` incrementally owns
77
+ `Begin`/`Commit` buffering and emits exact `CDC::Core::ChangeEvent` or
78
+ `CDC::Core::TransactionEnvelope` work items. Mammoth only composes the
79
+ transport, parser, decoder, and source adapter and forwards the resulting core
80
+ work to delivery.
81
+
72
82
  ## Extensions
73
83
 
74
84
  Mammoth OSS exposes small adapter registries for future extensions:
@@ -8,7 +8,7 @@ module Mammoth
8
8
  # Postgres realizes the CDC Ecosystem libraries for Mammoth's product
9
9
  # boundary. It composes pgoutput-client, pgoutput-parser,
10
10
  # pgoutput-decoder, and pgoutput-source-adapter into a single source that
11
- # yields CDC::Core-shaped work to the delivery runtime.
11
+ # yields CDC::Core work to the delivery runtime.
12
12
  #
13
13
  # This class may mention pgoutput implementation details because it is the
14
14
  # concrete PostgreSQL source adapter used by Mammoth. The rest of Mammoth
@@ -45,22 +45,26 @@ module Mammoth
45
45
  @checkpoint_store = checkpoint_store
46
46
  end
47
47
 
48
- # Stream CDC::Core-shaped work from PostgreSQL logical replication.
48
+ # Stream CDC::Core work from PostgreSQL logical replication.
49
49
  #
50
50
  # Calling this method starts the injected or configured pgoutput-client
51
51
  # runner. The runner owns the PostgreSQL replication connection and slot
52
- # lifecycle; this class only composes the parser, decoder, and adapter
53
- # libraries around the stream.
52
+ # lifecycle, while pgoutput-source-adapter owns transaction buffering and
53
+ # normalization into CDC::Core work items. This class only composes those
54
+ # layers and forwards transport source positions to the adapter.
54
55
  #
55
- # @yieldparam work [Object] CDC::Core::ChangeEvent or TransactionEnvelope
56
+ # @yieldparam work [CDC::Core::ChangeEvent, CDC::Core::TransactionEnvelope]
56
57
  # @return [Enumerator, nil]
57
58
  # @raise [Mammoth::ReplicationError] when the source cannot stream CDC work
58
59
  def each(&block)
59
60
  return enum_for(:each) unless block_given?
60
61
 
61
- effective_runner.start do |payload, metadata = nil|
62
- process_payload(payload, metadata, &block)
62
+ normalizer = effective_adapter
63
+ unless normalizer.respond_to?(:each_normalized)
64
+ raise ReplicationError, "pgoutput source adapter must respond to #each_normalized"
63
65
  end
66
+
67
+ normalizer.each_normalized(decoded_stream, &block)
64
68
  nil
65
69
  rescue StandardError => e
66
70
  raise e if e.is_a?(ReplicationError)
@@ -70,41 +74,37 @@ module Mammoth
70
74
 
71
75
  private
72
76
 
77
+ def decoded_stream
78
+ Enumerator.new do |stream|
79
+ effective_runner.start do |payload, metadata = nil|
80
+ process_payload(payload, metadata) { |decoded| stream << stream_event(decoded, metadata) }
81
+ end
82
+ end
83
+ end
84
+
73
85
  def process_payload(payload, metadata, &block)
74
86
  parsed = parse_payload(payload)
75
87
  decoded = decode_message(parsed, metadata)
76
- process_decoded(decoded, metadata, &block)
88
+ each_decoded(decoded, &block)
77
89
  end
78
90
 
79
- # rubocop:disable Metrics/MethodLength
80
- def process_decoded(decoded, metadata, &block)
91
+ def each_decoded(decoded, &block)
81
92
  return if decoded.nil?
82
93
 
83
94
  if decoded.is_a?(Array)
84
- decoded.each { |item| process_decoded(item, metadata, &block) }
95
+ decoded.each { |item| each_decoded(item, &block) }
85
96
  return
86
97
  end
87
98
 
88
- if begin_message?(decoded)
89
- start_transaction_buffer(decoded)
90
- return
91
- end
99
+ block.call(decoded)
100
+ end
92
101
 
93
- if commit_message?(decoded)
94
- emit_transaction_buffer(decoded, metadata, &block)
95
- return
96
- end
102
+ def stream_event(decoded, metadata)
103
+ normalizer = effective_adapter
104
+ return decoded unless normalizer.respond_to?(:stream_event)
97
105
 
98
- normalize_decoded(decoded).each do |work|
99
- work = enrich_work_position(work, metadata, decoded)
100
- if transaction_buffer_active?
101
- Array(@transaction_events) << work
102
- else
103
- block.call(work)
104
- end
105
- end
106
+ normalizer.stream_event(decoded, source_position: source_position(metadata))
106
107
  end
107
- # rubocop:enable Metrics/MethodLength
108
108
 
109
109
  def parse_payload(payload)
110
110
  parser = effective_parser
@@ -127,102 +127,6 @@ module Mammoth
127
127
  raise ReplicationError, "pgoutput decoder must respond to #decode or #call"
128
128
  end
129
129
 
130
- def normalize_decoded(decoded)
131
- return [] if decoded.nil?
132
- return decoded.flat_map { |item| normalize_decoded(item) } if decoded.is_a?(Array)
133
-
134
- adapter = effective_adapter
135
- result = if adapter.respond_to?(:normalize)
136
- adapter.normalize(decoded)
137
- elsif adapter.respond_to?(:call)
138
- adapter.call(decoded)
139
- else
140
- raise ReplicationError, "pgoutput source adapter must respond to #normalize or #call"
141
- end
142
-
143
- result.is_a?(Array) ? result.compact : [result].compact
144
- end
145
-
146
- def begin_message?(decoded)
147
- message_kind(decoded).include?("begin")
148
- end
149
-
150
- def commit_message?(decoded)
151
- message_kind(decoded).include?("commit")
152
- end
153
-
154
- def message_kind(decoded)
155
- (value_from(decoded, :message_type, :type, :kind) || decoded.class.name.to_s.split("::").last).to_s.downcase
156
- end
157
-
158
- def start_transaction_buffer(decoded)
159
- @transaction_events = []
160
- @transaction_id = value_from(decoded, :transaction_id, :xid, :final_lsn)
161
- @transaction_metadata = value_hash(decoded, :metadata) || {}
162
- end
163
-
164
- def emit_transaction_buffer(decoded, metadata, &block)
165
- return unless transaction_buffer_active?
166
-
167
- block.call(
168
- TransactionEnvelope.new(
169
- @transaction_events,
170
- transaction_id_for(decoded),
171
- commit_lsn_for(decoded, metadata),
172
- value_from(decoded, :commit_time, :committed_at, :timestamp),
173
- @transaction_metadata
174
- )
175
- )
176
- ensure
177
- clear_transaction_buffer
178
- end
179
-
180
- def transaction_id_for(decoded)
181
- value_from(decoded, :transaction_id, :xid) || @transaction_id || first_event_value(:transaction_id, :xid)
182
- end
183
-
184
- def commit_lsn_for(decoded, metadata)
185
- value_from(decoded, :commit_lsn, :source_position, :lsn, :end_lsn, :final_lsn) ||
186
- value_from(metadata, :commit_lsn, :source_position, :lsn, :end_lsn, :final_lsn) ||
187
- first_event_value(:commit_lsn, :source_position, :lsn)
188
- end
189
-
190
- def transaction_buffer_active?
191
- !@transaction_events.nil?
192
- end
193
-
194
- def clear_transaction_buffer
195
- @transaction_events = nil
196
- @transaction_id = nil
197
- @transaction_metadata = nil
198
- end
199
-
200
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
201
- def enrich_work_position(work, metadata, decoded)
202
- position = value_from(work, :source_position, :commit_lsn) ||
203
- value_from(metadata, :source_position, :commit_lsn, :lsn) ||
204
- value_from(decoded, :source_position, :commit_lsn, :lsn)
205
- return work unless position
206
-
207
- work_hash = work.respond_to?(:to_h) ? work.to_h : work
208
- return work unless work_hash.is_a?(Hash)
209
-
210
- key_style = work_hash.key?("operation") ? :string : :symbol
211
- source_position_key = key_style == :string ? "source_position" : :source_position
212
- commit_lsn_key = key_style == :string ? "commit_lsn" : :commit_lsn
213
- return work if work_hash[source_position_key] || work_hash[commit_lsn_key]
214
-
215
- work_hash.merge(source_position_key => position, commit_lsn_key => position)
216
- end
217
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
218
-
219
- def first_event_value(*keys)
220
- event = Array(@transaction_events).find do |event|
221
- keys.any? { |key| value_from(event, key) }
222
- end
223
- value_from(event, *keys) if event
224
- end
225
-
226
130
  def value_from(object, *keys)
227
131
  return nil if object.nil?
228
132
 
@@ -239,9 +143,8 @@ module Mammoth
239
143
  nil
240
144
  end
241
145
 
242
- def value_hash(object, key)
243
- value = value_from(object, key)
244
- value.is_a?(Hash) ? value : nil
146
+ def source_position(metadata)
147
+ value_from(metadata, :source_position, :commit_lsn, :lsn, :wal_end_lsn, :end_lsn, :final_lsn)
245
148
  end
246
149
 
247
150
  def effective_runner
@@ -375,8 +278,6 @@ module Mammoth
375
278
  rescue LoadError => e
376
279
  raise ReplicationError, "#{gem_name} is required for PostgreSQL CDC source integration: #{e.message}"
377
280
  end
378
- TransactionEnvelope = Data.define(:events, :transaction_id, :commit_lsn, :commit_time, :metadata)
379
- private_constant :TransactionEnvelope
380
281
  end
381
282
  # rubocop:enable Metrics/ClassLength
382
283
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mammoth
4
4
  # Current Mammoth gem version.
5
- VERSION = "0.7.2"
5
+ VERSION = "0.8.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mammoth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
@@ -99,14 +99,20 @@ dependencies:
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '0.1'
102
+ version: '0.2'
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: 0.2.0
103
106
  type: :runtime
104
107
  prerelease: false
105
108
  version_requirements: !ruby/object:Gem::Requirement
106
109
  requirements:
107
110
  - - "~>"
108
111
  - !ruby/object:Gem::Version
109
- version: '0.1'
112
+ version: '0.2'
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: 0.2.0
110
116
  - !ruby/object:Gem::Dependency
111
117
  name: sqlite3
112
118
  requirement: !ruby/object:Gem::Requirement