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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b050ff23c85039e0169e83b14b7afc1f3c034cd5eedd372982c0f9f82844a2f
4
- data.tar.gz: 5dfb41f2cd37724277c33b6932d75cfd57f4cd6b3564aff5317944aa9d97114e
3
+ metadata.gz: 5ace8b27c4df8285c5354599650cb450cddaad1402c913689e638fb9ec57d8b9
4
+ data.tar.gz: 381f77ee16278ea147b083b677d5e9248a2fb6bf1955c40f9d2eab82055a1e49
5
5
  SHA512:
6
- metadata.gz: e34666e6300fbdf9ad44c3a442cc3cee75b0edaad9cde14cbdede71ff68a666752838b2b2aafb0db54254857f704727f26465961bdf0c6e876a80c44acb86174
7
- data.tar.gz: ac796f126b00581d4e6d44294953dbe16c0082e0f4b7cdfd85ed1699e813dd82cd0ff2d0e663b0f4ac8c3c7de50d02e2e12801a86c3b1879a0f29f4288f4f8c9
6
+ metadata.gz: c0447c01564b04f20170e8a7f9e60cb95c12f77ef87ce54ab8a090083b7054a041224247349a9424e803f22255bbe097b33706a16c267ed1710e67fab7e6fff0
7
+ data.tar.gz: 9aef7f1a3ffd777c6031e700c49f17a3aa460fa46e898d648d55b81c1a2afaf6df4e19fd700bbadad57be382d0809990909f9a97ac721ea7121fc3e327c868b7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
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
+
1
8
  ## [Unreleased]
2
9
 
10
+ ### Added
11
+
12
+ - Placeholder for future development.
13
+
14
+ ---
15
+
3
16
  ## [0.1.0] - 2026-05-31
4
17
 
