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.
@@ -0,0 +1,71 @@
1
+ module Pgoutput
2
+ module Client
3
+ class FeedbackData < Data
4
+ attr_reader received_lsn: Integer
5
+ attr_reader flushed_lsn: Integer
6
+ attr_reader applied_lsn: Integer
7
+ attr_reader client_clock: Integer
8
+ attr_reader reply_requested: bool
9
+
10
+ def self.new: (
11
+ Integer received_lsn,
12
+ Integer flushed_lsn,
13
+ Integer applied_lsn,
14
+ Integer client_clock,
15
+ bool reply_requested
16
+ ) -> instance
17
+ end
18
+
19
+ # Standby status feedback message builder.
20
+ #
21
+ # Logical replication clients periodically send standby status updates to
22
+ # tell PostgreSQL which WAL location has been received, flushed, and applied.
23
+ # `Feedback` models that update and can serialize itself into the CopyData
24
+ # payload expected by the replication protocol.
25
+ #
26
+ # @attr_reader received_lsn [Integer] latest WAL location received by the client
27
+ # @attr_reader flushed_lsn [Integer] latest WAL location flushed by the client
28
+ # @attr_reader applied_lsn [Integer] latest WAL location applied by the client
29
+ # @attr_reader client_clock [Integer] PostgreSQL timestamp in microseconds since 2000-01-01 UTC
30
+ # @attr_reader reply_requested [Boolean] whether this feedback is responding to an immediate-reply request
31
+ class Feedback < FeedbackData
32
+ def self.build: (
33
+ Integer received_lsn,
34
+ Integer flushed_lsn,
35
+ Integer applied_lsn,
36
+ ?reply_requested: bool
37
+ ) -> Feedback
38
+
39
+ # Build feedback using the current wall-clock time.
40
+ #
41
+ # By default, flushed and applied LSNs follow the received LSN. Callers can
42
+ # pass lower values if they need to distinguish receipt from durable flush
43
+ # or application progress.
44
+ #
45
+ # @param received_lsn [Integer] latest WAL location received by the client
46
+ # @param flushed_lsn [Integer] latest WAL location flushed by the client
47
+ # @param applied_lsn [Integer] latest WAL location applied by the client
48
+ # @param reply_requested [Boolean] whether this is an immediate reply
49
+ # @return [Feedback] immutable feedback value
50
+ def self.now: (received_lsn: untyped, ?flushed_lsn: untyped, ?applied_lsn: untyped, ?reply_requested: bool) -> untyped
51
+
52
+ # Build a protocol CopyData payload for standby status update.
53
+ #
54
+ # The payload begins with the standby status update tag `r`, followed by
55
+ # three unsigned 64-bit LSN fields, the PostgreSQL timestamp, and a
56
+ # one-byte reply-requested flag.
57
+ #
58
+ # @return [String] frozen binary CopyData payload
59
+ def to_copy_data: () -> untyped
60
+
61
+ # Current PostgreSQL protocol timestamp.
62
+ #
63
+ # PostgreSQL timestamps in replication messages are expressed as
64
+ # microseconds since 2000-01-01 00:00:00 UTC, not Unix epoch
65
+ # microseconds.
66
+ #
67
+ # @return [Integer] microseconds since 2000-01-01 UTC
68
+ def self.current_pg_time: () -> untyped
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,55 @@
1
+ module Pgoutput
2
+ module Client
3
+ class KeepaliveData < Data
4
+ attr_reader wal_end: Integer
5
+ attr_reader server_clock: Integer
6
+ attr_reader reply_requested: bool
7
+
8
+ def self.new: (
9
+ Integer wal_end,
10
+ Integer server_clock,
11
+ bool reply_requested
12
+ ) -> instance
13
+ end
14
+
15
+ # Immutable primary keepalive replication message.
16
+ #
17
+ # PostgreSQL sends keepalive CopyData payloads while a replication stream is
18
+ # active. The payload layout is:
19
+ #
20
+ # ```text
21
+ # Byte 0 : message tag `k`
22
+ # Bytes 1-8 : current server WAL end, unsigned 64-bit big-endian
23
+ # Bytes 9-16 : server clock, PostgreSQL timestamp format
24
+ # Byte 17 : reply-requested flag, 1 for immediate feedback
25
+ # ```
26
+ #
27
+ # The stream layer uses this message to advance its known WAL position and to
28
+ # decide whether to send standby status feedback immediately.
29
+ #
30
+ # @attr_reader wal_end [Integer] latest server WAL position
31
+ # @attr_reader server_clock [Integer] PostgreSQL server timestamp in
32
+ # microseconds since 2000-01-01 UTC
33
+ # @attr_reader reply_requested [Boolean] whether PostgreSQL requested
34
+ # immediate feedback
35
+ class Keepalive < KeepaliveData
36
+ # Parse a keepalive CopyData payload.
37
+ #
38
+ # @param bytes [String] raw CopyData payload beginning with `k`
39
+ # @return [Keepalive] immutable parsed keepalive message
40
+ # @raise [ProtocolError] if the payload is empty, has the wrong message
41
+ # tag, or is too short to contain the required fields
42
+ def self.parse: (untyped bytes) -> untyped
43
+
44
+ # Latest server WAL position formatted as a PostgreSQL LSN string.
45
+ #
46
+ # @return [String]
47
+ def wal_end_lsn: () -> untyped
48
+
49
+ # @param binary [String]
50
+ # @param offset [Integer]
51
+ # @return [Integer]
52
+ def self.unpack_u64: (untyped binary, untyped offset) -> untyped
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,36 @@
1
+ module Pgoutput
2
+ module Client
3
+ # PostgreSQL Log Sequence Number conversion helpers.
4
+ #
5
+ # PostgreSQL represents LSNs as two hexadecimal 32-bit halves separated by a
6
+ # slash, such as `0/16B6C50`. The replication protocol transmits the same WAL
7
+ # position as an unsigned 64-bit integer. This module converts between those
8
+ # two representations.
9
+ #
10
+ # @example Parse a textual LSN
11
+ # Pgoutput::Client::LSN.parse("0/10")
12
+ # # => 16
13
+ #
14
+ # @example Format an integer WAL position
15
+ # Pgoutput::Client::LSN.format(16)
16
+ # # => "0/10"
17
+ #
18
+ # @api public
19
+ module LSN
20
+ # Parse a PostgreSQL LSN string into an integer WAL position.
21
+ #
22
+ # @param value [#to_s] LSN string in `HEX/HEX` form
23
+ # @return [Integer] unsigned 64-bit WAL position
24
+ # @raise [ArgumentError] if the value is not a valid LSN string
25
+ def self?.parse: (untyped value) -> untyped
26
+
27
+ # Format an integer WAL position as a PostgreSQL LSN string.
28
+ #
29
+ # @param value [#to_int, #to_s] non-negative integer WAL position
30
+ # @return [String] LSN string in uppercase hexadecimal `HEX/HEX` form
31
+ # @raise [ArgumentError] if the value is negative or cannot be coerced to
32
+ # an integer
33
+ def self?.format: (untyped value) -> ::String
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,68 @@
1
+ module Pgoutput
2
+ module Client
3
+ # Logical replication stream loop.
4
+ #
5
+ # `Stream` consumes PostgreSQL CopyData payloads from a {Connection}. It
6
+ # understands the two replication envelope message types used by PostgreSQL's
7
+ # streaming replication protocol:
8
+ #
9
+ # * `w` — XLogData, containing logical decoding plugin payload bytes.
10
+ # * `k` — primary keepalive, optionally requesting immediate feedback.
11
+ #
12
+ # The stream yields only XLogData plugin payloads. Keepalive messages are
13
+ # handled internally by updating the latest known WAL position and sending
14
+ # standby feedback when requested.
15
+ #
16
+ # @api private
17
+ class Stream
18
+ @connection: untyped
19
+
20
+ @configuration: untyped
21
+
22
+ @latest_lsn: untyped
23
+
24
+ @last_feedback_at: untyped
25
+
26
+ @running: untyped
27
+
28
+ # Build a stream loop.
29
+ #
30
+ # @param connection [Connection] replication connection
31
+ # @param configuration [Configuration] stream configuration
32
+ # @return [void]
33
+ def initialize: (connection: untyped, configuration: untyped) -> void
34
+
35
+ # Start the stream loop.
36
+ #
37
+ # The method blocks while the stream is running. For every XLogData
38
+ # envelope, it yields the raw pgoutput payload and the parsed envelope
39
+ # metadata. When no CopyData payload is currently available, the loop
40
+ # continues and checks again.
41
+ #
42
+ # @yield [payload, metadata] called for each XLogData payload
43
+ # @yieldparam payload [String] frozen raw pgoutput payload bytes
44
+ # @yieldparam metadata [XLogData] parsed WAL envelope metadata
45
+ # @return [void]
46
+ # @raise [ArgumentError] if no block is provided
47
+ # @raise [ProtocolError] if an unknown or malformed replication message is
48
+ # received
49
+ # @raise [ConnectionError] if standby feedback cannot be sent
50
+ def start: () ?{ (?) -> untyped } -> untyped
51
+
52
+ # Stop the stream loop after the current iteration.
53
+ #
54
+ # @return [void]
55
+ def stop: () -> untyped
56
+
57
+ private
58
+
59
+ def process_copy_data: (untyped copy_data) { (untyped, untyped) -> untyped } -> untyped
60
+
61
+ def send_periodic_feedback: () -> (nil | untyped)
62
+
63
+ def send_feedback: (reply_requested: untyped) -> untyped
64
+
65
+ def monotonic_time: () -> untyped
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,8 @@
1
+ module Pgoutput
2
+ module Client
3
+ # Current pgoutput-client gem version.
4
+ #
5
+ # @return [String]
6
+ VERSION: "0.1.0"
7
+ end
8
+ end
@@ -0,0 +1,63 @@
1
+ module Pgoutput
2
+ module Client
3
+ class ReplicationXLogData < Data
4
+ attr_reader wal_start: Integer
5
+ attr_reader wal_end: Integer
6
+ attr_reader server_clock: Integer
7
+ attr_reader payload: String
8
+
9
+ def self.new: (
10
+ Integer wal_start,
11
+ Integer wal_end,
12
+ Integer server_clock,
13
+ String payload
14
+ ) -> instance
15
+ end
16
+
17
+ # Immutable XLogData replication envelope.
18
+ #
19
+ # PostgreSQL wraps logical decoding plugin output in an XLogData CopyData
20
+ # payload while streaming replication is active. The payload layout is:
21
+ #
22
+ # ```text
23
+ # Byte 0 : message tag `w`
24
+ # Bytes 1-8 : WAL start position, unsigned 64-bit big-endian
25
+ # Bytes 9-16 : WAL end position, unsigned 64-bit big-endian
26
+ # Bytes 17-24 : server clock, PostgreSQL timestamp format
27
+ # Bytes 25.. : logical decoding plugin payload
28
+ # ```
29
+ #
30
+ # Instances are created through {.parse} and made shareable so they can cross
31
+ # Ractor boundaries with their frozen payload bytes.
32
+ #
33
+ # @attr_reader wal_start [Integer] WAL position where this message begins
34
+ # @attr_reader wal_end [Integer] WAL position after this message
35
+ # @attr_reader server_clock [Integer] PostgreSQL server timestamp in
36
+ # microseconds since 2000-01-01 UTC
37
+ # @attr_reader payload [String] frozen raw logical decoding plugin payload
38
+ class XLogData < ReplicationXLogData
39
+ # Parse an XLogData CopyData payload.
40
+ #
41
+ # @param bytes [String] raw CopyData payload beginning with `w`
42
+ # @return [XLogData] immutable parsed envelope
43
+ # @raise [ProtocolError] if the payload is empty, has the wrong message
44
+ # tag, or is too short to contain the required fields
45
+ def self.parse: (untyped bytes) -> untyped
46
+
47
+ # Starting WAL position formatted as a PostgreSQL LSN string.
48
+ #
49
+ # @return [String]
50
+ def wal_start_lsn: () -> untyped
51
+
52
+ # Ending WAL position formatted as a PostgreSQL LSN string.
53
+ #
54
+ # @return [String]
55
+ def wal_end_lsn: () -> untyped
56
+
57
+ # @param binary [String]
58
+ # @param offset [Integer]
59
+ # @return [Integer]
60
+ def self.unpack_u64: (untyped binary, untyped offset) -> untyped
61
+ end
62
+ end
63
+ end
@@ -1,6 +1,97 @@
1
1
  module Pgoutput
