mammoth 1.0.0 → 1.2.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: fc13868a4aa6d845e63c655af36b9bbb69604be8bfb5b7ab6beda7d1a765c145
4
- data.tar.gz: 9a3c9514953f68db2d5287903e29980ffb147460691817f5de1a19a8123f1727
3
+ metadata.gz: 9169e3ff1f9fad3a07df87cee57891aea0ca6815f356f7ef998e94572a432ea4
4
+ data.tar.gz: 6ee2140075cb22b1fc0aea3977a33adfd1aa49918d184a823ee3cbcab9b87bac
5
5
  SHA512:
6
- metadata.gz: 552aa45078fa0ed209890bbfe8eb2dc0672234d4e008c8cc0edd954a284e0b40bffafe16a2a8c2df8dd0219aee3eed742ba2a38531cd3d2acd7b13b35bb48b11
7
- data.tar.gz: 69f509480ecdd46aa50a7879c3dbc1589becce9a651b1697e1f164ea0d6be781edb9f54238940a32553b59956c8cba05cd2b0caaffca57812823cb32e6fa481b
6
+ metadata.gz: 37191be4257dbab756e5e2ba81bae60905df5a921792001daf12b5ece1bcacbf60c353967b6da5c0de4e71311a8b95311ed37e7f8f2587bcb49e7dfbae6a57bd
7
+ data.tar.gz: 603479a5b0e7e067743eb6dcb8802ea2077954df50e23a3b29b97bdefb161cb1c47f493dfac919aa27d6a0552f018fcc1a7116e876acca50f71bc716f0d8c539
data/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.2.0 - 2026-07-19
6
+
7
+ ### Added
8
+
9
+ - Added an opt-in monitoring profile to `webhooks-quickstart` with provisioned
10
+ Prometheus and Grafana services, meaningful seeded traffic, a Mammoth
11
+ operations dashboard, curated Prometheus queries, recording rules, and
12
+ example Prometheus and Grafana alerting rules.
13
+
14
+ ## 1.1.0 - 2026-07-19
15
+
16
+ ### Added
17
+
18
+ - Added a serializer benchmark comparing explicit and deterministic fallback
19
+ event IDs for event and transaction payload projection.
20
+ - Added `webhooks-quickstart`, a one-command PostgreSQL-to-Mammoth demo with an
21
+ order application; inspectable signed webhook deliveries and retries;
22
+ `INSERT`, `UPDATE`, and `DELETE` events; multi-event payment and reversal
23
+ transactions; adaptation guidance; WAL-retention safeguards; and end-to-end
24
+ verification.
25
+
26
+ ### Fixed
27
+
28
+ - Generate deterministic fallback event and transaction-envelope IDs so retries
29
+ and post-crash redelivery preserve destination idempotency when upstream
30
+ metadata does not provide an event ID.
31
+ - Serialize `CDC::Core::ChangeEvent` column differences when complete old and
32
+ new rows are available, while avoiding false differences from key-only old
33
+ rows.
34
+ - Require pgoutput-source-adapter 0.3.1 and use its stable transaction-local
35
+ event sequence to prevent deterministic ID collisions between otherwise
36
+ identical changes while preserving replay stability.
37
+
5
38
  ## 1.0.0 - 2026-07-17
6
39
 
7
40
  ### Added
data/README.md CHANGED
@@ -38,10 +38,28 @@ API documentation:
38
38
 
39
39
  https://kanutocd.github.io/mammoth/Mammoth.html
40
40
 
41
+ ## Start Here
41
42
 
42
- ## v1.0 Release Scope
43
+ The recommended first-run experience is the
44
+ [`webhooks-quickstart`](webhooks-quickstart) stack. One Compose command starts a
45
+ demo application, logical-replication-enabled PostgreSQL, Mammoth, a signed
46
+ webhook receiver with visible retries, and Mammoth health endpoints:
43
47
 