5
- - Initial release
18
+ ### Added
19
+
20
+ - Added immutable `CDC::Core::ChangeEvent`.
21
+ - Added immutable `CDC::Core::ColumnChange`.
22
+ - Added immutable `CDC::Core::TransactionEnvelope`.
23
+ - Added `CDC::Core::EventMetadata`.
24
+ - Added operation validation.
25
+ - Added `CDC::Core::Processor` base contract.
26
+ - Added `CDC::Core::ProcessorResult`.
27
+ - Added `CDC::Core::CompositeProcessor`.
28
+ - Added `CDC::Core::Filter`.
29
+ - Added `CDC::Core::Pipeline`.
30
+ - Added Ractor-safe processor intent declaration via `ractor_safe!`.
31
+ - Added RBS signatures.
32
+ - Added Minitest coverage.
33
+ - Added README and examples.
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
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
@@ -19,3 +19,4 @@ 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
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  THE SOFTWARE.
22
+
data/README.md CHANGED
@@ -1,43 +1,166 @@
1
- # Cdc::Core
2
-
3
- TODO: Delete this and the text below, and describe your gem
1
+ # cdc-core
2
+
3
+ Database-agnostic Change Data Capture domain primitives for Ruby.
4
+
5
+ `cdc-core` provides immutable, Ractor-safe event objects and processor contracts for building CDC systems. It intentionally does not connect to databases, parse wire protocols, decode PostgreSQL OIDs, or integrate with Rails.
6
+
7
+ ## Requirements
8
+
9
+ - Ruby 3.4+
10
+
11
+ ## Features
12
+
13
+ - Immutable `ChangeEvent` objects
14
+ - Transaction grouping via `TransactionEnvelope`
15
+ - Column-level change objects
16
+ - Processor and composite processor contracts
17
+ - Event filters
18
+ - Small pipeline orchestration object
19
+ - Ractor-safe event and transaction objects
20
+ - RBS signatures
21
+ - YARD-compatible documentation
22
+ - No runtime dependencies
23
+
24
+ ## Ecosystem Position
25
+
26
+ ```text
27
+ pgoutput-client
28
+
29
+
30
+ pgoutput-parser
31
+
32
+
33
+ pgoutput-decoder
34
+
35
+
36
+ cdc-core
37
+
38
+
39
+ whodunit-chronicles
40
+ ```
4
41
 
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/cdc/core`. To experiment with that code, run `bin/console` for an interactive prompt.
42
+ `cdc-core` is the shared vocabulary layer. It defines what a change event, transaction, processor, and pipeline result means without caring where the event came from.
6
43
 
7
44
  ## Installation
8
45
 
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.
46
+ ```ruby
47
+ gem "cdc-core"
48
+ ```
49
+
50
+ ```ruby
51
+ require "cdc/core"
52
+ ```
10
53
 
11
- Install the gem and add to the application's Gemfile by executing:
54
+ ## Change Events
12
55
 
13
- ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
56
+ ```ruby
57
+ event = CDC::Core::ChangeEvent.new(
58
+ operation: :update,
59
+ schema: "public",
60
+ table: "users",
61
+ old_values: { "email" => "old@example.com" },
62
+ new_values: { "email" => "new@example.com" },
63
+ primary_key: { "id" => 7 },
64
+ transaction_id: 789,
65
+ commit_lsn: "0/16B6C50"
66
+ )
67
+
68
+ event.update?
69
+ # => true
70
+
71
+ event.qualified_table_name
72
+ # => "public.users"
73
+
74
+ event.changes.map(&:name)
75
+ # => ["email"]
15
76
  ```
16
77
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
78
+ ## Transactions
18
79
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
80
+ ```ruby
81
+ transaction = CDC::Core::TransactionEnvelope.new(
82
+ transaction_id: 789,
83
+ events: [event],
84
+ commit_lsn: "0/16B6C50",
85
+ committed_at: Time.now.utc
86
+ )
21
87
  ```
22
88
 
23
- ## Usage
89
+ A transaction envelope is the natural unit for future parallel processing because it preserves database transaction boundaries.
24
90
 
25
- TODO: Write usage instructions here
91
+ ## Processors
26
92
 
27
- ## Development
93
+ ```ruby
94
+ class AuditProcessor < CDC::Core::Processor
95
+ def process(event)
96
+ puts event.to_h
97
+ CDC::Core::ProcessorResult.success(event)
98
+ end
99
+ end
100
+ ```
28
101
 
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.
102
+ ## Ractor-safe processor intent
30
103
 
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).
104
+ ```ruby
105
+ class AnalyticsProcessor < CDC::Core::Processor
106
+ ractor_safe!
32
107
 
33
- ## Contributing
108
+ def process(event)
109
+ CDC::Core::ProcessorResult.success(event)
110
+ end
111
+ end
34
112
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/cdc-core. 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]/cdc-core/blob/main/CODE_OF_CONDUCT.md).
113
+ AnalyticsProcessor.new.ractor_safe?
114
+ # => true
115
+ ```
36
116
 
37
- ## License
117
+ This declares intent only. `cdc-core` does not execute processors in Ractors. A future runtime gem can use this signal.
118
+
119
+ ## Composite Processor
38
120
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
121
+ ```ruby
122
+ processor = CDC::Core::CompositeProcessor.new([
123
+ AuditProcessor.new,
124
+ AnalyticsProcessor.new
125
+ ])
40
126
 
