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.
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgoutput
4
+ module Client
5
+ # High-level logical replication client facade.
6
+ #
7
+ # `Runner` is the simplest public entry point for consumers that want to
8
+ # stream raw pgoutput payloads without manually managing a replication
9
+ # connection. It owns the connection lifecycle for one logical replication
10
+ # stream:
11
+ #
12
+ # 1. Build an immutable {Configuration} from keyword arguments.
13
+ # 2. Open a PostgreSQL replication connection.
14
+ # 3. Optionally create the configured replication slot.
15
+ # 4. Start logical replication.
16
+ # 5. Yield raw pgoutput payload bytes and {XLogData} metadata.
17
+ # 6. Close the connection when streaming exits.
18
+ #
19
+ # If a live stream loses its connection, the runner retries a small number
20
+ # of times with a backoff and resumes from the latest confirmed WAL
21
+ # position. Replay, checkpointing, and deduplication are not owned here;
22
+ # those concerns belong to the downstream CDC runtime and sink layer.
23
+ #
24
+ # @example Stream raw pgoutput messages
25
+ # runner = Pgoutput::Client::Runner.new(
26
+ # database_url: "postgres://localhost/app",
27
+ # slot_name: "cdc_slot",
28
+ # publication_names: ["app_publication"]
29
+ # )
30
+ #
31
+ # runner.start do |payload, metadata|
32
+ # puts "received #{payload.bytesize} bytes at #{metadata.wal_end_lsn}"
33
+ # end
34
+ #
35
+ # @see Configuration
36
+ # @see Connection
37
+ # @see Stream
38
+ # @api public
39
+ class Runner
40
+ DEFAULT_RECONNECT_ATTEMPTS = 3
41
+ DEFAULT_RECONNECT_BACKOFF = 0.5
42
+
43
+ # Configuration used by this runner.
44
+ #
45
+ # @return [Configuration]
46
+ attr_reader :configuration
47
+
48
+ # Last transport error seen by the runner.
49
+ #
50
+ # @return [Exception, nil]
51
+ attr_reader :last_error
52
+
53
+ # Build a runner from configuration keyword arguments.
54
+ #
55
+ # The accepted keywords are the same as {Configuration#initialize}. The
56
+ # resulting configuration object is immutable and reused for the lifetime
57
+ # of this runner.
58
+ #
59
+ # @param options [Hash{Symbol=>Object}] configuration options forwarded to
60
+ # {Configuration#initialize}
61
+ # @return [void]
62
+ # @raise [ConfigurationError] if the supplied configuration is invalid
63
+ def initialize(**options)
64
+ @configuration = Configuration.new(
65
+ **options # : untyped
66
+ )
67
+ @stopped = true
68
+ @running = false
69
+ @stream = nil
70
+ @resume_lsn = configuration.start_lsn
71
+ @acked_lsn = configuration.start_lsn
72
+ @slot_created = false
73
+ @last_error = nil
74
+ @reconnect_attempts = 0
75
+ end
76
+
77
+ # Start streaming raw pgoutput payloads.
78
+ #
79
+ # This method blocks until the stream stops or the underlying connection
80
+ # raises an error. The yielded payload is the raw logical decoding plugin
81
+ # payload contained inside PostgreSQL's XLogData envelope; callers normally
82
+ # pass this payload to a pgoutput parser.
83
+ #
84
+ # @yield [payload, metadata] called once for each XLogData payload
85
+ # @yieldparam payload [String] frozen raw pgoutput payload bytes
86
+ # @yieldparam metadata [XLogData] WAL envelope metadata for the payload
87
+ # @return [void]
88
+ # @raise [ArgumentError] if no block is provided
89
+ # @raise [ConnectionError] if a PostgreSQL connection or command fails
90
+ # @raise [ProtocolError] if an invalid replication message is received
91
+ def start(&block)
92
+ raise ArgumentError, "block required" unless block
93
+
94
+ @stopped = false
95
+ @running = true
96
+ @last_error = nil
97
+ @reconnect_attempts = 0
98
+
99
+ loop do
100
+ current_configuration = configuration_for_resume
101
+ case run_stream_cycle(current_configuration, &block)
102
+ when :done
103
+ break
104
+ when :retry
105
+ @reconnect_attempts += 1
106
+ raise @last_error if @reconnect_attempts > DEFAULT_RECONNECT_ATTEMPTS
107
+
108
+ sleep(reconnect_backoff_for(@reconnect_attempts))
109
+ end
110
+ end
111
+ ensure
112
+ @running = false
113
+ @stopped = true
114
+ end
115
+
116
+ # Request graceful stop.
117
+ #
118
+ # This method records the caller's intent to stop and asks the active
119
+ # {Stream}, if any, to stop after its current iteration.
120
+ #
121
+ # @return [void]
122
+ def stop
123
+ @stopped = true
124
+ @stream&.stop
125
+ nil
126
+ end
127
+
128
+ # Stop the active stream and start again with the same block.
129
+ #
130
+ # The runner is synchronous, so this helper is primarily useful for
131
+ # supervisors that call it instead of manually calling {#stop} followed by
132
+ # {#start}.
133
+ #
134
+ # @yield [payload, metadata] called once for each XLogData payload
135
+ # @return [void]
136
+ def restart(&block)
137
+ stop
138
+ start(&block)
139
+ end
140
+
141
+ # Whether the runner is currently inside its streaming loop.
142
+ #
143
+ # @return [Boolean]
144
+ def running?
145
+ @running
146
+ end
147
+
148
+ # Whether the runner has stopped.
149
+ #
150
+ # @return [Boolean]
151
+ def stopped?
152
+ !running?
153
+ end
154
+
155
+ # Whether an active replication stream exists.
156
+ #
157
+ # @return [Boolean]
158
+ def connected?
159
+ !@stream.nil?
160
+ end
161
+
162
+ # Mark a WAL position as durably handled by downstream code.
163
+ #
164
+ # This does not checkpoint or persist anything. It only updates transport
165
+ # feedback state so future standby status updates can distinguish received
166
+ # WAL from downstream-acknowledged WAL.
167
+ #
168
+ # @param lsn [String, Integer] WAL position acknowledged by downstream code
169
+ # @return [Integer] normalized acknowledged WAL position
170
+ def ack(lsn)
171
+ parsed = normalize_lsn_value(lsn)
172
+ @acked_lsn = [@acked_lsn ? normalize_lsn_value(@acked_lsn) : 0, parsed].max
173
+ @stream&.ack(@acked_lsn)
174
+ @acked_lsn
175
+ end
176
+
177
+ # Return an immutable transport status snapshot.
178
+ #
179
+ # @return [RunnerState]
180
+ def monitor
181
+ RunnerState.new(
182
+ running: running?,
183
+ stop_requested: @stopped,
184
+ connected: connected?,
185
+ last_received_lsn: current_lsn_string(@stream&.latest_lsn || @resume_lsn),
186
+ last_feedback_lsn: current_lsn_string(@stream&.acked_lsn || @acked_lsn),
187
+ last_keepalive_at: @stream&.last_keepalive_at,
188
+ last_error: @last_error&.message,
189
+ reconnect_attempts: @reconnect_attempts
190
+ )
191
+ end
192
+
193
+ private
194
+
195
+ def setup_connection(connection)
196
+ if configuration.auto_create_slot && !@slot_created
197
+ connection.create_replication_slot
198
+ @slot_created = true
199
+ end
200
+
201
+ connection.start_replication
202
+ end
203
+
204
+ def run_stream_cycle(configuration, &block)
205
+ connection = Connection.open(configuration)
206
+ setup_connection(connection)
207
+ @stream = Stream.new(connection:, configuration:, acked_lsn: @acked_lsn)
208
+ @stream.start(&block)
209
+ :done
210
+ rescue ConnectionError => e
211
+ @last_error = e
212
+ raise if @stopped || @stream.nil?
213
+
214
+ @resume_lsn = @stream.latest_lsn || @resume_lsn
215
+ @acked_lsn = @stream.acked_lsn || @acked_lsn
216
+ :retry
217
+ ensure
218
+ @stream = nil
219
+ connection&.close
220
+ end
221
+
222
+ def configuration_for_resume
223
+ return configuration if @resume_lsn.nil?
224
+
225
+ Configuration.new(
226
+ database_url: configuration.database_url,
227
+ slot_name: configuration.slot_name,
228
+ publication_names: configuration.publication_names,
229
+ start_lsn: @resume_lsn,
230
+ proto_version: configuration.proto_version,
231
+ binary: configuration.binary,
232
+ messages: configuration.messages,
233
+ auto_create_slot: configuration.auto_create_slot,
234
+ temporary_slot: configuration.temporary_slot,
235
+ feedback_interval: configuration.feedback_interval
236
+ )
237
+ end
238
+
239
+ def normalize_lsn_value(value)
240
+ value.is_a?(String) ? LSN.parse(value) : LSN.parse(LSN.format(value))
241
+ end
242
+
243
+ def current_lsn_string(value)
244
+ return nil if value.nil?
245
+
246
+ value.is_a?(String) ? value : LSN.format(value)
247
+ end
248
+
249
+ def reconnect_backoff_for(attempts)
250
+ DEFAULT_RECONNECT_BACKOFF * attempts
251
+ end
252
+
253
+ def sleep(duration)
254
+ Kernel.sleep(duration)
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgoutput
4
+ module Client
5
+ RunnerStateData = Data.define(
6
+ :running,
7
+ :stop_requested,
8
+ :connected,
9
+ :last_received_lsn,
10
+ :last_feedback_lsn,
11
+ :last_keepalive_at,
12
+ :last_error,
13
+ :reconnect_attempts
14
+ )
15
+
16
+ # Immutable operational snapshot for a {Runner}.
17
+ #
18
+ # `RunnerState` is intentionally small and transport-focused. It exposes
19
+ # connection, feedback, keepalive, and retry state without claiming anything
20
+ # about downstream processing, sink delivery, or business-level consumption.
21
+ #
22
+ # @attr_reader running [Boolean] whether the runner is inside its streaming loop
23
+ # @attr_reader stop_requested [Boolean] whether graceful stop was requested
24
+ # @attr_reader connected [Boolean] whether an active replication stream exists
25
+ # @attr_reader last_received_lsn [String, nil] latest WAL position received from PostgreSQL
26
+ # @attr_reader last_feedback_lsn [String, nil] latest downstream-acknowledged WAL position
27
+ # used for flushed/applied feedback
28
+ # @attr_reader last_keepalive_at [Time, nil] last time a primary keepalive was observed
29
+ # @attr_reader last_error [String, nil] last transport error message
30
+ # @attr_reader reconnect_attempts [Integer] reconnect attempts used by the current/last run
31
+ # @api public
32
+ class RunnerState < RunnerStateData
33
+ # Whether the runner currently has no active stream.
34
+ #
35
+ # @return [Boolean]
36
+ def stopped?
37
+ !running
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgoutput
4
+ module Client
5
+ # Logical replication stream loop.
6
+ #
7
+ # `Stream` consumes PostgreSQL CopyData payloads from a {Connection}. It
8
+ # understands the two replication envelope message types used by PostgreSQL's
9
+ # streaming replication protocol:
10
+ #
11
+ # * `w` — XLogData, containing logical decoding plugin payload bytes.
12
+ # * `k` — primary keepalive, optionally requesting immediate feedback.
13
+ #
14
+ # The stream yields only XLogData plugin payloads. Keepalive messages are
15
+ # handled internally by updating the latest known WAL position and sending
16
+ # standby feedback when requested.
17
+ #
18
+ # Feedback separates receipt from downstream acknowledgment. Received LSN
19
+ # follows the latest WAL position seen from PostgreSQL. Flushed/applied LSN
20
+ # follows {#ack}, allowing downstream consumers to decide when a WAL position
21
+ # is safe to report as durable.
22
+ #
23
+ # @api private
24
+ class Stream
25
+ # Latest WAL position observed from XLogData or keepalive messages.
26
+ #
27
+ # @return [Integer]
28
+ attr_reader :latest_lsn
29
+
30
+ # Latest downstream-acknowledged WAL position used as flushed/applied LSN
31
+ # in standby feedback.
32
+ #
33
+ # @return [Integer]
34
+ attr_reader :acked_lsn
35
+
36
+ # Last time a primary keepalive was observed.
37
+ #
38
+ # @return [Time, nil]
39
+ attr_reader :last_keepalive_at
40
+
41
+ # Build a stream loop.
42
+ #
43
+ # @param connection [Connection] replication connection
44
+ # @param configuration [Configuration] stream configuration
45
+ # @param acked_lsn [String, Integer, nil] initial downstream-acknowledged
46
+ # WAL position
47
+ # @return [void]
48
+ def initialize(connection:, configuration:, acked_lsn: nil)
49
+ @connection = connection
50
+ @configuration = configuration
51
+ @latest_lsn = LSN.parse(configuration.start_lsn_string)
52
+ @acked_lsn = acked_lsn ? normalize_lsn_value(acked_lsn) : @latest_lsn
53
+ @last_feedback_at = monotonic_time
54
+ @last_keepalive_at = nil
55
+ @running = false
56
+ @stop_requested = false
57
+ end
58
+
59
+ # Start the stream loop.
60
+ #
61
+ # The method blocks while the stream is running. For every XLogData
62
+ # envelope, it yields the raw pgoutput payload and the parsed envelope
63
+ # metadata. When no CopyData payload is currently available, the loop
64
+ # pauses briefly before checking again.
65
+ #
66
+ # @yield [payload, metadata] called for each XLogData payload
67
+ # @yieldparam payload [String] frozen raw pgoutput payload bytes
68
+ # @yieldparam metadata [XLogData] parsed WAL envelope metadata
69
+ # @return [void]
70
+ # @raise [ArgumentError] if no block is provided
71
+ # @raise [ProtocolError] if an unknown or malformed replication message is
72
+ # received
73
+ # @raise [ConnectionError] if standby feedback cannot be sent
74
+ def start(&block)
75
+ raise ArgumentError, "block required" unless block_given?
76
+
77
+ @running = true
78
+ while @running
79
+ copy_data = @connection.get_copy_data
80
+ if copy_data.nil?
81
+ sleep 0.01
82
+ next
83
+ end
84
+
85
+ process_copy_data(copy_data, &block)
86
+ send_periodic_feedback
87
+ end
88
+ ensure
89
+ send_feedback(reply_requested: false) if @stop_requested
90
+ @running = false
91
+ end
92
+
93
+ # Stop the stream loop after the current iteration.
94
+ #
95
+ # @return [void]
96
+ def stop
97
+ @stop_requested = true
98
+ @running = false
99
+ nil
100
+ end
101
+
102
+ # Whether the stream loop is active.
103
+ #
104
+ # @return [Boolean]
105
+ def running?
106
+ @running
107
+ end
108
+
109
+ # Mark a WAL position as durably handled by downstream code.
110
+ #
111
+ # The stream never decides sink durability on its own. Downstream code may
112
+ # call this after checkpointing, enqueueing, or otherwise making progress
113
+ # durable. Feedback sent after this call reports the acknowledged LSN as
114
+ # both flushed and applied.
115
+ #
116
+ # @param lsn [String, Integer] WAL position acknowledged by downstream code
117
+ # @return [Integer] normalized acknowledged WAL position
118
+ def ack(lsn)
119
+ parsed = normalize_lsn_value(lsn)
120
+ @acked_lsn = [@acked_lsn, parsed].max
121
+ end
122
+
123
+ private
124
+
125
+ def process_copy_data(copy_data)
126
+ case copy_data.getbyte(0)
127
+ when "w".ord
128
+ xlog = XLogData.parse(copy_data)
129
+ @latest_lsn = xlog.wal_end
130
+ yield xlog.payload, xlog
131
+ when "k".ord
132
+ keepalive = Keepalive.parse(copy_data)
133
+ @latest_lsn = [@latest_lsn, keepalive.wal_end].max
134
+ @last_keepalive_at = Time.now.utc
135
+ send_feedback(reply_requested: true) if keepalive.reply_requested
136
+ else
137
+ raise ProtocolError, "unknown CopyData replication message: #{copy_data.getbyte(0).inspect}"
138
+ end
139
+ end
140
+
141
+ def send_periodic_feedback
142
+ return if monotonic_time - @last_feedback_at < @configuration.feedback_interval
143
+
144
+ send_feedback(reply_requested: false)
145
+ end
146
+
147
+ def send_feedback(reply_requested:)
148
+ feedback = Feedback.now(received_lsn: @latest_lsn, flushed_lsn: @acked_lsn, applied_lsn: @acked_lsn,
149
+ reply_requested:)
150
+ @connection.put_copy_data(feedback.to_copy_data)
151
+ @last_feedback_at = monotonic_time
152
+ end
153
+
154
+ def normalize_lsn_value(value)
155
+ value.is_a?(String) ? LSN.parse(value) : LSN.parse(LSN.format(value))
156
+ end
157
+
158
+ def monotonic_time
159
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Pgoutput
4
4
  module Client