2
+ # Namespace for PostgreSQL logical replication transport support.
3
+ #
4
+ # `Pgoutput::Client` is the replication transport layer of the CDC Ecosystem.
5
+ # It is responsible for connecting to PostgreSQL in replication mode, creating
6
+ # or consuming replication slots, issuing `START_REPLICATION`, reading CopyData
7
+ # messages, and sending standby feedback.
8
+ #
9
+ # This namespace intentionally does not parse pgoutput plugin payloads into
10
+ # table-level changes. Raw plugin bytes are yielded to downstream protocol and
11
+ # type layers such as `pgoutput-parser` and `pgoutput-decoder`.
12
+ #
13
+ # @api public
2
14
  module Client
3
- VERSION: String
4
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
15
+ # High-level logical replication client facade.
16
+ #
17
+ # `Runner` is the simplest public entry point for consumers that want to
18
+ # stream raw pgoutput payloads without manually managing a replication
19
+ # connection. It owns the connection lifecycle for one logical replication
20
+ # stream:
21
+ #
22
+ # 1. Build an immutable {Configuration} from keyword arguments.
23
+ # 2. Open a PostgreSQL replication connection.
24
+ # 3. Optionally create the configured replication slot.
25
+ # 4. Start logical replication.
26
+ # 5. Yield raw pgoutput payload bytes and {XLogData} metadata.
27
+ # 6. Close the connection when streaming exits.
28
+ #
29
+ # @example Stream raw pgoutput messages
30
+ # runner = Pgoutput::Client::Runner.new(
31
+ # database_url: "postgres://localhost/app",
32
+ # slot_name: "cdc_slot",
33
+ # publication_names: ["app_publication"]
34
+ # )
35
+ #
36
+ # runner.start do |payload, metadata|
37
+ # puts "received #{payload.bytesize} bytes at #{metadata.wal_end_lsn}"
38
+ # end
39
+ #
40
+ # @see Configuration
41
+ # @see Connection
42
+ # @see Stream
43
+ # @api public
44
+ class Runner
45
+ @configuration: untyped
46
+
47
+ @stopped: untyped
48
+
49
+ # Configuration used by this runner.
50
+ #
51
+ # @return [Configuration]
52
+ attr_reader configuration: untyped
53
+
54
+ # Build a runner from configuration keyword arguments.
55
+ #
56
+ # The accepted keywords are the same as {Configuration#initialize}. The
57
+ # resulting configuration object is immutable and reused for the lifetime
58
+ # of this runner.
59
+ #
60
+ # @param options [Hash{Symbol=>Object}] configuration options forwarded to
61
+ # {Configuration#initialize}
62
+ # @return [void]
63
+ # @raise [ConfigurationError] if the supplied configuration is invalid
64
+ def initialize: (**untyped options) -> void
65
+
66
+ # Start streaming raw pgoutput payloads.
67
+ #
68
+ # This method blocks until the stream stops or the underlying connection
69
+ # raises an error. The yielded payload is the raw logical decoding plugin
70
+ # payload contained inside PostgreSQL's XLogData envelope; callers normally
71
+ # pass this payload to a pgoutput parser.
72
+ #
73
+ # @yield [payload, metadata] called once for each XLogData payload
74
+ # @yieldparam payload [String] frozen raw pgoutput payload bytes
75
+ # @yieldparam metadata [XLogData] WAL envelope metadata for the payload
76
+ # @return [void]
77
+ # @raise [ArgumentError] if no block is provided
78
+ # @raise [ConnectionError] if a PostgreSQL connection or command fails
79
+ # @raise [ProtocolError] if an invalid replication message is received
80
+ def start: () ?{ (?) -> untyped } -> untyped
81
+
82
+ # Request graceful stop.
83
+ #
84
+ # This method records the caller's intent to stop. The current
85
+ # implementation does not yet interrupt an active {Stream}; it exists as
86
+ # part of the public lifecycle API and may be wired into cooperative stream
87
+ # shutdown in a future release.
88
+ #
89
+ # @return [void]
90
+ def stop: () -> nil
91
+
92
+ private
93
+
94
+ def setup_connection: (untyped connection) -> untyped
95
+ end
5
96
  end