41
- ## Code of Conduct
127
+ results = processor.process(event)
128
+ ```
129
+
130
+ ## Filters and Pipeline
131
+
132
+ ```ruby
133
+ pipeline = CDC::Core::Pipeline.new(
134
+ processor: AuditProcessor.new,
135
+ filters: [
136
+ CDC::Core::Filter.schema("public"),
137
+ CDC::Core::Filter.table("users")
138
+ ]
139
+ )
140
+
141
+ result = pipeline.process(event)
142
+ ```
143
+
144
+ ## Non-goals
145
+
146
+ `cdc-core` does not:
147
+
148
+ - Connect to PostgreSQL
149
+ - Parse `pgoutput`
150
+ - Decode PostgreSQL values
151
+ - Manage replication slots
152
+ - Run Ractor pools
153
+ - Persist audit records
154
+ - Integrate with ActiveRecord
155
+ - Publish to Kafka, Redis, or HTTP sinks
156
+
157
+ ## Development
158
+
159
+ ```bash
160
+ bundle exec rake
161
+ bundle exec steep check
162
+ ```
163
+
164
+ ## License
42
165
 
43
- Everyone interacting in the Cdc::Core project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/cdc-core/blob/main/CODE_OF_CONDUCT.md).
166
+ MIT.
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDC
4
+ module Core
5
+ # Immutable representation of one logical database change.
6
+ #
7
+ # ChangeEvent is the core data structure passed through filters, pipelines,
8
+ # and processors. It is database-agnostic but carries common CDC fields such
9
+ # as operation, schema, table, before/after values, primary key, LSN, and
10
+ # metadata.
11
+ class ChangeEvent
12
+ # @return [Symbol] normalized CDC operation
13
+ # @return [String] database schema name
14
+ # @return [String] database table name
15
+ # @return [Hash, nil] values before the change
16
+ # @return [Hash, nil] values after the change
17
+ # @return [Hash, nil] primary key values for the changed row
18
+ # @return [Object, nil] transaction identifier from the upstream source
19
+ # @return [String, nil] commit log sequence number
20
+ # @return [Integer, nil] event sequence within a transaction or stream
21
+ # @return [Time, nil] timestamp associated with the event
22
+ # @return [EventMetadata] additional normalized metadata
23
+ attr_reader :operation, :schema, :table, :old_values, :new_values, :primary_key,
24
+ :transaction_id, :commit_lsn, :sequence_number, :occurred_at, :metadata
25
+
26
+ # Build a change event.
27
+ #
28
+ # @param operation [#to_sym] CDC operation
29
+ # @param schema [#to_s] schema name
30
+ # @param table [#to_s] table name
31
+ # @param old_values [Hash, nil] values before the change
32
+ # @param new_values [Hash, nil] values after the change
33
+ # @param primary_key [Hash, nil] primary key values
34
+ # @param transaction_id [Object, nil] source transaction identifier
35
+ # @param commit_lsn [#to_s, nil] commit log sequence number
36
+ # @param sequence_number [Integer, nil] event sequence number
37
+ # @param occurred_at [Time, nil] event timestamp
38
+ # @param metadata [Hash, EventMetadata] additional event metadata
39
+ def initialize(operation:, schema:, table:, old_values: nil, new_values: nil, primary_key: nil,
40
+ transaction_id: nil, commit_lsn: nil, sequence_number: nil, occurred_at: nil,
41
+ metadata: {})
42
+ @operation = Operation.normalize(operation)
43
+ @schema = String(schema).freeze
44
+ @table = String(table).freeze
45
+ @old_values = freeze_hash_or_nil(old_values)
46
+ @new_values = freeze_hash_or_nil(new_values)
47
+ @primary_key = freeze_hash_or_nil(primary_key)
48
+ @transaction_id = transaction_id
49
+ @commit_lsn = commit_lsn&.to_s&.freeze
50
+ @sequence_number = sequence_number
51
+ @occurred_at = occurred_at
52
+ @metadata = metadata.is_a?(EventMetadata) ? metadata : EventMetadata.new(metadata)
53
+ Ractor.make_shareable(self)
54
+ end
55
+
56
+ # @return [Boolean] true for insert events
57
+ def insert? = operation == Operation::INSERT
58
+
59
+ # @return [Boolean] true for update events
60
+ def update? = operation == Operation::UPDATE
61
+
62
+ # @return [Boolean] true for delete events
63
+ def delete? = operation == Operation::DELETE
64
+
65
+ # Fully qualified table name in schema.table form.
66
+ #
67
+ # @return [String]
68
+ def qualified_table_name = "#{schema}.#{table}".freeze
69
+
70
+ # Compute changed columns by comparing old and new values.
71
+ #
72
+ # Columns with equal old and new values are omitted. Insert and delete
73
+ # events can pass nil for one side; missing values are represented as nil.
74
+ #
75
+ # @return [Array<ColumnChange>] Ractor-shareable changed columns
76
+ def changes
77
+ keys = ((old_values || {}).keys | (new_values || {}).keys)
78
+ keys.filter_map do |key|
79
+ change = ColumnChange.new(name: key, old_value: old_values&.[](key), new_value: new_values&.[](key))
80
+ change if change.changed?
81
+ end.then { |items| Ractor.make_shareable(items.freeze) }
82
+ end
83
+
84
+ # Convert the event into a Ractor-shareable hash.
85
+ #
86
+ # @return [Hash{String=>Object,nil}]
87
+ def to_h
88
+ Ractor.make_shareable({
89
+ 'operation' => operation,
90
+ 'schema' => schema,
91
+ 'table' => table,
92
+ 'old_values' => old_values,
93
+ 'new_values' => new_values,
94
+ 'primary_key' => primary_key,
95
+ 'transaction_id' => transaction_id,
96
+ 'commit_lsn' => commit_lsn,
97
+ 'sequence_number' => sequence_number,
98
+ 'occurred_at' => occurred_at,
99
+ 'metadata' => metadata.to_h
100
+ }.freeze)
101
+ end
102
+
103
+ private
104
+
105
+ # Convert a hash into immutable EventMetadata storage, preserving nil.
106
+ #
107
+ # @param hash [Hash, nil]
108
+ # @return [Hash, nil]
109
+ def freeze_hash_or_nil(hash)
110
+ return nil if hash.nil?
111
+
112
+ EventMetadata.new(hash).to_h
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDC
4
+ module Core
5
+ # Represents a single column-level value change.
6
+ #
7
+ # ColumnChange is immutable and Ractor-shareable. Values that cannot be made
8
+ # shareable by Ruby are represented by their frozen #inspect string so the
9
+ # enclosing event can still cross Ractor boundaries safely.
10
+ class ColumnChange
11
+ # @return [String] column name
12
+ # @return [Object, nil] value before the change
13
+ # @return [Object, nil] value after the change
14
+ attr_reader :name, :old_value, :new_value
15
+
16
+ # Build a column-level change object.
17
+ #
18
+ # @param name [#to_s] column name
19
+ # @param old_value [Object, nil] previous value
20
+ # @param new_value [Object, nil] new value
21
+ def initialize(name:, old_value:, new_value:)
22
+ @name = String(name).freeze
23
+ @old_value = make_value_shareable(old_value)
24
+ @new_value = make_value_shareable(new_value)
25
+ Ractor.make_shareable(self)
26
+ end
27
+
28
+ # Whether the old and new values differ.
29
+ #
30
+ # @return [Boolean]
31
+ def changed?
32
+ old_value != new_value
33
+ end
34
+
35
+ # Convert the change into a Ractor-shareable hash.
36
+ #
37
+ # @return [Hash{String=>Object,nil}]
38
+ def to_h
39
+ Ractor.make_shareable({ 'name' => name, 'old_value' => old_value, 'new_value' => new_value }.freeze)
40
+ end
41
+
42
+ private
43
+
44
+ # Convert a value into a Ractor-shareable representation.
45
+ #
46
+ # @param value [Object, nil]
47
+ # @return [Object, String, nil]
48
+ def make_value_shareable(value)
49
+ Ractor.make_shareable(value)
50
+ rescue Ractor::Error
51
+ Ractor.make_shareable(value.inspect.freeze)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDC
4
+ module Core
5
+ # Processor that delegates each event to multiple processors.
6
+ #
7
+ # CompositeProcessor enables fan-out processing while preserving a simple
8
+ # sequential execution model. It normalizes truthy/falsey processor returns
9
+ # into ProcessorResult objects and can stop at the first failure.
10
+ class CompositeProcessor < Processor
11
+ # @return [Array<Processor>] processors executed for each event
12
+ # @return [Boolean] whether processing stops on the first failure
13
+ attr_reader :processors, :fail_fast
14
+
15
+ # Build a composite processor.
16
+ #
17
+ # @param processors [Array<#process>] processors to execute
18
+ # @param fail_fast [Boolean] whether to stop after the first failure
19
+ def initialize(processors, fail_fast: true) # rubocop:disable Lint/MissingSuper
20
+ @processors = processors.freeze
21
+ @fail_fast = fail_fast
22
+ end
23
+
24
+ # Process an event through each configured processor.
25
+ #
26
+ # @param event [ChangeEvent] event to process
27
+ # @return [Array<ProcessorResult>] result from each attempted processor
28
+ def process(event)
29
+ results = [] # : Array[ProcessorResult]
30
+ processors.each do |processor|
31
+ result = normalize_result(processor.process(event), event)
32
+ results << result
33
+ break if fail_fast && result.failure?
34
+ rescue StandardError => e
35
+ result = ProcessorResult.failure(e, event:)
36
+ results << result
37
+ break if fail_fast
38
+ end
39
+ Ractor.make_shareable(results.freeze) if results.none?(&:failure?)
40
+ results.freeze
41
+ end
42
+
43
+ # Processors that declared Ractor safety.
44
+ #
45
+ # @return [Array<Processor>]
46
+ def ractor_safe_processors
47
+ processors.select(&:ractor_safe?).freeze
48
+ end
49
+
50
+ # Processors that should remain sequential in the core runtime.
51
+ #
52
+ # @return [Array<Processor>]
53
+ def sequential_processors
54
+ processors.reject(&:ractor_safe?).freeze
55
+ end
56
+
57
+ private
58
+
59
+ # Normalize processor return values into ProcessorResult objects.
60
+ #
61
+ # @param result [Object] raw processor result
62
+ # @param event [ChangeEvent] processed event
63
+ # @return [ProcessorResult]
64
+ def normalize_result(result, event)
65
+ return result if result.is_a?(ProcessorResult)
66
+
67
+ result ? ProcessorResult.success(event) : ProcessorResult.skipped(event)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDC
4
+ module Core
5
+ # Base error class for all cdc-core specific failures.
6
+ class Error < StandardError; end
7
+
8
+ # Raised when an operation cannot be normalized to a supported CDC action.
9
+ class InvalidOperationError < Error; end
10
+
11
+ # Raised by processors when a processor-specific failure needs wrapping.
12
+ class ProcessorError < Error; end
13
+ end
14
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDC
4
+ module Core
5
+ # Immutable metadata container for CDC domain objects.
6
+ #
7
+ # Metadata keys are normalized to frozen strings. Nested hashes and arrays
8
+ # are recursively converted into Ractor-shareable objects. Values that Ruby
9
+ # cannot make shareable are stored as frozen #inspect strings.
10
+ class EventMetadata
11
+ # @return [Hash{String=>Object}] normalized metadata
12
+ attr_reader :data
13
+
14
+ # Build metadata from a hash-like structure.
15
+ #
16
+ # @param data [Hash] metadata values
17
+ def initialize(data = {})
18
+ @data = deep_shareable_hash(data)
19
+ Ractor.make_shareable(self)
20
+ end
21
+
22
+ # Fetch a metadata value by string or symbol key.
23
+ #
24
+ # @param key [String, Symbol] metadata key
25
+ # @return [Object, nil]
26
+ def [](key)
27
+ data[key] || data[key.to_s] || data[key.to_sym]
28
+ end
29
+
30
+ # Return the normalized Ractor-shareable hash.
31
+ #
32
+ # @return [Hash{String=>Object}]
33
+ def to_h
34
+ data
35
+ end
36
+
37
+ private
38
+
39
+ # Recursively normalize and freeze a hash.
40
+ #
41
+ # @param hash [Hash]
42
+ # @return [Hash{String=>Object}]
43
+ def deep_shareable_hash(hash)
44
+ converted = hash.each_with_object(
45
+ {} # : Hash[String, untyped]
46
+ ) do |(key, value), memo|
47
+ memo[normalize_key(key)] = normalize_value(value)
48
+ end
49
+ Ractor.make_shareable(converted.freeze)
50
+ end
51
+
52
+ # Normalize metadata keys to frozen strings.
53
+ #
54
+ # @param key [Object]
55
+ # @return [String]
56
+ def normalize_key(key)
57
+ key.to_s.freeze
58
+ end
59
+
60
+ # Normalize a metadata value into a shareable representation.
61
+ #
62
+ # @param value [Object]
63
+ # @return [Object]
64
+ def normalize_value(value)
65
+ case value
66
+ when Hash
67
+ deep_shareable_hash(value)
68
+ when Array
69
+ Ractor.make_shareable(value.map { |item| normalize_value(item) }.freeze)
70
+ else
71
+ Ractor.make_shareable(value)
72
+ end
73
+ rescue Ractor::Error
74
+ Ractor.make_shareable(value.inspect.freeze)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDC
4
+ module Core
5
+ # Predicate object used to decide whether a pipeline should process an event.
6
+ #
7
+ # Filters are composable with #& and #|. A filter only matches when its
8
+ # predicate returns true exactly, keeping accidental truthy values from
9
+ # silently passing events through a pipeline.
10
+ class Filter
11
+ # Match every event.
12
+ #
13
+ # @return [Filter]
14
+ def self.all = new { |_event| true }
15
+
16
+ # Match events from a schema.
17
+ #
18
+ # @param name [#to_s] schema name
19
+ # @return [Filter]
20
+ def self.schema(name) = new { |event| event.schema == name.to_s }
21
+
22
+ # Match events from a table regardless of schema.
23
+ #
24
+ # @param name [#to_s] table name
25
+ # @return [Filter]
26
+ def self.table(name) = new { |event| event.table == name.to_s }
27
+
28
+ # Match events from a fully qualified schema.table name.
29
+ #
30
+ # @param name [#to_s] qualified table name
31
+ # @return [Filter]
32
+ def self.qualified_table(name) = new { |event| event.qualified_table_name == name.to_s }
33
+
34
+ # Match events by operation.
35
+ #
36
+ # @param operation [#to_sym] CDC operation
37
+ # @return [Filter]
38
+ def self.operation(operation) = new { |event| event.operation == Operation.normalize(operation) }
39
+
40
+ # Build a custom filter.
41
+ #
42
+ # @yieldparam event [ChangeEvent] event being tested
43
+ # @yieldreturn [Boolean] true to match the event
44
+ # @raise [ArgumentError] when no predicate block is provided
45
+ def initialize(&predicate)
46
+ raise ArgumentError, 'predicate block required' unless predicate
47
+
48
+ @predicate = predicate
49
+ end
50
+
51
+ # Whether this filter matches an event.
52
+ #
53
+ # @param event [ChangeEvent] event to test
54
+ # @return [Boolean]
55
+ def match?(event)
56
+ @predicate.call(event) == true
57
+ end
58
+ alias =~ match?
59
+
60
+ # Compose this filter with another filter using logical AND.
61
+ #
62
+ # @param other [Filter] other filter
63
+ # @return [Filter]
64
+ def &(other)
65
+ self.class.new { |event| match?(event) && other.match?(event) }
66
+ end
67
+
68
+ # Compose this filter with another filter using logical OR.
69
+ #
70
+ # @param other [Filter] other filter
71
+ # @return [Filter]
72
+ def |(other)
73
+ self.class.new { |event| match?(event) || other.match?(event) }
74
+ end
75
+ end
76
+ end
77
+ end