5
- VERSION = "0.0.0"
5
+ # Current pgoutput-client gem version.
6
+ #
7
+ # @return [String]
8
+ VERSION = "0.2.0"
6
9
  end
7
10
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgoutput
4
+ module Client
5
+ ReplicationXLogData = Data.define(:wal_start, :wal_end, :server_clock, :payload)
6
+
7
+ # Immutable XLogData replication envelope.
8
+ #
9
+ # PostgreSQL wraps logical decoding plugin output in an XLogData CopyData
10
+ # payload while streaming replication is active. The payload layout is:
11
+ #
12
+ # ```text
13
+ # Byte 0 : message tag `w`
14
+ # Bytes 1-8 : WAL start position, unsigned 64-bit big-endian
15
+ # Bytes 9-16 : WAL end position, unsigned 64-bit big-endian
16
+ # Bytes 17-24 : server clock, PostgreSQL timestamp format
17
+ # Bytes 25.. : logical decoding plugin payload
18
+ # ```
19
+ #
20
+ # Instances are created through {.parse} and made shareable so they can cross
21
+ # Ractor boundaries with their frozen payload bytes.
22
+ #
23
+ # @attr_reader wal_start [Integer] WAL position where this message begins
24
+ # @attr_reader wal_end [Integer] WAL position after this message
25
+ # @attr_reader server_clock [Integer] PostgreSQL server timestamp in
26
+ # microseconds since 2000-01-01 UTC
27
+ # @attr_reader payload [String] frozen raw logical decoding plugin payload
28
+ class XLogData < ReplicationXLogData
29
+ # Parse an XLogData CopyData payload.
30
+ #
31
+ # @param bytes [String] raw CopyData payload beginning with `w`
32
+ # @return [XLogData] immutable parsed envelope
33
+ # @raise [ProtocolError] if the payload is empty, has the wrong message
34
+ # tag, or is too short to contain the required fields
35
+ def self.parse(bytes)
36
+ binary = bytes.b
37
+ raise ProtocolError, "empty CopyData payload" if binary.empty?
38
+ raise ProtocolError, "expected XLogData message" unless binary.getbyte(0) == "w".ord
39
+ raise ProtocolError, "truncated XLogData message" if binary.bytesize < 25
40
+
41
+ wal_start = unpack_u64(binary, 1)
42
+ wal_end = unpack_u64(binary, 9)
43
+ server_clock = unpack_u64(binary, 17)
44
+ payload = binary.byteslice(25..)&.freeze || "".b.freeze
45
+
46
+ Ractor.make_shareable(new(wal_start, wal_end, server_clock, payload))
47
+ end
48
+
49
+ # Starting WAL position formatted as a PostgreSQL LSN string.
50
+ #
51
+ # @return [String]
52
+ def wal_start_lsn = LSN.format(wal_start)
53
+
54
+ # Ending WAL position formatted as a PostgreSQL LSN string.
55
+ #
56
+ # @return [String]
57
+ def wal_end_lsn = LSN.format(wal_end)
58
+
59
+ # @param binary [String]
60
+ # @param offset [Integer]
61
+ # @return [Integer]
62
+ def self.unpack_u64(binary, offset)
63
+ value = binary.byteslice(offset, 8)&.unpack1("Q>")
64
+ raise ProtocolError, "failed to unpack uint64" unless value.is_a?(Integer)
65
+
66
+ value
67
+ end
68
+ private_class_method :unpack_u64
69
+ end
70
+ end
71
+ end
@@ -1,10 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "client/version"
4
+ require_relative "client/errors"
5
+ require_relative "client/configuration"
6
+ require_relative "client/lsn"
7
+ require_relative "client/xlog_data"
8
+ require_relative "client/keepalive"
9
+ require_relative "client/feedback"
10
+ require_relative "client/state"
11
+ require_relative "client/commands"
12
+ require_relative "client/connection"
13
+ require_relative "client/stream"
14
+ require_relative "client/runner"
4
15
 
