pgoutput-client 0.0.0 → 0.1.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: 3dd08a56a5b98573babde8d5aa72e02c44668877803a5272006879c28bd69ebd
4
+ data.tar.gz: c385343185a60a6304ecc276b5f6fac207cb5791fb8035823d863620b238ebda
5
5
  SHA512:
6
- metadata.gz: 8f3bd156e1bd93334370744f148b719b27a97bee0becca0b10bc45a3eb088e952b0f57bfb2fbaae94d5f3b3d59c26e98a7e39921c176775cc2b1118d16ac253e
7
- data.tar.gz: 8fc1960bf9ee93f6558b008dfdf8bf456026d8cf13909639a148a9e7d568ca7b64236d2a895110018deccae6a9213e39f7b0dac1a9b685a087c73eb477174c50
6
+ metadata.gz: 687bd8396cdf3b7a9e019c6cf2c7d584cfa2dcd273dcb5a4c662079d059c03aa2c55b487f9b1d8f58cc82ebfc36421d9d65c774f1fbb6ec6e65ba811418cb82e
7
+ data.tar.gz: 8d5e7c7a2b172084071006ca893e85fcff841539919bab78065a5a70d61fed89929dcd469b0837e959a164a7c3c882734bce24405c2660400c1f06b159e56401
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
- ## [Unreleased]
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ### Added
9
+
10
+ - Placeholder for future development.
11
+
12
+ ---
2
13
 
3
14
  ## [0.1.0] - 2026-05-31
4
15
 
