pgoutput-client 0.0.0 → 0.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: 8a179cfe8d0274992c3f3690f097d47e3b948002dcc30de7aa2b499d6e33a664
4
- data.tar.gz: 4fe772473f4926b0586ae819dc47f68c9b9d84cd95de343ceddf43ea6be96052
3
+ metadata.gz: 1413514b4a4dbe4f3c0f4c59744931ed865ca6faea5662f33844c0786a46e287
4
+ data.tar.gz: ebb972e2f0dd5a40f46161d6cd5be70df2f42db42e05862edf16f11e0f9a15ef
5
5
  SHA512:
6
- metadata.gz: 8f3bd156e1bd93334370744f148b719b27a97bee0becca0b10bc45a3eb088e952b0f57bfb2fbaae94d5f3b3d59c26e98a7e39921c176775cc2b1118d16ac253e
7
- data.tar.gz: 8fc1960bf9ee93f6558b008dfdf8bf456026d8cf13909639a148a9e7d568ca7b64236d2a895110018deccae6a9213e39f7b0dac1a9b685a087c73eb477174c50
6
+ metadata.gz: 7cb26f20d9a5a86c231956cb801c98b269aeebcf969dc2408b8ded3dbcae9a1b7995f88179480cbdd59c6f99b9d5df9d7910db78f0d84175d74fcc7e1ac6a0f5
7
+ data.tar.gz: 93a66bfe718e050fc636c77e78dcc1dd8c8bce064d64794f982944e9dc7a83ed261e3831a88d91ce1a6acb4c2498a5506135d815a8f6d278f5ff1fda168dc868
data/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
- ## [Unreleased]
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## [0.2.0] - 2026-06-16
6
+
7
+ ### Fixed
8
+
9
+ - Remove the configurable plugin surface; this transport layer is fixed to `pgoutput`.
10
+ - Wire `Pgoutput::Client::Runner#stop` to the active stream for cooperative shutdown.
11
+ - Back off briefly when the replication socket has no `CopyData` ready instead of busy polling.
12
+ - Retry live stream connection loss with backoff and resume from the latest confirmed WAL position.
13
+ - Avoid recreating an existing replication slot during reconnect attempts.
14
+ - Document the Docker-backed E2E workflow in the README and test skip hint.
15
+ - Document that replay, checkpointing, deduplication, and sink ordering belong to downstream layers.
16
+ - Add a live E2E reconnect test that restarts PostgreSQL mid-stream and verifies resume behavior.
17
+
18
+ ---
2
19
 
3
20
  ## [0.1.0] - 2026-05-31
4
21
 
5
- - Initial release
22
+ ### Added
23
+
24
+ - Initial transport-only PostgreSQL logical replication client.
25
+ - Added `Pgoutput::Client::Runner` facade.
26
+ - Added immutable configuration object.
27
+ - Added LSN parse and format helpers.
28
+ - Added XLogData envelope parsing.
29
+ - Added primary keepalive parsing.
30
+ - Added standby feedback payload builder.
31
+ - Added replication command builders.
32
+ - Added `PG::Connection` wrapper.
33
+ - Added logical replication stream loop.
34
+ - Added RBS signatures.
35
+ - Added Minitest test suite.
36
+ - Added README and examples.
37
+
38
+ ### Notes
39
+
40
+ This release intentionally does not parse pgoutput protocol messages or decode PostgreSQL values.
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
- The MIT License (MIT)
1
+ MIT License
2
2
 
3
- Copyright (c) 2026 Ken C. Demanawa
3
+ Copyright (c) 2026 Kenneth C. Demanawa
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
9
  copies of the Software, and to permit persons to whom the Software is
10
10
  furnished to do so, subject to the following conditions:
11
11
 
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
14
 