44
- Mammoth 1.0 includes:
48
+ ```bash
49
+ cd webhooks-quickstart
50
+ docker compose up --build --wait
51
+ ```
52
+
53
+ Once the flow is visible, follow its
54
+ [`ADAPTING.md`](webhooks-quickstart/ADAPTING.md) guide to connect Mammoth to an
55
+ existing PostgreSQL application. Its optional `monitoring` profile adds seeded
56
+ traffic, a provisioned Grafana dashboard and alert rules, and a curated
57
+ Prometheus query library; see the
58
+ [observability guide](docs/OBSERVABILITY.md#runnable-monitoring-showcase).
59
+
60
+ ## v1 Release Scope
61
+
62
+ Mammoth 1.x includes:
45
63
 
46
64
  - operator CLI for validation, bootstrap, status, delivery, observability, and
47
65
  dead-letter workflows
@@ -107,7 +125,9 @@ Human-readable CLI formatting and PostgreSQL-derived row columns are not frozen:
107
125
  scripts should rely on documented exit behavior, while receivers must tolerate
108
126
  additive fields and coordinate source schema changes. See
109
127
  [`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md) for the complete promise and
110
- major-version boundaries.
128
+ major-version boundaries. See
129
+ [`docs/WEBHOOK-PAYLOADS.md`](docs/WEBHOOK-PAYLOADS.md) for the canonical event
130
+ and transaction JSON contracts, column-change semantics, and event-ID behavior.
111
131
 
112
132
  ## Boundary
113
133
 
@@ -222,6 +242,7 @@ rather than opening SQLite directly.
222
242
  Mammoth includes local benchmarks for the product surfaces operators tune in
223
243
  production:
224
244
 
245
+ - event and transaction serialization, including deterministic fallback IDs
225
246
  - concurrent delivery runtime
226
247
  - real webhook delivery
227
248
  - multi-destination webhook fanout
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require "securerandom"
4
+ require "digest"
5
5
  require "time"
6
6
 
7
7
  module Mammoth
@@ -27,17 +27,17 @@ module Mammoth
27
27
  def initialize(event)
28
28
  raise ArgumentError, "event must be a CDC::Core::ChangeEvent" unless event.is_a?(CDC::Core::ChangeEvent)
29
29
 
30
- @event = event.to_h
30
+ @event = event
31
31
  end
32
32
 
33
33
  # Return the webhook payload.
34
34
  #
35
35
  # @return [Hash] webhook payload
36
36
  def call
37
- event_hash = stringify_keys(@event)
37
+ event_hash = stringify_keys(@event.to_h)
38
38
  metadata = stringify_keys(event_hash["metadata"] || {})
39
39
  {
40
- "event_id" => event_id(metadata),
40
+ "event_id" => event_id(event_hash, metadata),
41
41
  "source" => source(metadata),
42
42
  "operation" => normalize_operation(event_hash.fetch("operation")),
43
43
  "namespace" => event_hash["schema"],
@@ -47,7 +47,7 @@ module Mammoth
47
47
  "transaction_id" => event_hash["transaction_id"],
48
48
  "occurred_at" => occurred_at(event_hash),
49
49
  "data" => event_data(event_hash),
50
- "changes" => metadata["changes"] || [],
50
+ "changes" => serialized_changes(event_hash, metadata),
51
51
  "metadata" => metadata
52
52
  }
53
53
  end
@@ -69,8 +69,25 @@ module Mammoth
69
69
  operation.to_s
70
70
  end
71
71
 
72
- def event_id(metadata)
73
- metadata["event_id"] || SecureRandom.uuid
72
+ def event_id(event_hash, metadata)
73
+ metadata["event_id"] || deterministic_event_id(event_hash, metadata)
74
+ end
75
+
76
+ def deterministic_event_id(event_hash, metadata)
77
+ identity = {
78
+ source: source(metadata),
79
+ transaction_id: event_hash["transaction_id"],
80
+ source_position: event_hash["commit_lsn"],
81
+ sequence_number: event_hash["sequence_number"],
82
+ occurred_at: event_hash["occurred_at"],
83
+ namespace: event_hash["schema"],
84
+ entity: event_hash["table"],
85
+ operation: normalize_operation(event_hash.fetch("operation")),
86
+ primary_key: event_hash["primary_key"],
87
+ old_values: event_hash["old_values"],
88
+ new_values: event_hash["new_values"]
89
+ }
90
+ "evt_#{Digest::SHA256.hexdigest(JSON.generate(identity))}"
74
91
  end
75
92
 
76
93
  def source(metadata)
@@ -87,5 +104,20 @@ module Mammoth
87
104
  def event_data(event_hash)
88
105
  event_hash["data"] || event_hash["new_values"] || event_hash["old_values"] || {}
89
106
  end
107
+
108
+ def serialized_changes(event_hash, metadata)
109
+ return metadata["changes"] || [] if metadata.key?("changes")
110
+ return [] unless changes_available?(event_hash)
111
+
112
+ @event.changes.map(&:to_h)
113
+ end
114
+
115
+ def changes_available?(event_hash)
116
+ return true unless normalize_operation(event_hash.fetch("operation")) == "update"
117
+
118
+ old_values = event_hash["old_values"]
119
+ new_values = event_hash["new_values"]
120
+ old_values.is_a?(Hash) && new_values.is_a?(Hash) && old_values.keys.sort == new_values.keys.sort
121
+ end
90
122
  end
91
123
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require "securerandom"
4
+ require "digest"
5
5
  require "time"
6
6
 
7
7
  module Mammoth
@@ -38,7 +38,7 @@ module Mammoth
38
38
  def call
39
39
  event_payloads = envelope.events.map { |event| EventSerializer.call(event) }
40
40
  {
41
- "event_id" => envelope_metadata["event_id"] || SecureRandom.uuid,
41
+ "event_id" => envelope_metadata["event_id"] || deterministic_event_id(event_payloads),
42
42
  "type" => PAYLOAD_TYPE,
43
43
  "source" => first_event_value(event_payloads, "source") || EventSerializer::DEFAULT_SOURCE,
44
44
  "transaction_id" => envelope.transaction_id,
@@ -70,6 +70,16 @@ module Mammoth
70
70
  @envelope_metadata ||= stringify_keys(envelope.metadata)
71
71
  end
72
72
 
73
+ def deterministic_event_id(event_payloads)
74
+ identity = {
75
+ source: first_event_value(event_payloads, "source") || EventSerializer::DEFAULT_SOURCE,
76
+ transaction_id: envelope.transaction_id,
77
+ source_position: source_position(event_payloads),
78
+ event_ids: event_payloads.map { |payload| payload.fetch("event_id") }
79
+ }
80
+ "txn_#{Digest::SHA256.hexdigest(JSON.generate(identity))}"
81
+ end
82
+
73
83
  def source_position(event_payloads)
74
84
  envelope.commit_lsn ||
75
85
  first_event_value(event_payloads.reverse, "source_position")
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mammoth
4
4
  # Current Mammoth gem version.
5
- VERSION = "1.0.0"
5
+ VERSION = "1.2.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: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
@@ -122,7 +122,7 @@ dependencies:
122
122
  version: '0.3'
123
123
  - - ">="
124
124
  - !ruby/object:Gem::Version
125
- version: 0.3.0
125
+ version: 0.3.1
126
126
  type: :runtime
127
127
  prerelease: false
128
128
  version_requirements: !ruby/object:Gem::Requirement
@@ -132,7 +132,7 @@ dependencies:
132
132
  version: '0.3'
133
133
  - - ">="
134
134
  - !ruby/object:Gem::Version
135
- version: 0.3.0
135
+ version: 0.3.1
136
136
  - !ruby/object:Gem::Dependency
137
137
  name: sqlite3
138
138
  requirement: !ruby/object:Gem::Requirement