5
- - Initial release
16
+ ### Added
17
+
18
+ - Initial transport-only PostgreSQL logical replication client.
19
+ - Added `Pgoutput::Client::Runner` facade.
20
+ - Added immutable configuration object.
21
+ - Added LSN parse and format helpers.
22
+ - Added XLogData envelope parsing.
23
+ - Added primary keepalive parsing.
24
+ - Added standby feedback payload builder.
25
+ - Added replication command builders.
26
+ - Added `PG::Connection` wrapper.
27
+ - Added logical replication stream loop.
28
+ - Added RBS signatures.
29
+ - Added Minitest test suite.
30
+ - Added README and examples.
31
+
32
+ ### Notes
33
+
34
+ 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,336 @@
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
+ [![Coverage Status](https://codecov.io/gh/kanutocd/pgoutput-client/branch/main/graph/badge.svg)](https://codecov.io/gh/kanutocd/pgoutput-client)
6
+ [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.4-ruby.svg)](https://www.ruby-lang.org/en/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
8
 
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.
9
+
10
+ A transport-only PostgreSQL logical replication client for receiving raw `pgoutput` payloads in Ruby.
11
+
12
+ `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`.
13
+
14
+ It intentionally does **not** parse row-change messages or decode PostgreSQL values.
15
+
16
+ ---
17
+
18
+ ## Requirements
19
+
20
+ - Ruby 3.4+
21
+ - PostgreSQL 10+
22
+ - `pg` gem
23
+ - PostgreSQL publication and logical replication slot
24
+
25
+ ---
26
+
27
+ ## Ecosystem Position
28
+
29
+ ```text
30
+ PostgreSQL logical replication
31
+
32
+
33
+ pgoutput-client
34
+
35
+
36
+ CopyData / pgoutput payloads
37
+
38
+
39
+ pgoutput-parser
40
+
41
+
42
+ Protocol messages
43
+
44
+
45
+ pgoutput-decoder
46
+
47
+
48
+ Decoded row events
49
+ ```
50
+
51
+ `pgoutput-client` is the transport layer only.
52
+
53
+ ---
54
+
55
+ ## Features
56
+
57
+ - Opens PostgreSQL logical replication connections
58
+ - Builds replication commands
59
+ - Supports `CREATE_REPLICATION_SLOT`
60
+ - Supports `DROP_REPLICATION_SLOT`
61
+ - Supports `START_REPLICATION SLOT ... LOGICAL ...`
62
+ - Parses XLogData envelopes
63
+ - Parses primary keepalive messages
64
+ - Builds standby feedback messages
65
+ - Provides LSN parse/format helpers
66
+ - Yields raw pgoutput payload bytes
67
+ - Includes RBS signatures
68
+ - Includes Minitest coverage
69
+ - No audit, parser, or decoder concerns
70
+
71
+ ---
6
72
 
7
73
  ## Installation
8
74
 
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.
75
+ ```ruby
76
+ gem "pgoutput-client"
77
+ ```
78
+
79
+ Then:
80
+
81
+ ```bash
82
+ bundle install
83
+ ```
84
+
85
+ Require:
86
+
87
+ ```ruby
88
+ require "pgoutput-client"
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Quick Start
94
+
95
+ ```ruby
96
+ require "pgoutput-client"
97
+
98
+ client =
99
+ Pgoutput::Client::Runner.new(
100
+ database_url: ENV.fetch("DATABASE_URL"),
101
+ slot_name: "my_slot",
102
+ publication_names: ["my_publication"],
103
+ auto_create_slot: true
104
+ )
105
+
106
+ client.start do |payload, metadata|
107
+ puts "WAL end: #{metadata.wal_end_lsn}"
108
+ puts "Raw pgoutput payload bytes: #{payload.bytesize}"
109
+ end
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Using With pgoutput-parser
115
+
116
+ ```ruby
117
+ require "pgoutput-client"
118
+ require "pgoutput"
119
+
120
+ client = Pgoutput::Client::Runner.new(
121
+ database_url: ENV.fetch("DATABASE_URL"),
122
+ slot_name: "my_slot",
123
+ publication_names: ["my_publication"]
124
+ )
125
+
126
+ tracker = Pgoutput::RelationTracker.new
127
+
128
+ client.start do |payload, metadata|
129
+ message = tracker.process(payload)
130
+ p [metadata.wal_end_lsn, message]
131
+ end
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Using With pgoutput-decoder
137
+
138
+ ```ruby
139
+ require "pgoutput-client"
140
+ require "pgoutput"
141
+ require "pgoutput/decoder"
142
+
143
+ tracker = Pgoutput::RelationTracker.new
144
+ decoder = Pgoutput::Decoder.new
145
+
146
+ client.start do |payload, metadata|
147
+ protocol_message = tracker.process(payload)
148
+ event = decoder.decode(protocol_message)
149
+ p [metadata.wal_end_lsn, event]
150
+ end
151
+ ```
152
+
153
+ ---
154
+
155
+ ## What This Gem Does
156
+
157
+ ```text
158
+ PostgreSQL replication connection
159
+
160
+
161
+ CopyData stream
162
+
163
+
164
+ XLogData / Keepalive handling
165
+
166
+
167
+ Raw pgoutput payloads
168
+ ```
169
+
170
+ It owns:
171
+
172
+ - Replication connection setup
173
+ - Replication command generation
174
+ - CopyData reading
175
+ - XLogData envelope parsing
176
+ - Keepalive handling
177
+ - Standby status feedback
178
+ - LSN conversion
179
+
180
+ ---
181
+
182
+ ## What This Gem Does Not Do
183
+
184
+ It does not:
185
+
186
+ - Parse pgoutput row messages
187
+ - Decode PostgreSQL OIDs
188
+ - Build application events
189
+ - Group transactions
190
+ - Run processor pipelines
191
+ - Manage Ractor worker pools
192
+ - Store audit records
193
+
194
+ Those responsibilities belong to higher layers.
10
195
 
11
- Install the gem and add to the application's Gemfile by executing:
196
+ ---
197
+
198
+ ## Logical Replication Setup
199
+
200
+ Example PostgreSQL setup:
201
+
202
+ ```sql
203
+ ALTER SYSTEM SET wal_level = logical;
204
+
205
+ CREATE PUBLICATION my_publication FOR TABLE users, posts;
206
+ ```
207
+
208
+ Create a slot automatically:
209
+
210
+ ```ruby
211
+ Pgoutput::Client::Runner.new(
212
+ database_url: ENV.fetch("DATABASE_URL"),
213
+ slot_name: "my_slot",
214
+ publication_names: ["my_publication"],
215
+ auto_create_slot: true
216
+ )
217
+ ```
218
+
219
+ Or create the slot yourself:
220
+
221
+ ```sql
222
+ SELECT * FROM pg_create_logical_replication_slot('my_slot', 'pgoutput');
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Public API
228
+
229
+ ### Pgoutput::Client::Runner
230
+
231
+ High-level facade.
232
+
233
+ ```ruby
234
+ client = Pgoutput::Client::Runner.new(...)
235
+ client.start { |payload, metadata| ... }
236
+ ```
237
+
238
+ ### Pgoutput::Client::Configuration
239
+
240
+ Immutable configuration object.
241
+
242
+ ### Pgoutput::Client::Connection
243
+
244
+ Thin wrapper around `PG::Connection` for replication commands.
245
+
246
+ ### Pgoutput::Client::Stream
247
+
248
+ Consumes CopyData messages and yields pgoutput payloads.
249
+
250
+ ### Pgoutput::Client::LSN
251
+
252
+ ```ruby
253
+ Pgoutput::Client::LSN.parse("0/16B6C50")
254
+ Pgoutput::Client::LSN.format(23_817_296)
255
+ ```
256
+
257
+ ### Pgoutput::Client::XLogData
258
+
259
+ Represents a WAL data envelope.
260
+
261
+ ### Pgoutput::Client::Keepalive
262
+
263
+ Represents a primary keepalive message.
264
+
265
+ ### Pgoutput::Client::Feedback
266
+
267
+ Builds standby status update payloads.
268
+
269
+ ---
270
+
271
+ ## Ractor Position
272
+
273
+ The replication connection itself is stateful and ordered. It should normally run as a single reader.
274
+
275
+ Downstream parsing, decoding, and processing can be parallelized with Ractors:
276
+
277
+ ```text
278
+ pgoutput-client reader
279
+
280
+
281
+ Ractor-safe queue
282
+
283
+
284
+ parser / decoder / processor pools
285
+ ```
286
+
287
+ ---
288
+
289
+ ## Rake Tasks
290
+
291
+ ### Default
292
+
293
+ Run them all
12
294
 
13
295
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
296
+ bundle exec rake
15
297
  ```
16
298
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
299
+ ### Code Linting and Formatting
18
300
 
19
301
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
302
+ bundle exec rake rubocop
21
303
  ```
22
304
 
23
- ## Usage
305
+ ### Testing
24
306
 
25
- TODO: Write usage instructions here
307
+ ```bash
308
+ bundle exec rake test
309
+ ```
26
310
 
27
- ## Development
311
+ With coverage:
28
312
 
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.
313
+ ```bash
314
+ COVERAGE=true bundle exec rake test
315
+ ```
316
+ ---
30
317
 
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).
318
+ ### Type Checking
32
319
 
33
- ## Contributing
320
+ ```bash
321
+ bundle exec rbs:validate
322
+ ```
34
323
 
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).
324
+ ---
36
325
 