5
16
  module Pgoutput
17
+ # Namespace for PostgreSQL logical replication transport support.
18
+ #
19
+ # `Pgoutput::Client` is the replication transport layer of the CDC Ecosystem.
20
+ # It is responsible for connecting to PostgreSQL in replication mode, creating
21
+ # or consuming replication slots, issuing `START_REPLICATION`, reading CopyData
22
+ # messages, and sending standby feedback.
23
+ #
24
+ # This namespace intentionally does not parse pgoutput plugin payloads into
25
+ # table-level changes. Raw plugin bytes are yielded to downstream protocol and
26
+ # type layers such as `pgoutput-parser` and `pgoutput-decoder`.
27
+ #
28
+ # @api public
6
29
  module Client
7
- class Error < StandardError; end
8
- # Your code goes here...
9
30
  end
10
31
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Main convenience require for pgoutput-client.
4
+ #
5
+ # Requiring this file loads the public `Pgoutput::Client` namespace and its
6
+ # transport-layer classes.
7
+ #
8
+ # @example
9
+ # require "pgoutput_client"
10
+ #
11
+ # @see Pgoutput::Client
12
+ require_relative "pgoutput/client"
data/sig/pg.rbs ADDED
@@ -0,0 +1,12 @@
1
+ module PG
2
+ def self.connect: (String conninfo, ?replication: String) -> Connection
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class Connection
8
+ def exec: (String sql) -> untyped
9
+ def close: () -> void
10
+ def finished?: () -> bool
11
+ end
12
+ end
@@ -0,0 +1,42 @@
1
+ module Pgoutput
2
+ module Client
3
+ # SQL command builders for PostgreSQL replication-mode commands.
4
+ #
5
+ # PostgreSQL replication commands are issued on a connection opened with the
6
+ # replication parameter enabled. The methods in this module render the small
7
+ # command subset needed by `pgoutput-client` and rely on {Configuration} to
8
+ # validate identifier-like values before interpolation.
9
+ #
10
+ # @api private
11
+ module Commands
12
+ # Render a `CREATE_REPLICATION_SLOT` command.
13
+ #
14
+ # Temporary slots are requested only when
15
+ # {Configuration#temporary_slot} is true.
16
+ #
17
+ # @example Permanent slot
18
+ # Commands.create_replication_slot(config)
19
+ # # => "CREATE_REPLICATION_SLOT cdc_slot LOGICAL pgoutput"
20
+ #
21
+ # @param configuration [Configuration] replication configuration
22
+ # @return [String] SQL command suitable for `PG::Connection#exec`
23
+ def self?.create_replication_slot: (untyped configuration) -> ::String
24
+
25
+ # Render a `DROP_REPLICATION_SLOT` command.
26
+ #
27
+ # @param configuration [Configuration] replication configuration
28
+ # @return [String] SQL command suitable for `PG::Connection#exec`
29
+ def self?.drop_replication_slot: (untyped configuration) -> ::String
30
+
31
+ # Render a `START_REPLICATION SLOT ... LOGICAL ...` command.
32
+ #
33
+ # The command includes the pgoutput options required by PostgreSQL:
34
+ # `proto_version` and `publication_names`. Optional pgoutput switches such
35
+ # as `binary` and `messages` are emitted only when enabled.
36
+ #
37
+ # @param configuration [Configuration] replication configuration
38
+ # @return [String] SQL command suitable for `PG::Connection#exec`
39
+ def self?.start_replication: (untyped configuration) -> ::String
40
+ end
41
+ end
42
+ end