6
97
  end
metadata CHANGED
@@ -1,15 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgoutput-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
12
- description: Write a longer description or delete this line.
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: pg
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.6'
26
+ description: Transport-only PostgreSQL logical replication client for receiving pgoutput
27
+ CopyData payloads.
13
28
  email:
14
29
  - kenneth.c.demanawa@gmail.com
15
30
  executables: []
@@ -17,20 +32,41 @@ extensions: []
17
32
  extra_rdoc_files: []
18
33
  files:
19
34
  - CHANGELOG.md
20
- - CODE_OF_CONDUCT.md
21
35
  - LICENSE.txt
22
36
  - README.md
23
- - Rakefile
24
37
  - lib/pgoutput/client.rb
38
+ - lib/pgoutput/client/commands.rb
39
+ - lib/pgoutput/client/configuration.rb
40
+ - lib/pgoutput/client/connection.rb
41
+ - lib/pgoutput/client/errors.rb
42
+ - lib/pgoutput/client/feedback.rb
43
+ - lib/pgoutput/client/keepalive.rb
44
+ - lib/pgoutput/client/lsn.rb
45
+ - lib/pgoutput/client/stream.rb
25
46
  - lib/pgoutput/client/version.rb
47
+ - lib/pgoutput/client/xlog_data.rb
48
+ - lib/pgoutput_client.rb
49
+ - sig/pg.rbs
26
50
  - sig/pgoutput/client.rbs