37
- ## License
326
+ ### Documentation
327
+
328
+ ```bash
329
+ bundle exec rake yard
330
+ ```
38
331
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
332
+ ---
40
333
 
41
- ## Code of Conduct
334
+ ## License
42
335
 
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).
336
+ 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.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
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgoutput
4
+ module Client
5
+ # Immutable configuration for a PostgreSQL logical replication stream.
6
+ #
7
+ # A configuration describes how `pgoutput-client` should connect to
8
+ # PostgreSQL and how it should request logical replication from the server.
9
+ # It deliberately contains transport-level settings only; parsing pgoutput
10
+ # records and decoding PostgreSQL values belong to downstream layers.
11
+ #
12
+ # The object freezes itself and its string/array attributes during
13
+ # initialization so it can be safely shared by transport components without
14
+ # defensive copying.
15
+ #
16
+ # @example Minimal configuration
17
+ # config = Pgoutput::Client::Configuration.new(
18
+ # database_url: "postgres://localhost/app",
19
+ # slot_name: "cdc_slot",
20
+ # publication_names: "app_publication"
21
+ # )
22
+ #
23
+ # @example Start from a known LSN and request binary values from pgoutput
24
+ # config = Pgoutput::Client::Configuration.new(
25
+ # database_url: ENV.fetch("DATABASE_URL"),
26
+ # slot_name: "cdc_slot",
27
+ # publication_names: %w[app_publication],
28
+ # start_lsn: "0/16B6C50",
29
+ # binary: true
30
+ # )
31
+ #
32
+ # @api public
33
+ class Configuration
34
+ # Default logical decoding output plugin.
35
+ #
36
+ # @return [String]
37
+ DEFAULT_PLUGIN = "pgoutput"
38
+
39
+ # Default pgoutput protocol version.
40
+ #
41
+ # @return [Integer]
42
+ DEFAULT_PROTO_VERSION = 1
43
+
44
+ # Default interval, in seconds, between standby status feedback messages.
45
+ #
46
+ # @return [Float]
47
+ DEFAULT_FEEDBACK_INTERVAL = 10.0
48
+
49
+ # @!attribute [r] database_url
50
+ # PostgreSQL connection URL.
51
+ # @return [String]
52
+ # @!attribute [r] slot_name
53
+ # Logical replication slot name.
54
+ # @return [String]
55
+ # @!attribute [r] publication_names
56
+ # Publication names requested from pgoutput.
57
+ # @return [Array<String>]
58
+ # @!attribute [r] start_lsn
59
+ # Optional normalized starting LSN.
60
+ # @return [String, nil]
61
+ # @!attribute [r] plugin
62
+ # Logical decoding output plugin name.
63
+ # @return [String]
64
+ # @!attribute [r] proto_version
65
+ # pgoutput protocol version.
66
+ # @return [Integer]
67
+ # @!attribute [r] binary
68
+ # Whether to request binary column values from pgoutput.
69
+ # @return [Boolean]
70
+ # @!attribute [r] messages
71
+ # Whether to request logical decoding messages from pgoutput.
72
+ # @return [Boolean]
73
+ # @!attribute [r] auto_create_slot
74
+ # Whether the client should create the slot before streaming.
75
+ # @return [Boolean]
76
+ # @!attribute [r] temporary_slot
77
+ # Whether a newly created slot should be temporary.
78
+ # @return [Boolean]
79
+ # @!attribute [r] feedback_interval
80
+ # Standby feedback interval in seconds.
81
+ # @return [Float]
82
+ attr_reader :database_url,
83
+ :slot_name,
84
+ :publication_names,
85
+ :start_lsn,
86
+ :plugin,
87
+ :proto_version,
88
+ :binary,
89
+ :messages,
90
+ :auto_create_slot,
91
+ :temporary_slot,
92
+ :feedback_interval
93
+
94
+ # Build and validate a logical replication stream configuration.
95
+ #
96
+ # `slot_name` and every publication name are intentionally limited to
97
+ # simple PostgreSQL identifier-like strings. This keeps command rendering
98
+ # small and predictable while avoiding quoting rules in this transport
99
+ # layer.
100
+ #
101
+ # Boolean options are normalized with Ruby truthiness. `nil` and `false`
102
+ # become `false`; all other values become `true`.
103
+ #
104
+ # @param database_url [#to_s] PostgreSQL connection URL
105
+ # @param slot_name [#to_s] logical replication slot name
106
+ # @param publication_names [Array<#to_s>, #to_s] one or more publication
107
+ # names to pass to pgoutput
108
+ # @param start_lsn [String, Integer, nil] starting LSN as a PostgreSQL LSN
109
+ # string, an integer WAL position, or `nil` for `0/0`
110
+ # @param plugin [#to_s] logical decoding plugin name
111
+ # @param proto_version [#to_int, #to_s] pgoutput protocol version
112
+ # @param binary [Object] truthy to request binary column values
113
+ # @param messages [Object] truthy to request logical decoding messages
114
+ # @param auto_create_slot [Object] truthy to create the slot before
115
+ # starting replication
116
+ # @param temporary_slot [Object] truthy to create a temporary replication
117
+ # slot when `auto_create_slot` is enabled
118
+ # @param feedback_interval [#to_f, #to_s] seconds between periodic standby
119
+ # feedback messages
120
+ # @return [void]
121
+ # @raise [ConfigurationError] if publication names are empty or numeric
122
+ # settings are invalid
123
+ # @raise [ArgumentError] if `start_lsn`, `proto_version`, or
124
+ # `feedback_interval` cannot be coerced
125
+ def initialize(database_url:,
126
+ slot_name:,
127
+ publication_names:,
128
+ start_lsn: nil,
129
+ plugin: DEFAULT_PLUGIN,
130
+ proto_version: DEFAULT_PROTO_VERSION,
131
+ binary: false,
132
+ messages: false,
133
+ auto_create_slot: false,
134
+ temporary_slot: false,
135
+ feedback_interval: DEFAULT_FEEDBACK_INTERVAL)
136
+ @database_url = String(database_url).freeze
137
+ @slot_name = validate_identifier(slot_name, "slot_name").freeze
138
+ @publication_names = Array(publication_names).map do |name|
139
+ validate_identifier(name, "publication_name").freeze
140
+ end.freeze
141
+ @start_lsn = normalize_lsn(start_lsn).freeze
142
+ @plugin = String(plugin).freeze
143
+ @proto_version = Integer(proto_version)
144
+ @binary = boolean(binary, "binary")
145
+ @messages = boolean(messages, "messages")
146
+ @auto_create_slot = boolean(auto_create_slot, "auto_create_slot")
147
+ @temporary_slot = boolean(temporary_slot, "temporary_slot")
148
+ @feedback_interval = Float(feedback_interval)
149
+
150
+ validate!
151
+ freeze
152
+ end
153
+
154
+ # Starting LSN to render in `START_REPLICATION`.
155
+ #
156
+ # @return [String] normalized LSN string, defaulting to `"0/0"`
157
+ def start_lsn_string
158
+ start_lsn || "0/0"
159
+ end
160
+
161
+ private
162
+
163
+ def validate!
164
+ raise ConfigurationError, "publication_names must not be empty" if publication_names.empty?
165
+ raise ConfigurationError, "proto_version must be positive" unless proto_version.positive?
166
+ raise ConfigurationError, "feedback_interval must be positive" unless feedback_interval.positive?
167
+ end
168
+
169
+ def normalize_lsn(value)
170
+ return nil if value.nil?
171
+ return LSN.format(value) if value.is_a?(Integer)
172
+
173
+ LSN.format(LSN.parse(String(value)))
174
+ end
175
+
176
+ def validate_identifier(value, field)
177
+ string = String(value)
178
+ unless string.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
179
+ raise ConfigurationError, "#{field} must be a PostgreSQL identifier-like string"
180
+ end
181
+
182
+ string
183
+ end
184
+
185
+ # Boolean type checking helper
186
+ def boolean(value, name)
187
+ return true if value == true
188
+ return false if value == false
189
+
190
+ raise ArgumentError, "#{name} must be true or false"
191
+ end
192
+ end
193
+ end
194
+ end