cdc-core 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 +4 -4
- data/CHANGELOG.md +29 -1
- data/LICENSE.txt +2 -1
- data/README.md +145 -22
- data/lib/cdc/core/change_event.rb +116 -0
- data/lib/cdc/core/column_change.rb +55 -0
- data/lib/cdc/core/composite_processor.rb +71 -0
- data/lib/cdc/core/errors.rb +14 -0
- data/lib/cdc/core/event_metadata.rb +78 -0
- data/lib/cdc/core/filter.rb +77 -0
- data/lib/cdc/core/operation.rb +51 -0
- data/lib/cdc/core/pipeline.rb +66 -0
- data/lib/cdc/core/processor.rb +43 -0
- data/lib/cdc/core/processor_result.rb +63 -0
- data/lib/cdc/core/transaction_envelope.rb +58 -0
- data/lib/cdc/core/version.rb +3 -2
- data/lib/cdc/core.rb +20 -4
- data/lib/cdc_core.rb +7 -0
- data/sig/cdc/core/change_event.rbs +226 -0
- data/sig/cdc/core/column_change.rbs +56 -0
- data/sig/cdc/core/composite_processor.rbs +53 -0
- data/sig/cdc/core/errors.rbs +15 -0
- data/sig/cdc/core/event_metadata.rbs +51 -0
- data/sig/cdc/core/filter.rbs +68 -0
- data/sig/cdc/core/operation.rbs +44 -0
- data/sig/cdc/core/pipeline.rbs +55 -0
- data/sig/cdc/core/processor.rbs +35 -0
- data/sig/cdc/core/processor_result.rbs +81 -0
- data/sig/cdc/core/transaction_envelope.rbs +79 -0
- data/sig/cdc/core/version.rbs +6 -0
- data/sig/cdc/core.rbs +8 -3
- data/sig/cdc_core.rbs +0 -0
- metadata +34 -7
- data/CODE_OF_CONDUCT.md +0 -10
- data/Rakefile +0 -12
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module Core
|
|
5
|
+
# Canonical CDC operation names.
|
|
6
|
+
#
|
|
7
|
+
# Operations are represented as symbols to keep event objects small,
|
|
8
|
+
# immutable, and easy to compare. Use .normalize when accepting user input
|
|
9
|
+
# and .supported? when validating optional values.
|
|
10
|
+
module Operation
|
|
11
|
+
# Insert row operation.
|
|
12
|
+
INSERT = :insert
|
|
13
|
+
# Update row operation.
|
|
14
|
+
UPDATE = :update
|
|
15
|
+
# Delete row operation.
|
|
16
|
+
DELETE = :delete
|
|
17
|
+
# Truncate table operation.
|
|
18
|
+
TRUNCATE = :truncate
|
|
19
|
+
# Logical replication message operation.
|
|
20
|
+
MESSAGE = :message
|
|
21
|
+
# All operation names currently recognized by cdc-core.
|
|
22
|
+
SUPPORTED = Ractor.make_shareable([INSERT, UPDATE, DELETE, TRUNCATE, MESSAGE].freeze)
|
|
23
|
+
# Minimal row-change operations used by the initial runtime surface.
|
|
24
|
+
MVP = Ractor.make_shareable([INSERT, UPDATE, DELETE].freeze)
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# Convert an operation-like value into a supported operation symbol.
|
|
29
|
+
#
|
|
30
|
+
# @param operation [#to_sym] value to normalize
|
|
31
|
+
# @return [Symbol] one of SUPPORTED
|
|
32
|
+
# @raise [InvalidOperationError] when the operation is not supported
|
|
33
|
+
def normalize(operation)
|
|
34
|
+
value = operation.to_sym
|
|
35
|
+
return value if SUPPORTED.include?(value)
|
|
36
|
+
|
|
37
|
+
raise InvalidOperationError, "unsupported CDC operation: #{operation.inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check whether an operation-like value is supported.
|
|
41
|
+
#
|
|
42
|
+
# @param operation [#to_sym] value to check
|
|
43
|
+
# @return [Boolean] true when the value normalizes to a supported operation
|
|
44
|
+
def supported?(operation)
|
|
45
|
+
SUPPORTED.include?(operation.to_sym)
|
|
46
|
+
rescue NoMethodError
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module Core
|
|
5
|
+
# Connects filters with a processor to form an event-processing unit.
|
|
6
|
+
#
|
|
7
|
+
# A Pipeline first evaluates all filters. Matching events are handed to the
|
|
8
|
+
# processor, while filtered events produce skipped results. Processor errors
|
|
9
|
+
# are captured as failure results instead of escaping to the caller.
|
|
10
|
+
class Pipeline
|
|
11
|
+
# @return [#process] processor invoked for matching events
|
|
12
|
+
# @return [Array<Filter>] filters that must all match before processing
|
|
13
|
+
attr_reader :processor, :filters
|
|
14
|
+
|
|
15
|
+
# Build a pipeline.
|
|
16
|
+
#
|
|
17
|
+
# @param processor [#process] processor for matching events
|
|
18
|
+
# @param filters [Array<Filter>] filters applied before processing
|
|
19
|
+
def initialize(processor:, filters: [])
|
|
20
|
+
@processor = processor
|
|
21
|
+
@filters = filters.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Process one event through the pipeline.
|
|
25
|
+
#
|
|
26
|
+
# @param event [ChangeEvent] event to process
|
|
27
|
+
# @return [ProcessorResult]
|
|
28
|
+
def process(event)
|
|
29
|
+
return ProcessorResult.skipped(event, metadata: { reason: 'filtered' }) unless matches?(event)
|
|
30
|
+
|
|
31
|
+
normalize_result(processor.process(event), event)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
ProcessorResult.failure(e, event:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Process many events in order.
|
|
37
|
+
#
|
|
38
|
+
# @param events [Enumerable<ChangeEvent>] events to process
|
|
39
|
+
# @return [Array<ProcessorResult>]
|
|
40
|
+
def process_many(events)
|
|
41
|
+
events.map { |event| process(event) }.freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Check whether every filter matches an event.
|
|
47
|
+
#
|
|
48
|
+
# @param event [ChangeEvent]
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def matches?(event)
|
|
51
|
+
filters.all? { |filter| filter.match?(event) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Normalize raw processor output into a ProcessorResult.
|
|
55
|
+
#
|
|
56
|
+
# @param result [Object] raw processor result
|
|
57
|
+
# @param event [ChangeEvent] processed event
|
|
58
|
+
# @return [ProcessorResult]
|
|
59
|
+
def normalize_result(result, event)
|
|
60
|
+
return result if result.is_a?(ProcessorResult)
|
|
61
|
+
|
|
62
|
+
result ? ProcessorResult.success(event) : ProcessorResult.skipped(event)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module Core
|
|
5
|
+
# Base class for CDC event processors.
|
|
6
|
+
#
|
|
7
|
+
# Subclasses implement #process and may opt into Ractor-safe execution by
|
|
8
|
+
# calling .ractor_safe!. cdc-core itself does not schedule Ractors; it only
|
|
9
|
+
# records processor capabilities for runtime layers such as cdc-ractor.
|
|
10
|
+
class Processor
|
|
11
|
+
# Mark this processor class as safe to execute in Ractor-aware runtimes.
|
|
12
|
+
#
|
|
13
|
+
# @return [true]
|
|
14
|
+
def self.ractor_safe!
|
|
15
|
+
@ractor_safe = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Whether this processor class has declared Ractor safety.
|
|
19
|
+
#
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def self.ractor_safe?
|
|
22
|
+
@ractor_safe == true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Whether this processor instance is Ractor-safe.
|
|
26
|
+
#
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
def ractor_safe?
|
|
29
|
+
self.class.ractor_safe?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Process one event.
|
|
33
|
+
#
|
|
34
|
+
# Subclasses must override this method.
|
|
35
|
+
#
|
|
36
|
+
# @param _event [ChangeEvent] event to process
|
|
37
|
+
# @raise [NotImplementedError] when not implemented by a subclass
|
|
38
|
+
def process(_event)
|
|
39
|
+
raise NotImplementedError, "#{self.class} must implement #process"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module Core
|
|
5
|
+
# Result returned by processors and pipelines.
|
|
6
|
+
#
|
|
7
|
+
# ProcessorResult standardizes processor outcomes so callers can distinguish
|
|
8
|
+
# successful processing, skipped events, and failures without relying on
|
|
9
|
+
# processor-specific return values.
|
|
10
|
+
class ProcessorResult
|
|
11
|
+
# @return [Symbol] result status
|
|
12
|
+
# @return [ChangeEvent, nil] event associated with the result
|
|
13
|
+
# @return [Exception, nil] failure error, when status is :failure
|
|
14
|
+
# @return [EventMetadata] result metadata
|
|
15
|
+
attr_reader :status, :event, :error, :metadata
|
|
16
|
+
|
|
17
|
+
# Build a successful result.
|
|
18
|
+
#
|
|
19
|
+
# @param event [ChangeEvent, nil] processed event
|
|
20
|
+
# @param metadata [Hash, EventMetadata] result metadata
|
|
21
|
+
# @return [ProcessorResult]
|
|
22
|
+
def self.success(event = nil, metadata: {}) = new(:success, event:, metadata:)
|
|
23
|
+
|
|
24
|
+
# Build a failure result.
|
|
25
|
+
#
|
|
26
|
+
# @param error [Exception] processor error
|
|
27
|
+
# @param event [ChangeEvent, nil] event being processed
|
|
28
|
+
# @param metadata [Hash, EventMetadata] result metadata
|
|
29
|
+
# @return [ProcessorResult]
|
|
30
|
+
def self.failure(error, event: nil, metadata: {}) = new(:failure, event:, error:, metadata:)
|
|
31
|
+
|
|
32
|
+
# Build a skipped result.
|
|
33
|
+
#
|
|
34
|
+
# @param event [ChangeEvent, nil] skipped event
|
|
35
|
+
# @param metadata [Hash, EventMetadata] result metadata
|
|
36
|
+
# @return [ProcessorResult]
|
|
37
|
+
def self.skipped(event = nil, metadata: {}) = new(:skipped, event:, metadata:)
|
|
38
|
+
|
|
39
|
+
# Build a processor result with an explicit status.
|
|
40
|
+
#
|
|
41
|
+
# @param status [#to_sym] result status
|
|
42
|
+
# @param event [ChangeEvent, nil] associated event
|
|
43
|
+
# @param error [Exception, nil] associated failure
|
|
44
|
+
# @param metadata [Hash, EventMetadata] result metadata
|
|
45
|
+
def initialize(status, event: nil, error: nil, metadata: {})
|
|
46
|
+
@status = status.to_sym
|
|
47
|
+
@event = event
|
|
48
|
+
@error = error
|
|
49
|
+
@metadata = metadata.is_a?(EventMetadata) ? metadata : EventMetadata.new(metadata)
|
|
50
|
+
Ractor.make_shareable(self) unless error
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Boolean] true when status is :success
|
|
54
|
+
def success? = status == :success
|
|
55
|
+
|
|
56
|
+
# @return [Boolean] true when status is :failure
|
|
57
|
+
def failure? = status == :failure
|
|
58
|
+
|
|
59
|
+
# @return [Boolean] true when status is :skipped
|
|
60
|
+
def skipped? = status == :skipped
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module Core
|
|
5
|
+
# Immutable group of change events committed in one transaction.
|
|
6
|
+
#
|
|
7
|
+
# TransactionEnvelope is useful when a downstream processor needs transaction
|
|
8
|
+
# boundaries instead of isolated row-level events. The contained events and
|
|
9
|
+
# metadata are Ractor-shareable when construction succeeds.
|
|
10
|
+
class TransactionEnvelope
|
|
11
|
+
# @return [Object] transaction identifier
|
|
12
|
+
# @return [Array<ChangeEvent>] events committed by the transaction
|
|
13
|
+
# @return [String, nil] commit log sequence number
|
|
14
|
+
# @return [Time, nil] commit timestamp
|
|
15
|
+
# @return [EventMetadata] transaction metadata
|
|
16
|
+
attr_reader :transaction_id, :events, :commit_lsn, :committed_at, :metadata
|
|
17
|
+
|
|
18
|
+
# Build a transaction envelope.
|
|
19
|
+
#
|
|
20
|
+
# @param transaction_id [Object] upstream transaction identifier
|
|
21
|
+
# @param events [Array<ChangeEvent>] committed events
|
|
22
|
+
# @param commit_lsn [#to_s, nil] commit log sequence number
|
|
23
|
+
# @param committed_at [Time, nil] commit timestamp
|
|
24
|
+
# @param metadata [Hash, EventMetadata] transaction metadata
|
|
25
|
+
def initialize(transaction_id:, events:, commit_lsn: nil, committed_at: nil, metadata: {})
|
|
26
|
+
@transaction_id = transaction_id
|
|
27
|
+
@events = Ractor.make_shareable(events.freeze)
|
|
28
|
+
@commit_lsn = commit_lsn&.to_s&.freeze
|
|
29
|
+
@committed_at = committed_at
|
|
30
|
+
@metadata = metadata.is_a?(EventMetadata) ? metadata : EventMetadata.new(metadata)
|
|
31
|
+
Ractor.make_shareable(self)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether the envelope has no events.
|
|
35
|
+
#
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def empty? = events.empty?
|
|
38
|
+
|
|
39
|
+
# Number of events in the envelope.
|
|
40
|
+
#
|
|
41
|
+
# @return [Integer]
|
|
42
|
+
def size = events.size
|
|
43
|
+
|
|
44
|
+
# Convert the transaction envelope into a Ractor-shareable hash.
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash{String=>Object,nil}]
|
|
47
|
+
def to_h
|
|
48
|
+
Ractor.make_shareable({
|
|
49
|
+
'transaction_id' => transaction_id,
|
|
50
|
+
'events' => events.map(&:to_h).freeze,
|
|
51
|
+
'commit_lsn' => commit_lsn,
|
|
52
|
+
'committed_at' => committed_at,
|
|
53
|
+
'metadata' => metadata.to_h
|
|
54
|
+
}.freeze)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/cdc/core/version.rb
CHANGED
data/lib/cdc/core.rb
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'core/version'
|
|
4
|
+
require_relative 'core/errors'
|
|
5
|
+
require_relative 'core/operation'
|
|
6
|
+
require_relative 'core/column_change'
|
|
7
|
+
require_relative 'core/event_metadata'
|
|
8
|
+
require_relative 'core/change_event'
|
|
9
|
+
require_relative 'core/transaction_envelope'
|
|
10
|
+
require_relative 'core/processor_result'
|
|
11
|
+
require_relative 'core/processor'
|
|
12
|
+
require_relative 'core/composite_processor'
|
|
13
|
+
require_relative 'core/filter'
|
|
14
|
+
require_relative 'core/pipeline'
|
|
4
15
|
|
|
5
|
-
|
|
16
|
+
# Top-level namespace for Change Data Capture libraries.
|
|
17
|
+
module CDC
|
|
18
|
+
# Database-agnostic Change Data Capture domain primitives.
|
|
19
|
+
#
|
|
20
|
+
# CDC::Core intentionally contains only lightweight runtime abstractions:
|
|
21
|
+
# events, metadata, processors, filters, pipelines, and processor results.
|
|
22
|
+
# Transport, PostgreSQL protocol parsing, and value decoding live in sibling
|
|
23
|
+
# gems so this layer can remain independently useful.
|
|
6
24
|
module Core
|
|
7
|
-
class Error < StandardError; end
|
|
8
|
-
# Your code goes here...
|
|
9
25
|
end
|
|
10
26
|
end
|
data/lib/cdc_core.rb
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
module CDC
|
|
2
|
+
module Core
|
|
3
|
+
# Immutable representation of one logical database change.
|
|
4
|
+
#
|
|
5
|
+
# ChangeEvent is the core data structure passed through filters, pipelines,
|
|
6
|
+
# and processors. It is database-agnostic but carries common CDC fields such
|
|
7
|
+
# as operation, schema, table, before/after values, primary key, LSN, and
|
|
8
|
+
# metadata.
|
|
9
|
+
class ChangeEvent
|
|
10
|
+
@operation: untyped
|
|
11
|
+
|
|
12
|
+
@schema: untyped
|
|
13
|
+
|
|
14
|
+
@table: untyped
|
|
15
|
+
|
|
16
|
+
@old_values: untyped
|
|
17
|
+
|
|
18
|
+
@new_values: untyped
|
|
19
|
+
|
|
20
|
+
@primary_key: untyped
|
|
21
|
+
|
|
22
|
+
@transaction_id: untyped
|
|
23
|
+
|
|
24
|
+
@commit_lsn: untyped
|
|
25
|
+
|
|
26
|
+
@sequence_number: untyped
|
|
27
|
+
|
|
28
|
+
@occurred_at: untyped
|
|
29
|
+
|
|
30
|
+
@metadata: untyped
|
|
31
|
+
|
|
32
|
+
# @return [Symbol] normalized CDC operation
|
|
33
|
+
# @return [String] database schema name
|
|
34
|
+
# @return [String] database table name
|
|
35
|
+
# @return [Hash, nil] values before the change
|
|
36
|
+
# @return [Hash, nil] values after the change
|
|
37
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
38
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
39
|
+
# @return [String, nil] commit log sequence number
|
|
40
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
41
|
+
# @return [Time, nil] timestamp associated with the event
|
|
42
|
+
# @return [EventMetadata] additional normalized metadata
|
|
43
|
+
attr_reader operation: untyped
|
|
44
|
+
|
|
45
|
+
# @return [Symbol] normalized CDC operation
|
|
46
|
+
# @return [String] database schema name
|
|
47
|
+
# @return [String] database table name
|
|
48
|
+
# @return [Hash, nil] values before the change
|
|
49
|
+
# @return [Hash, nil] values after the change
|
|
50
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
51
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
52
|
+
# @return [String, nil] commit log sequence number
|
|
53
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
54
|
+
# @return [Time, nil] timestamp associated with the event
|
|
55
|
+
# @return [EventMetadata] additional normalized metadata
|
|
56
|
+
attr_reader schema: untyped
|
|
57
|
+
|
|
58
|
+
# @return [Symbol] normalized CDC operation
|
|
59
|
+
# @return [String] database schema name
|
|
60
|
+
# @return [String] database table name
|
|
61
|
+
# @return [Hash, nil] values before the change
|
|
62
|
+
# @return [Hash, nil] values after the change
|
|
63
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
64
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
65
|
+
# @return [String, nil] commit log sequence number
|
|
66
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
67
|
+
# @return [Time, nil] timestamp associated with the event
|
|
68
|
+
# @return [EventMetadata] additional normalized metadata
|
|
69
|
+
attr_reader table: untyped
|
|
70
|
+
|
|
71
|
+
# @return [Symbol] normalized CDC operation
|
|
72
|
+
# @return [String] database schema name
|
|
73
|
+
# @return [String] database table name
|
|
74
|
+
# @return [Hash, nil] values before the change
|
|
75
|
+
# @return [Hash, nil] values after the change
|
|
76
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
77
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
78
|
+
# @return [String, nil] commit log sequence number
|
|
79
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
80
|
+
# @return [Time, nil] timestamp associated with the event
|
|
81
|
+
# @return [EventMetadata] additional normalized metadata
|
|
82
|
+
attr_reader old_values: untyped
|
|
83
|
+
|
|
84
|
+
# @return [Symbol] normalized CDC operation
|
|
85
|
+
# @return [String] database schema name
|
|
86
|
+
# @return [String] database table name
|
|
87
|
+
# @return [Hash, nil] values before the change
|
|
88
|
+
# @return [Hash, nil] values after the change
|
|
89
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
90
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
91
|
+
# @return [String, nil] commit log sequence number
|
|
92
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
93
|
+
# @return [Time, nil] timestamp associated with the event
|
|
94
|
+
# @return [EventMetadata] additional normalized metadata
|
|
95
|
+
attr_reader new_values: untyped
|
|
96
|
+
|
|
97
|
+
# @return [Symbol] normalized CDC operation
|
|
98
|
+
# @return [String] database schema name
|
|
99
|
+
# @return [String] database table name
|
|
100
|
+
# @return [Hash, nil] values before the change
|
|
101
|
+
# @return [Hash, nil] values after the change
|
|
102
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
103
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
104
|
+
# @return [String, nil] commit log sequence number
|
|
105
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
106
|
+
# @return [Time, nil] timestamp associated with the event
|
|
107
|
+
# @return [EventMetadata] additional normalized metadata
|
|
108
|
+
attr_reader primary_key: untyped
|
|
109
|
+
|
|
110
|
+
# @return [Symbol] normalized CDC operation
|
|
111
|
+
# @return [String] database schema name
|
|
112
|
+
# @return [String] database table name
|
|
113
|
+
# @return [Hash, nil] values before the change
|
|
114
|
+
# @return [Hash, nil] values after the change
|
|
115
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
116
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
117
|
+
# @return [String, nil] commit log sequence number
|
|
118
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
119
|
+
# @return [Time, nil] timestamp associated with the event
|
|
120
|
+
# @return [EventMetadata] additional normalized metadata
|
|
121
|
+
attr_reader transaction_id: untyped
|
|
122
|
+
|
|
123
|
+
# @return [Symbol] normalized CDC operation
|
|
124
|
+
# @return [String] database schema name
|
|
125
|
+
# @return [String] database table name
|
|
126
|
+
# @return [Hash, nil] values before the change
|
|
127
|
+
# @return [Hash, nil] values after the change
|
|
128
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
129
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
130
|
+
# @return [String, nil] commit log sequence number
|
|
131
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
132
|
+
# @return [Time, nil] timestamp associated with the event
|
|
133
|
+
# @return [EventMetadata] additional normalized metadata
|
|
134
|
+
attr_reader commit_lsn: untyped
|
|
135
|
+
|
|
136
|
+
# @return [Symbol] normalized CDC operation
|
|
137
|
+
# @return [String] database schema name
|
|
138
|
+
# @return [String] database table name
|
|
139
|
+
# @return [Hash, nil] values before the change
|
|
140
|
+
# @return [Hash, nil] values after the change
|
|
141
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
142
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
143
|
+
# @return [String, nil] commit log sequence number
|
|
144
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
145
|
+
# @return [Time, nil] timestamp associated with the event
|
|
146
|
+
# @return [EventMetadata] additional normalized metadata
|
|
147
|
+
attr_reader sequence_number: untyped
|
|
148
|
+
|
|
149
|
+
# @return [Symbol] normalized CDC operation
|
|
150
|
+
# @return [String] database schema name
|
|
151
|
+
# @return [String] database table name
|
|
152
|
+
# @return [Hash, nil] values before the change
|
|
153
|
+
# @return [Hash, nil] values after the change
|
|
154
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
155
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
156
|
+
# @return [String, nil] commit log sequence number
|
|
157
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
158
|
+
# @return [Time, nil] timestamp associated with the event
|
|
159
|
+
# @return [EventMetadata] additional normalized metadata
|
|
160
|
+
attr_reader occurred_at: untyped
|
|
161
|
+
|
|
162
|
+
# @return [Symbol] normalized CDC operation
|
|
163
|
+
# @return [String] database schema name
|
|
164
|
+
# @return [String] database table name
|
|
165
|
+
# @return [Hash, nil] values before the change
|
|
166
|
+
# @return [Hash, nil] values after the change
|
|
167
|
+
# @return [Hash, nil] primary key values for the changed row
|
|
168
|
+
# @return [Object, nil] transaction identifier from the upstream source
|
|
169
|
+
# @return [String, nil] commit log sequence number
|
|
170
|
+
# @return [Integer, nil] event sequence within a transaction or stream
|
|
171
|
+
# @return [Time, nil] timestamp associated with the event
|
|
172
|
+
# @return [EventMetadata] additional normalized metadata
|
|
173
|
+
attr_reader metadata: untyped
|
|
174
|
+
|
|
175
|
+
# Build a change event.
|
|
176
|
+
#
|
|
177
|
+
# @param operation [#to_sym] CDC operation
|
|
178
|
+
# @param schema [#to_s] schema name
|
|
179
|
+
# @param table [#to_s] table name
|
|
180
|
+
# @param old_values [Hash, nil] values before the change
|
|
181
|
+
# @param new_values [Hash, nil] values after the change
|
|
182
|
+
# @param primary_key [Hash, nil] primary key values
|
|
183
|
+
# @param transaction_id [Object, nil] source transaction identifier
|
|
184
|
+
# @param commit_lsn [#to_s, nil] commit log sequence number
|
|
185
|
+
# @param sequence_number [Integer, nil] event sequence number
|
|
186
|
+
# @param occurred_at [Time, nil] event timestamp
|
|
187
|
+
# @param metadata [Hash, EventMetadata] additional event metadata
|
|
188
|
+
def initialize: (operation: untyped, schema: untyped, table: untyped, ?old_values: untyped?, ?new_values: untyped?, ?primary_key: untyped?, ?transaction_id: untyped?, ?commit_lsn: untyped?, ?sequence_number: untyped?, ?occurred_at: untyped?, ?metadata: ::Hash[untyped, untyped]) -> void
|
|
189
|
+
|
|
190
|
+
# @return [Boolean] true for insert events
|
|
191
|
+
def insert?: () -> untyped
|
|
192
|
+
|
|
193
|
+
# @return [Boolean] true for update events
|
|
194
|
+
def update?: () -> untyped
|
|
195
|
+
|
|
196
|
+
# @return [Boolean] true for delete events
|
|
197
|
+
def delete?: () -> untyped
|
|
198
|
+
|
|
199
|
+
# Fully qualified table name in schema.table form.
|
|
200
|
+
#
|
|
201
|
+
# @return [String]
|
|
202
|
+
def qualified_table_name: () -> ::String
|
|
203
|
+
|
|
204
|
+
# Compute changed columns by comparing old and new values.
|
|
205
|
+
#
|
|
206
|
+
# Columns with equal old and new values are omitted. Insert and delete
|
|
207
|
+
# events can pass nil for one side; missing values are represented as nil.
|
|
208
|
+
#
|
|
209
|
+
# @return [Array<ColumnChange>] Ractor-shareable changed columns
|
|
210
|
+
def changes: () -> untyped
|
|
211
|
+
|
|
212
|
+
# Convert the event into a Ractor-shareable hash.
|
|
213
|
+
#
|
|
214
|
+
# @return [Hash{String=>Object,nil}]
|
|
215
|
+
def to_h: () -> untyped
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
# Convert a hash into immutable EventMetadata storage, preserving nil.
|
|
220
|
+
#
|
|
221
|
+
# @param hash [Hash, nil]
|
|
222
|
+
# @return [Hash, nil]
|
|
223
|
+
def freeze_hash_or_nil: (untyped hash) -> (nil | untyped)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module CDC
|
|
2
|
+
module Core
|
|
3
|
+
# Represents a single column-level value change.
|
|
4
|
+
#
|
|
5
|
+
# ColumnChange is immutable and Ractor-shareable. Values that cannot be made
|
|
6
|
+
# shareable by Ruby are represented by their frozen #inspect string so the
|
|
7
|
+
# enclosing event can still cross Ractor boundaries safely.
|
|
8
|
+
class ColumnChange
|
|
9
|
+
@name: untyped
|
|
10
|
+
|
|
11
|
+
@old_value: untyped
|
|
12
|
+
|
|
13
|
+
@new_value: untyped
|
|
14
|
+
|
|
15
|
+
# @return [String] column name
|
|
16
|
+
# @return [Object, nil] value before the change
|
|
17
|
+
# @return [Object, nil] value after the change
|
|
18
|
+
attr_reader name: untyped
|
|
19
|
+
|
|
20
|
+
# @return [String] column name
|
|
21
|
+
# @return [Object, nil] value before the change
|
|
22
|
+
# @return [Object, nil] value after the change
|
|
23
|
+
attr_reader old_value: untyped
|
|
24
|
+
|
|
25
|
+
# @return [String] column name
|
|
26
|
+
# @return [Object, nil] value before the change
|
|
27
|
+
# @return [Object, nil] value after the change
|
|
28
|
+
attr_reader new_value: untyped
|
|
29
|
+
|
|
30
|
+
# Build a column-level change object.
|
|
31
|
+
#
|
|
32
|
+
# @param name [#to_s] column name
|
|
33
|
+
# @param old_value [Object, nil] previous value
|
|
34
|
+
# @param new_value [Object, nil] new value
|
|
35
|
+
def initialize: (name: untyped, old_value: untyped, new_value: untyped) -> void
|
|
36
|
+
|
|
37
|
+
# Whether the old and new values differ.
|
|
38
|
+
#
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def changed?: () -> untyped
|
|
41
|
+
|
|
42
|
+
# Convert the change into a Ractor-shareable hash.
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash{String=>Object,nil}]
|
|
45
|
+
def to_h: () -> untyped
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Convert a value into a Ractor-shareable representation.
|
|
50
|
+
#
|
|
51
|
+
# @param value [Object, nil]
|
|
52
|
+
# @return [Object, String, nil]
|
|
53
|
+
def make_value_shareable: (untyped value) -> untyped
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module CDC
|
|
2
|
+
module Core
|
|
3
|
+
# Processor that delegates each event to multiple processors.
|
|
4
|
+
#
|
|
5
|
+
# CompositeProcessor enables fan-out processing while preserving a simple
|
|
6
|
+
# sequential execution model. It normalizes truthy/falsey processor returns
|
|
7
|
+
# into ProcessorResult objects and can stop at the first failure.
|
|
8
|
+
class CompositeProcessor < Processor
|
|
9
|
+
@processors: untyped
|
|
10
|
+
|
|
11
|
+
@fail_fast: untyped
|
|
12
|
+
|
|
13
|
+
# @return [Array<Processor>] processors executed for each event
|
|
14
|
+
# @return [Boolean] whether processing stops on the first failure
|
|
15
|
+
attr_reader processors: untyped
|
|
16
|
+
|
|
17
|
+
# @return [Array<Processor>] processors executed for each event
|
|
18
|
+
# @return [Boolean] whether processing stops on the first failure
|
|
19
|
+
attr_reader fail_fast: untyped
|
|
20
|
+
|
|
21
|
+
# Build a composite processor.
|
|
22
|
+
#
|
|
23
|
+
# @param processors [Array<#process>] processors to execute
|
|
24
|
+
# @param fail_fast [Boolean] whether to stop after the first failure
|
|
25
|
+
def initialize: (untyped processors, ?fail_fast: bool) -> void
|
|
26
|
+
|
|
27
|
+
# Process an event through each configured processor.
|
|
28
|
+
#
|
|
29
|
+
# @param event [ChangeEvent] event to process
|
|
30
|
+
# @return [Array<ProcessorResult>] result from each attempted processor
|
|
31
|
+
def process: (untyped event) -> untyped
|
|
32
|
+
|
|
33
|
+
# Processors that declared Ractor safety.
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<Processor>]
|
|
36
|
+
def ractor_safe_processors: () -> untyped
|
|
37
|
+
|
|
38
|
+
# Processors that should remain sequential in the core runtime.
|
|
39
|
+
#
|
|
40
|
+
# @return [Array<Processor>]
|
|
41
|
+
def sequential_processors: () -> untyped
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Normalize processor return values into ProcessorResult objects.
|
|
46
|
+
#
|
|
47
|
+
# @param result [Object] raw processor result
|
|
48
|
+
# @param event [ChangeEvent] processed event
|
|
49
|
+
# @return [ProcessorResult]
|
|
50
|
+
def normalize_result: (untyped result, untyped event) -> untyped
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module CDC
|
|
2
|
+
module Core
|
|
3
|
+
# Base error class for all cdc-core specific failures.
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Raised when an operation cannot be normalized to a supported CDC action.
|
|
8
|
+
class InvalidOperationError < Error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Raised by processors when a processor-specific failure needs wrapping.
|
|
12
|
+
class ProcessorError < Error
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|