27
- homepage: https://github.com/kanutocd/pgoutput-client
51
+ - sig/pgoutput/client/commands.rbs
52
+ - sig/pgoutput/client/configuration.rbs
53
+ - sig/pgoutput/client/connection.rbs
54
+ - sig/pgoutput/client/errors.rbs
55
+ - sig/pgoutput/client/feedback.rbs
56
+ - sig/pgoutput/client/keepalive.rbs
57
+ - sig/pgoutput/client/lsn.rbs
58
+ - sig/pgoutput/client/stream.rbs
59
+ - sig/pgoutput/client/version.rbs
60
+ - sig/pgoutput/client/xlog_data.rbs
61
+ homepage: https://kanutocd.github.io/pgoutput-client/
28
62
  licenses:
29
63
  - MIT
30
64
  metadata:
31
- homepage_uri: https://github.com/kanutocd/pgoutput-client
65
+ homepage_uri: https://kanutocd.github.io/pgoutput-client/
32
66
  source_code_uri: https://github.com/kanutocd/pgoutput-client
33
67
  changelog_uri: https://github.com/kanutocd/pgoutput-client/blob/main/CHANGELOG.md
68
+ documentation_uri: https://kanutocd.github.io/pgoutput-client/
69
+ rubygems_mfa_required: 'true'
34
70
  rdoc_options: []
35
71
  require_paths:
36
72
  - lib
@@ -45,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
45
81
  - !ruby/object:Gem::Version
46
82
  version: '0'
47
83
  requirements: []
48
- rubygems_version: 4.0.10
84
+ rubygems_version: 3.6.9
49
85
  specification_version: 4
50
- summary: Write a short summary, because RubyGems requires one.
86
+ summary: PostgreSQL pgoutput logical replication transport client.
51
87
  test_files: []
data/CODE_OF_CONDUCT.md DELETED
@@ -1,10 +0,0 @@
1
- # Code of Conduct
2
-
3
- "pgoutput-client" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
-
5
- * Participants will be tolerant of opposing views.
6
- * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
- * When interpreting the words and actions of others, participants should always assume good intentions.
8
- * Behaviour which can be reasonably considered harassment will not be tolerated.
9
-
10
- If you have any concerns about behaviour within this project, please contact us at ["kenneth.c.demanawa@gmail.com"](mailto:"kenneth.c.demanawa@gmail.com").
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]