15
15
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
16
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
17
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,43 +1,370 @@
1
- # Pgoutput::Client
1
+ # pgoutput-client
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ [![Gem Version](https://badge.fury.io/rb/pgoutput-client.svg)](https://badge.fury.io/rb/pgoutput-client)
4
+ [![CI](https://github.com/kanutocd/pgoutput-client/workflows/CI/badge.svg)](https://github.com/kanutocd/pgoutput-client/actions)
5
+ [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.4-ruby.svg)](https://www.ruby-lang.org/en/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
7
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/pgoutput/client`. To experiment with that code, run `bin/console` for an interactive prompt.
8
+
9
+ A transport-only PostgreSQL logical replication client for receiving raw `pgoutput` payloads in Ruby.
10
+
11
+ `pgoutput-client` connects to PostgreSQL using logical replication, starts a `pgoutput` replication stream, receives `CopyData` messages, handles keepalives, sends standby feedback, and yields raw pgoutput payload bytes to downstream gems such as `pgoutput-parser` and `pgoutput-decoder`.
12
+
13
+ It intentionally does **not** parse row-change messages or decode PostgreSQL values.
14
+
15
+ ---
16
+
17
+ ## Requirements
18
+
19
+ - Ruby 3.4+
20
+ - PostgreSQL 10+
21
+ - `pg` gem
22
+ - PostgreSQL publication and logical replication slot
23
+
24
+ ---
25
+
26
+ ## Ecosystem Position
27
+
28
+ ```text
29
+ PostgreSQL logical replication
30
+
31
+
32
+ pgoutput-client
33
+
34
+
35
+ CopyData / pgoutput payloads
36
+
37
+
38
+ pgoutput-parser
39
+
40
+
41
+ Protocol messages
42
+
43
+
44
+ pgoutput-decoder
45
+
46
+
47
+ Decoded row events
48
+ ```
49
+
50
+ `pgoutput-client` is the transport layer only.
51
+
52
+ ---
53
+
54
+ ## Features
55
+
56
+ - Opens PostgreSQL logical replication connections
57
+ - Builds replication commands
58
+ - Supports `CREATE_REPLICATION_SLOT`
59
+ - Supports `DROP_REPLICATION_SLOT`
60
+ - Supports `START_REPLICATION SLOT ... LOGICAL ...`
61
+ - Parses XLogData envelopes
62
+ - Parses primary keepalive messages
63
+ - Builds standby feedback messages
64
+ - Provides LSN parse/format helpers
65
+ - Yields raw pgoutput payload bytes
66
+ - Includes RBS signatures
67
+ - Includes Minitest coverage
68
+ - No audit, parser, or decoder concerns
69
+
70
+ ---
6
71
 
7
72
  ## Installation
8
73
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
74
+ ```ruby
75
+ gem "pgoutput-client"
76
+ ```
77
+
78
+ Then:
79
+
80
+ ```bash
81
+ bundle install
82
+ ```
83
+
84
+ Require:
85
+
86
+ ```ruby
87
+ require "pgoutput-client"
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Quick Start
93
+
94
+ ```ruby
95
+ require "pgoutput-client"
96
+
97
+ client =
98
+ Pgoutput::Client::Runner.new(
99
+ database_url: ENV.fetch("DATABASE_URL"),
100
+ slot_name: "my_slot",
101
+ publication_names: ["my_publication"],
102
+ auto_create_slot: true
103
+ )
104
+
105
+ client.start do |payload, metadata|
106
+ puts "WAL end: #{metadata.wal_end_lsn}"
107
+ puts "Raw pgoutput payload bytes: #{payload.bytesize}"
108
+ end
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Using With pgoutput-parser
114
+
115
+ ```ruby
116
+ require "pgoutput-client"
117
+ require "pgoutput"
118
+
119
+ client = Pgoutput::Client::Runner.new(
120
+ database_url: ENV.fetch("DATABASE_URL"),
121
+ slot_name: "my_slot",
122
+ publication_names: ["my_publication"]
123
+ )
124
+
125
+ tracker = Pgoutput::RelationTracker.new
126
+
127
+ client.start do |payload, metadata|
128
+ message = tracker.process(payload)
129
+ p [metadata.wal_end_lsn, message]
130
+ end
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Using With pgoutput-decoder
136
+
137
+ ```ruby
138
+ require "pgoutput-client"
139
+ require "pgoutput"
140
+ require "pgoutput/decoder"
141
+
142
+ tracker = Pgoutput::RelationTracker.new
143
+ decoder = Pgoutput::Decoder.new
144
+
145
+ client.start do |payload, metadata|
146
+ protocol_message = tracker.process(payload)
147
+ event = decoder.decode(protocol_message)
148
+ p [metadata.wal_end_lsn, event]
149
+ end
150
+ ```
151
+
152
+ ---
153
+
154
+ ## What This Gem Does
155
+
156
+ ```text
157
+ PostgreSQL replication connection
158
+
159
+
160
+ CopyData stream
161
+
162
+
163
+ XLogData / Keepalive handling
164
+
165
+
166
+ Raw pgoutput payloads
167
+ ```
168
+
169
+ It owns:
170
+
171
+ - Replication connection setup
172
+ - Replication command generation
173
+ - CopyData reading
174
+ - XLogData envelope parsing
175
+ - Keepalive handling
176
+ - Standby status feedback
177
+ - LSN conversion
178
+
179
+ ---
180
+
181
+ ## What This Gem Does Not Do
182
+
183
+ It does not:
184
+
185
+ - Parse pgoutput row messages
186
+ - Decode PostgreSQL OIDs
187
+ - Build application events
188
+ - Group transactions
189
+ - Run processor pipelines
190
+ - Manage Ractor worker pools
191
+ - Store audit records
192
+ - Own replay, checkpointing, deduplication, or sink ordering
193
+
194
+ Those responsibilities belong to higher layers, especially `cdc-core` and the sink that materializes downstream state.
195
+
196
+ ## Failure Semantics
197
+
198
+ If the live replication stream loses its connection, `pgoutput-client` retries a small number of times with a backoff and resumes from the latest confirmed WAL position.
199
+
200
+ It does not decide replay policy, deduplication strategy, checkpoint storage, or exactly-once delivery. Those concerns belong to the downstream CDC runtime and sink layer.
201
+
202
+ ---
203
+
204
+ ## Logical Replication Setup
205
+
206
+ Example PostgreSQL setup:
207
+
208
+ ```sql
209
+ ALTER SYSTEM SET wal_level = logical;
210
+
211
+ CREATE PUBLICATION my_publication FOR TABLE users, posts;
212
+ ```
213
+
214
+ Create a slot automatically:
10
215
 
11
- Install the gem and add to the application's Gemfile by executing:
216
+ ```ruby
217
+ Pgoutput::Client::Runner.new(
218
+ database_url: ENV.fetch("DATABASE_URL"),
219
+ slot_name: "my_slot",
220
+ publication_names: ["my_publication"],
221
+ auto_create_slot: true
222
+ )
223
+ ```
224
+
225
+ Or create the slot yourself:
226
+
227
+ ```sql
228
+ SELECT * FROM pg_create_logical_replication_slot('my_slot', 'pgoutput');
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Public API
234
+
235
+ ### Pgoutput::Client::Runner
236
+
237
+ High-level facade.
238
+
239
+ ```ruby
240
+ client = Pgoutput::Client::Runner.new(...)
241
+ client.start { |payload, metadata| ... }
242
+ ```
243
+
244
+ ### Pgoutput::Client::Configuration
245
+
246
+ Immutable configuration object.
247
+
248
+ ### Pgoutput::Client::Connection
249
+
250
+ Thin wrapper around `PG::Connection` for replication commands.
251
+
252
+ ### Pgoutput::Client::Stream
253
+
254
+ Consumes CopyData messages and yields pgoutput payloads.
255
+
256
+ ### Pgoutput::Client::LSN
257
+
258
+ ```ruby
259
+ Pgoutput::Client::LSN.parse("0/16B6C50")
260
+ Pgoutput::Client::LSN.format(23_817_296)
261
+ ```
262
+
263
+ ### Pgoutput::Client::XLogData
264
+
265
+ Represents a WAL data envelope.
266
+
267
+ ### Pgoutput::Client::Keepalive
268
+
269
+ Represents a primary keepalive message.
270
+
271
+ ### Pgoutput::Client::Feedback
272
+
273
+ Builds standby status update payloads.
274
+
275
+ ---
276
+
277
+ ## Ractor Position
278
+
279
+ The replication connection itself is stateful and ordered. It should normally run as a single reader.
280
+
281
+ Downstream parsing, decoding, and processing can be parallelized with Ractors:
282
+
283
+ ```text
284
+ pgoutput-client reader
285
+
286
+
287
+ Ractor-safe queue
288
+
289
+
290
+ parser / decoder / processor pools
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Rake Tasks
296
+
297
+ ### Default
298
+
299
+ Run them all
300
+
301
+ ```bash
302
+ bundle exec rake
303
+ ```
304
+
305
+ ### Code Linting and Formatting
12
306
 
13
307
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
308
+ bundle exec rake rubocop
15
309
  ```
16
310
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
311
+ ### Testing
18
312
 
19
313
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
314
+ bundle exec rake test
21
315
  ```
22
316
 
23
- ## Usage
317
+ With coverage:
24
318
 
25
- TODO: Write usage instructions here
319
+ ```bash
320
+ COVERAGE=true bundle exec rake test
321
+ ```
322
+ ---
26
323
 
27
- ## Development
324
+ ### Type Checking
28
325
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
326
+ ```bash
327
+ bundle exec rbs:validate
328
+ ```
30
329
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
330
+ ---
32
331
 
33
- ## Contributing
332
+ ### Documentation
34
333
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pgoutput-client. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/pgoutput-client/blob/main/CODE_OF_CONDUCT.md).
334
+ ```bash
335
+ bundle exec rake yard
336
+ ```
36
337
 
37
- ## License
338
+ ### End-to-End PostgreSQL
38
339
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
340
+ Run the full Docker-backed E2E flow and clean up afterward:
40
341
 
41
- ## Code of Conduct
342
+ ```bash
343
+ script/test-e2e
344
+ ```
345
+
346
+ Keep PostgreSQL running after the test for debugging:
347
+
348
+ ```bash
349
+ KEEP_E2E_POSTGRES=1 script/test-e2e
350
+ ```
351
+
352
+ You can also run the steps manually:
353
+
354
+ ```bash
355
+ script/e2e-up
356
+ PGOUTPUT_CLIENT_E2E=1 bundle exec rake test:e2e
357
+ script/e2e-down
358
+ ```
359
+
360
+ Equivalent Rake task:
361
+
362
+ ```bash
363
+ bundle exec rake e2e:run
364
+ ```
365
+
366
+ ---
367
+
368
+ ## License
42
369
 
43
- Everyone interacting in the Pgoutput::Client project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/pgoutput-client/blob/main/CODE_OF_CONDUCT.md).
370
+ MIT.
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgoutput
4
+ module Client
5
+ # SQL command builders for PostgreSQL replication-mode commands.
6
+ #
7
+ # PostgreSQL replication commands are issued on a connection opened with the
8
+ # replication parameter enabled. The methods in this module render the small
9
+ # command subset needed by `pgoutput-client` and rely on {Configuration} to
10
+ # validate identifier-like values before interpolation.
11
+ #
12
+ # @api private
13
+ module Commands
14
+ module_function
15
+
16
+ # Render a `CREATE_REPLICATION_SLOT` command.
17
+ #
18
+ # Temporary slots are requested only when
19
+ # {Configuration#temporary_slot} is true.
20
+ #
21
+ # @example Permanent slot
22
+ # Commands.create_replication_slot(config)
23
+ # # => "CREATE_REPLICATION_SLOT cdc_slot LOGICAL pgoutput"
24
+ #
25
+ # @param configuration [Configuration] replication configuration
26
+ # @return [String] SQL command suitable for `PG::Connection#exec`
27
+ def create_replication_slot(configuration)
28
+ temporary = configuration.temporary_slot ? " TEMPORARY" : ""
29
+ "CREATE_REPLICATION_SLOT #{configuration.slot_name}#{temporary} LOGICAL #{Configuration::DEFAULT_PLUGIN}"
30
+ end
31
+
32
+ # Render a `DROP_REPLICATION_SLOT` command.
33
+ #
34
+ # @param configuration [Configuration] replication configuration
35
+ # @return [String] SQL command suitable for `PG::Connection#exec`
36
+ def drop_replication_slot(configuration)
37
+ "DROP_REPLICATION_SLOT #{configuration.slot_name}"
38
+ end
39
+
40
+ # Render a `START_REPLICATION SLOT ... LOGICAL ...` command.
41
+ #
42
+ # The command includes the pgoutput options required by PostgreSQL:
43
+ # `proto_version` and `publication_names`. Optional pgoutput switches such
44
+ # as `binary` and `messages` are emitted only when enabled.
45
+ #
46
+ # @param configuration [Configuration] replication configuration
47
+ # @return [String] SQL command suitable for `PG::Connection#exec`
48
+ def start_replication(configuration)
49
+ options = {
50
+ "proto_version" => configuration.proto_version.to_s,
51
+ "publication_names" => configuration.publication_names.join(","),
52
+ "binary" => configuration.binary ? "true" : nil,
53
+ "messages" => configuration.messages ? "true" : nil
54
+ }.compact
55
+
56
+ rendered_options = options.map { |key, value| %("#{key}" '#{value}') }.join(", ")
57
+
58
+ "START_REPLICATION SLOT #{configuration.slot_name} LOGICAL #{configuration.start_lsn_string} (#{rendered_options})"
59
+ end
60
+ end
61
+ end
62
+ end