pgoutput-source-adapter 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +22 -4
- data/lib/pgoutput/source_adapter/cdc.rb +24 -1
- data/lib/pgoutput/source_adapter/replica_identity_resolver.rb +99 -0
- data/lib/pgoutput/source_adapter/version.rb +1 -1
- data/lib/pgoutput/source_adapter.rb +1 -0
- data/sig/pgoutput/source_adapter/cdc.rbs +7 -1
- data/sig/pgoutput/source_adapter/replica_identity_resolver.rbs +20 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96fa814d19258cf12c3f2d6398f72f21f1a2dfab59e89562b880f26fbb7bf1cd
|
|
4
|
+
data.tar.gz: 300f6a751391ee90ba84dfdbe743839e605fb6655aafc5057f9b87a1e25c3faf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 314c92d8ec4d3853056f4c69d99eefb6794aaefbc58a1b5f9e0b2de25c372cce1e8c4864210ade04400d23feb60fc47d68d535c1df901d7ef60a373bc4c735b8
|
|
7
|
+
data.tar.gz: 88c5f21bc01667b196a1b347e176cca6aa7caa655a058fa1550bdee3bcb3b88c511fd9371225f5a95fe585dac572a219b46a14a7135e956c97ac47fd8466e986
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added `ReplicaIdentityResolver` for schema-aware non-`id` and composite keys,
|
|
8
|
+
keyed by relation id, schema/table pair, or qualified relation name.
|
|
9
|
+
- Added default extraction from decoded relation key-column metadata when
|
|
10
|
+
events expose replica-identity columns or flagged relation columns.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Preserved complete composite and non-`id` old-key tuples for normalized
|
|
15
|
+
UPDATE and DELETE events.
|
|
16
|
+
- Fail closed when configured replica-key columns are absent instead of
|
|
17
|
+
emitting an incomplete `ChangeEvent#primary_key`.
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
- Documented relation metadata, schema-aware resolver configuration, and the
|
|
22
|
+
legacy `id` compatibility fallback.
|
|
23
|
+
|
|
3
24
|
## [0.2.0] - 2026-07-17
|
|
4
25
|
|
|
5
26
|
### Added
|
data/README.md
CHANGED
|
@@ -107,18 +107,35 @@ normalization path for finite batches.
|
|
|
107
107
|
|
|
108
108
|
## Primary keys
|
|
109
109
|
|
|
110
|
-
For update and delete events, pgoutput may provide an old-key tuple. When it
|
|
110
|
+
For update and delete events, pgoutput may provide an old-key tuple. When it
|
|
111
|
+
does, the complete tuple is used as `CDC::Core::ChangeEvent#primary_key`,
|
|
112
|
+
including composite and non-`id` replica keys.
|
|
111
113
|
|
|
112
|
-
For
|
|
114
|
+
For inserts and events without an old-key tuple, the default resolver uses
|
|
115
|
+
relation key-column metadata when the decoded event exposes
|
|
116
|
+
`replica_identity_columns`, `key_columns`, or a relation whose column flags mark
|
|
117
|
+
replica-key columns. The adapter extracts every declared column and raises
|
|
118
|
+
`Pgoutput::SourceAdapter::Error` rather than returning a partial key.
|
|
113
119
|
|
|
114
|
-
|
|
120
|
+
When decoded events do not carry relation key metadata, configure the reusable
|
|
121
|
+
schema-aware resolver. Relations may be keyed by relation id,
|
|
122
|
+
`[schema, table]`, or a qualified name:
|
|
115
123
|
|
|
116
124
|
```ruby
|
|
125
|
+
resolver = Pgoutput::SourceAdapter::ReplicaIdentityResolver.new(
|
|
126
|
+
["public", "memberships"] => ["tenant_id", "member_uuid"],
|
|
127
|
+
"public.accounts" => ["account_uuid"]
|
|
128
|
+
)
|
|
129
|
+
|
|
117
130
|
adapter = Pgoutput::SourceAdapter::Cdc.new(
|
|
118
|
-
primary_key_resolver:
|
|
131
|
+
primary_key_resolver: resolver
|
|
119
132
|
)
|
|
120
133
|
```
|
|
121
134
|
|
|
135
|
+
The legacy `id` / `"id"` inference remains as a compatibility fallback only
|
|
136
|
+
when neither relation metadata nor an explicit resolver is available. Custom
|
|
137
|
+
callables remain supported through `primary_key_resolver:`.
|
|
138
|
+
|
|
122
139
|
## Metadata
|
|
123
140
|
|
|
124
141
|
Each normalized event includes pgoutput metadata:
|
|
@@ -144,6 +161,7 @@ adapter = Pgoutput::SourceAdapter::Cdc.new(
|
|
|
144
161
|
```ruby
|
|
145
162
|
Pgoutput::SourceAdapter
|
|
146
163
|
Pgoutput::SourceAdapter::Cdc
|
|
164
|
+
Pgoutput::SourceAdapter::ReplicaIdentityResolver
|
|
147
165
|
```
|
|
148
166
|
|
|
149
167
|
A compatibility alias is also provided for the generated gem path:
|
|
@@ -274,7 +274,9 @@ module Pgoutput
|
|
|
274
274
|
[input, nil]
|
|
275
275
|
end
|
|
276
276
|
|
|
277
|
-
def default_primary_key(
|
|
277
|
+
def default_primary_key(event, values)
|
|
278
|
+
key_columns = replica_identity_columns_for(event)
|
|
279
|
+
return key_from_columns(event, values, key_columns) unless key_columns.nil?
|
|
278
280
|
return nil unless values.respond_to?(:key?)
|
|
279
281
|
|
|
280
282
|
if values.key?('id')
|
|
@@ -284,6 +286,27 @@ module Pgoutput
|
|
|
284
286
|
end
|
|
285
287
|
end
|
|
286
288
|
|
|
289
|
+
def replica_identity_columns_for(event)
|
|
290
|
+
return normalize_key_columns(event.replica_identity_columns) if event.respond_to?(:replica_identity_columns)
|
|
291
|
+
return normalize_key_columns(event.key_columns) if event.respond_to?(:key_columns)
|
|
292
|
+
return unless event.respond_to?(:relation) && event.relation.respond_to?(:columns)
|
|
293
|
+
|
|
294
|
+
normalize_key_columns(
|
|
295
|
+
event.relation.columns.select { |column| column.flags.allbits?(1) }.map(&:name)
|
|
296
|
+
)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def normalize_key_columns(columns)
|
|
300
|
+
column_list = Array(columns) # : Array[untyped]
|
|
301
|
+
column_list.map(&:to_s)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def key_from_columns(event, values, columns)
|
|
305
|
+
return nil if columns.empty?
|
|
306
|
+
|
|
307
|
+
ReplicaIdentityResolver.extract(event, values, columns)
|
|
308
|
+
end
|
|
309
|
+
|
|
287
310
|
def metadata_for(event)
|
|
288
311
|
metadata = {
|
|
289
312
|
'source' => SOURCE_NAME,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgoutput
|
|
4
|
+
module SourceAdapter
|
|
5
|
+
# Resolves schema-aware replica keys from decoded row values.
|
|
6
|
+
#
|
|
7
|
+
# Relations may be keyed by pgoutput relation id, `[schema, table]`, or a
|
|
8
|
+
# `"schema.table"` string. Each value is the ordered list of replica-key
|
|
9
|
+
# columns. Relation-id mappings take precedence because they identify the
|
|
10
|
+
# exact relation metadata seen by the replication stream.
|
|
11
|
+
#
|
|
12
|
+
# @example Resolve a composite key
|
|
13
|
+
# resolver = ReplicaIdentityResolver.new(
|
|
14
|
+
# ["public", "memberships"] => ["tenant_id", "member_uuid"]
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# @api public
|
|
18
|
+
class ReplicaIdentityResolver
|
|
19
|
+
# Normalized relation keys mapped to ordered replica-identity columns.
|
|
20
|
+
#
|
|
21
|
+
# @return [Hash]
|
|
22
|
+
attr_reader :relations
|
|
23
|
+
|
|
24
|
+
# Extract a complete replica key from an ordered column list.
|
|
25
|
+
#
|
|
26
|
+
# @param event [Object] decoded row event with schema/table metadata
|
|
27
|
+
# @param values [Hash, nil] decoded row values
|
|
28
|
+
# @param columns [Array<String>] ordered replica-key column names
|
|
29
|
+
# @return [Hash{String=>Object}] complete replica key
|
|
30
|
+
# @raise [Error] when values or any configured column are unavailable
|
|
31
|
+
def self.extract(event, values, columns)
|
|
32
|
+
unless values.respond_to?(:key?)
|
|
33
|
+
raise Error, "replica identity values are unavailable for #{qualified_name(event)}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
columns.to_h do |column|
|
|
37
|
+
[column, fetch_column(event, values, column)]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param relations [Hash{Integer, Array<String>, Array<String>=>Array<String>}]
|
|
42
|
+
# relation identity to ordered replica-key column names
|
|
43
|
+
# @return [void]
|
|
44
|
+
def initialize(relations)
|
|
45
|
+
@relations = relations.to_h.transform_values { |columns| normalize_columns(columns) }.freeze
|
|
46
|
+
freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Extract the configured replica key for a decoded row event.
|
|
50
|
+
#
|
|
51
|
+
# @param event [Object] decoded row event with relation/schema/table metadata
|
|
52
|
+
# @param values [Hash, nil] decoded row values
|
|
53
|
+
# @return [Hash{String=>Object}, nil] complete replica key, or `nil` when
|
|
54
|
+
# the relation is not configured
|
|
55
|
+
# @raise [Error] when a configured key column is absent from row values
|
|
56
|
+
def call(event, values)
|
|
57
|
+
columns = columns_for(event)
|
|
58
|
+
return nil unless columns
|
|
59
|
+
|
|
60
|
+
self.class.extract(event, values, columns)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.fetch_column(event, values, column)
|
|
64
|
+
return values[column] if values.key?(column)
|
|
65
|
+
|
|
66
|
+
symbol = column.to_sym
|
|
67
|
+
return values[symbol] if values.key?(symbol)
|
|
68
|
+
|
|
69
|
+
raise Error, "replica identity column #{column.inspect} is missing from #{qualified_name(event)} values"
|
|
70
|
+
end
|
|
71
|
+
private_class_method :fetch_column
|
|
72
|
+
|
|
73
|
+
def self.qualified_name(event)
|
|
74
|
+
[event.schema, event.table].compact.join('.')
|
|
75
|
+
end
|
|
76
|
+
private_class_method :qualified_name
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def normalize_columns(columns)
|
|
81
|
+
column_list = Array(columns) # : Array[untyped]
|
|
82
|
+
names = column_list.map(&:to_s)
|
|
83
|
+
raise ArgumentError, 'replica identity columns must not be empty' if names.empty?
|
|
84
|
+
raise ArgumentError, 'replica identity columns must be unique' unless names.uniq.length == names.length
|
|
85
|
+
|
|
86
|
+
names.freeze
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def columns_for(event)
|
|
90
|
+
relation_id = event.relation_id if event.respond_to?(:relation_id)
|
|
91
|
+
return relations[relation_id] if relation_id && relations.key?(relation_id)
|
|
92
|
+
|
|
93
|
+
schema = event.schema
|
|
94
|
+
table = event.table
|
|
95
|
+
relations[[schema.to_s, table.to_s]] || relations["#{schema}.#{table}"]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -80,7 +80,13 @@ module Pgoutput
|
|
|
80
80
|
|
|
81
81
|
def stream_input: (untyped input) -> [untyped, untyped]
|
|
82
82
|
|
|
83
|
-
def default_primary_key: (untyped
|
|
83
|
+
def default_primary_key: (untyped event, untyped values) -> (nil | untyped)
|
|
84
|
+
|
|
85
|
+
def replica_identity_columns_for: (untyped event) -> untyped
|
|
86
|
+
|
|
87
|
+
def normalize_key_columns: (untyped columns) -> Array[String]
|
|
88
|
+
|
|
89
|
+
def key_from_columns: (untyped event, untyped values, Array[String] columns) -> untyped
|
|
84
90
|
|
|
85
91
|
def metadata_for: (untyped event) -> untyped
|
|
86
92
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Pgoutput
|
|
2
|
+
module SourceAdapter
|
|
3
|
+
class ReplicaIdentityResolver
|
|
4
|
+
@relations: untyped
|
|
5
|
+
|
|
6
|
+
attr_reader relations: untyped
|
|
7
|
+
|
|
8
|
+
def self.extract: (untyped event, untyped values, Array[String] columns) -> Hash[String, untyped]
|
|
9
|
+
def initialize: (untyped relations) -> void
|
|
10
|
+
def call: (untyped event, untyped values) -> untyped
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def self.fetch_column: (untyped event, untyped values, String column) -> untyped
|
|
15
|
+
def self.qualified_name: (untyped event) -> String
|
|
16
|
+
def normalize_columns: (untyped columns) -> Array[String]
|
|
17
|
+
def columns_for: (untyped event) -> untyped
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgoutput-source-adapter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -64,11 +64,13 @@ files:
|
|
|
64
64
|
- lib/pgoutput/source/adapter.rb
|
|
65
65
|
- lib/pgoutput/source_adapter.rb
|
|
66
66
|
- lib/pgoutput/source_adapter/cdc.rb
|
|
67
|
+
- lib/pgoutput/source_adapter/replica_identity_resolver.rb
|
|
67
68
|
- lib/pgoutput/source_adapter/version.rb
|
|
68
69
|
- mise.toml
|
|
69
70
|
- sig/pgoutput/source/adapter.rbs
|
|
70
71
|
- sig/pgoutput/source_adapter.rbs
|
|
71
72
|
- sig/pgoutput/source_adapter/cdc.rbs
|
|
73
|
+
- sig/pgoutput/source_adapter/replica_identity_resolver.rbs
|
|
72
74
|
- sig/pgoutput/source_adapter/version.rbs
|
|
73
75
|
homepage: https://github.com/kanutocd/pgoutput-source-adapter
|
|
74
76
|
licenses:
|