whodunit-chronicles 0.3.0 → 0.4.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 +22 -226
- data/LICENSE +1 -1
- data/README.md +96 -599
- data/exe/whodunit-chronicles +6 -0
- data/lib/whodunit/chronicles/chronicler.rb +62 -0
- data/lib/whodunit/chronicles/cli.rb +131 -0
- data/lib/whodunit/chronicles/errors.rb +7 -33
- data/lib/whodunit/chronicles/ledger.rb +69 -0
- data/lib/whodunit/chronicles/ledger_entry.rb +143 -0
- data/lib/whodunit/chronicles/ledger_factory.rb +66 -0
- data/lib/whodunit/chronicles/ledgers/file_ledger.rb +56 -0
- data/lib/whodunit/chronicles/ledgers/memory_ledger.rb +29 -0
- data/lib/whodunit/chronicles/ledgers/sqlite_ledger.rb +172 -0
- data/lib/whodunit/chronicles/version.rb +2 -1
- data/lib/whodunit/chronicles.rb +12 -65
- data/lib/whodunit-chronicles.rb +0 -1
- data/sig/whodunit/chronicles/chronicler.rbs +14 -0
- data/sig/whodunit/chronicles/cli.rbs +17 -0
- data/sig/whodunit/chronicles/errors.rbs +15 -0
- data/sig/whodunit/chronicles/ledger.rbs +13 -0
- data/sig/whodunit/chronicles/ledger_entry.rbs +62 -0
- data/sig/whodunit/chronicles/ledger_factory.rbs +14 -0
- data/sig/whodunit/chronicles/ledgers/file_ledger.rbs +14 -0
- data/sig/whodunit/chronicles/ledgers/memory_ledger.rbs +12 -0
- data/sig/whodunit/chronicles/ledgers/sqlite_ledger.rbs +30 -0
- data/sig/whodunit/chronicles.rbs +5 -0
- metadata +40 -326
- data/.codeclimate.yml +0 -50
- data/.rubocop.yml +0 -93
- data/.yardopts +0 -14
- data/CODE_OF_CONDUCT.md +0 -132
- data/CONTRIBUTING.md +0 -186
- data/Rakefile +0 -18
- data/docker/mysql/init.sql +0 -33
- data/docker/postgres/init.sql +0 -40
- data/docker-compose.yml +0 -138
- data/examples/images/campaign-performance-analytics.png +0 -0
- data/examples/images/candidate-journey-analytics.png +0 -0
- data/examples/images/recruitment-funnel-analytics.png +0 -0
- data/lib/.gitkeep +0 -0
- data/lib/whodunit/chronicles/adapter_loader.rb +0 -69
- data/lib/whodunit/chronicles/adapters/mysql.rb +0 -261
- data/lib/whodunit/chronicles/adapters/postgresql.rb +0 -278
- data/lib/whodunit/chronicles/change_event.rb +0 -201
- data/lib/whodunit/chronicles/composite_processor.rb +0 -86
- data/lib/whodunit/chronicles/configuration.rb +0 -112
- data/lib/whodunit/chronicles/connection.rb +0 -88
- data/lib/whodunit/chronicles/persistence.rb +0 -129
- data/lib/whodunit/chronicles/processor.rb +0 -127
- data/lib/whodunit/chronicles/service.rb +0 -207
- data/lib/whodunit/chronicles/stream_adapter.rb +0 -91
- data/lib/whodunit/chronicles/table.rb +0 -120
- data/whodunit-chronicles.gemspec +0 -79
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'trilogy'
|
|
4
|
-
require 'uri'
|
|
5
|
-
|
|
6
|
-
module Whodunit
|
|
7
|
-
module Chronicles
|
|
8
|
-
module Adapters
|
|
9
|
-
# MySQL/MariaDB binary log streaming adapter
|
|
10
|
-
#
|
|
11
|
-
# Uses MySQL's binary log replication to stream database changes
|
|
12
|
-
# without impacting application performance.
|
|
13
|
-
class MySQL < Chronicles::StreamAdapter
|
|
14
|
-
DEFAULT_SERVER_ID = 1001
|
|
15
|
-
|
|
16
|
-
attr_reader :connection, :database_url, :server_id, :binlog_file, :binlog_position
|
|
17
|
-
|
|
18
|
-
def initialize(
|
|
19
|
-
database_url: Chronicles.config.database_url,
|
|
20
|
-
server_id: DEFAULT_SERVER_ID,
|
|
21
|
-
logger: Chronicles.logger
|
|
22
|
-
)
|
|
23
|
-
super(logger: logger)
|
|
24
|
-
@database_url = database_url
|
|
25
|
-
@server_id = server_id
|
|
26
|
-
@connection = nil
|
|
27
|
-
@binlog_file = nil
|
|
28
|
-
@binlog_position = nil
|
|
29
|
-
@binlog_checksum = true
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Start streaming binary log changes
|
|
33
|
-
def start_streaming(&)
|
|
34
|
-
raise ArgumentError, 'Block required for processing events' unless block_given?
|
|
35
|
-
|
|
36
|
-
log(:info, 'Starting MySQL binary log streaming')
|
|
37
|
-
|
|
38
|
-
establish_connection
|
|
39
|
-
ensure_setup
|
|
40
|
-
|
|
41
|
-
self.running = true
|
|
42
|
-
fetch_current_position
|
|
43
|
-
|
|
44
|
-
log(:info, 'Starting replication from position',
|
|
45
|
-
file: @binlog_file, position: @binlog_position)
|
|
46
|
-
|
|
47
|
-
begin
|
|
48
|
-
stream_binlog_events(&)
|
|
49
|
-
rescue StandardError => e
|
|
50
|
-
log(:error, 'Streaming error', error: e.message, backtrace: e.backtrace.first(5))
|
|
51
|
-
raise ReplicationError, "Failed to stream changes: #{e.message}"
|
|
52
|
-
ensure
|
|
53
|
-
self.running = false
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Stop streaming
|
|
58
|
-
def stop_streaming
|
|
59
|
-
log(:info, 'Stopping MySQL binary log streaming')
|
|
60
|
-
self.running = false
|
|
61
|
-
close_connection
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Get current replication position
|
|
65
|
-
def current_position
|
|
66
|
-
return "#{@binlog_file}:#{@binlog_position}" if @binlog_file && @binlog_position
|
|
67
|
-
|
|
68
|
-
fetch_current_position
|
|
69
|
-
"#{@binlog_file}:#{@binlog_position}"
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Set up binary log replication
|
|
73
|
-
def setup
|
|
74
|
-
log(:info, 'Setting up MySQL binary log replication')
|
|
75
|
-
|
|
76
|
-
establish_connection
|
|
77
|
-
validate_binlog_format
|
|
78
|
-
validate_server_id
|
|
79
|
-
enable_binlog_checksum
|
|
80
|
-
|
|
81
|
-
log(:info, 'MySQL setup completed successfully')
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Remove binary log replication setup (minimal cleanup needed)
|
|
85
|
-
def teardown
|
|
86
|
-
log(:info, 'Tearing down MySQL binary log replication')
|
|
87
|
-
close_connection
|
|
88
|
-
log(:info, 'MySQL teardown completed')
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Test database connection
|
|
92
|
-
def test_connection
|
|
93
|
-
establish_connection
|
|
94
|
-
result = @connection.query('SELECT @@hostname, @@version, @@server_id')
|
|
95
|
-
info = result.first
|
|
96
|
-
|
|
97
|
-
log(:info, 'Connection test successful',
|
|
98
|
-
hostname: info['@@hostname'],
|
|
99
|
-
version: info['@@version'],
|
|
100
|
-
server_id: info['@@server_id'])
|
|
101
|
-
|
|
102
|
-
true
|
|
103
|
-
rescue StandardError => e
|
|
104
|
-
log(:error, 'Connection test failed', error: e.message)
|
|
105
|
-
false
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
private
|
|
109
|
-
|
|
110
|
-
def establish_connection
|
|
111
|
-
return if @connection&.ping
|
|
112
|
-
|
|
113
|
-
parsed_url = parse_database_url(@database_url)
|
|
114
|
-
|
|
115
|
-
@connection = Trilogy.new(
|
|
116
|
-
host: parsed_url[:host],
|
|
117
|
-
port: parsed_url[:port] || 3306,
|
|
118
|
-
username: parsed_url[:username],
|
|
119
|
-
password: parsed_url[:password],
|
|
120
|
-
database: parsed_url[:database],
|
|
121
|
-
ssl: parsed_url[:ssl],
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
log(:debug, 'Established MySQL connection',
|
|
125
|
-
host: parsed_url[:host],
|
|
126
|
-
database: parsed_url[:database])
|
|
127
|
-
rescue StandardError => e
|
|
128
|
-
log(:error, 'Failed to establish connection', error: e.message)
|
|
129
|
-
raise AdapterLoadError, "Connection failed: #{e.message}"
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def close_connection
|
|
133
|
-
@connection&.close
|
|
134
|
-
@connection = nil
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def parse_database_url(url)
|
|
138
|
-
uri = URI.parse(url)
|
|
139
|
-
{
|
|
140
|
-
host: uri.host,
|
|
141
|
-
port: uri.port,
|
|
142
|
-
username: uri.user,
|
|
143
|
-
password: uri.password,
|
|
144
|
-
database: uri.path&.sub('/', ''),
|
|
145
|
-
ssl: uri.query&.include?('ssl=true'),
|
|
146
|
-
}
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def ensure_setup
|
|
150
|
-
validate_binlog_format
|
|
151
|
-
validate_server_id
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def validate_binlog_format
|
|
155
|
-
result = @connection.query('SELECT @@binlog_format')
|
|
156
|
-
format = result.first['@@binlog_format']
|
|
157
|
-
|
|
158
|
-
unless %w[ROW MIXED].include?(format)
|
|
159
|
-
raise ReplicationError,
|
|
160
|
-
"Binary log format must be ROW or MIXED, currently: #{format}"
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
log(:debug, 'Binary log format validated', format: format)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def validate_server_id
|
|
167
|
-
result = @connection.query('SELECT @@server_id')
|
|
168
|
-
current_server_id = result.first['@@server_id'].to_i
|
|
169
|
-
|
|
170
|
-
if current_server_id == @server_id
|
|
171
|
-
raise ReplicationError,
|
|
172
|
-
"Server ID conflict: #{@server_id} is already in use"
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
log(:debug, 'Server ID validated',
|
|
176
|
-
current: current_server_id,
|
|
177
|
-
replication: @server_id)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def enable_binlog_checksum
|
|
181
|
-
@connection.query('SET @master_binlog_checksum = @@global.binlog_checksum')
|
|
182
|
-
log(:debug, 'Binary log checksum enabled')
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def fetch_current_position
|
|
186
|
-
result = @connection.query('SHOW MASTER STATUS')
|
|
187
|
-
status = result.first
|
|
188
|
-
|
|
189
|
-
raise ReplicationError, 'Unable to fetch master status - binary logging may be disabled' unless status
|
|
190
|
-
|
|
191
|
-
@binlog_file = status['File']
|
|
192
|
-
@binlog_position = status['Position']
|
|
193
|
-
log(:debug, 'Fetched master position',
|
|
194
|
-
file: @binlog_file,
|
|
195
|
-
position: @binlog_position)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def stream_binlog_events(&)
|
|
199
|
-
# Register as replica server
|
|
200
|
-
register_replica_server
|
|
201
|
-
|
|
202
|
-
# Request binary log dump
|
|
203
|
-
request_binlog_dump
|
|
204
|
-
|
|
205
|
-
# Process binary log events
|
|
206
|
-
process_binlog_stream(&)
|
|
207
|
-
rescue StandardError => e
|
|
208
|
-
log(:error, 'Binary log streaming error', error: e.message)
|
|
209
|
-
raise
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def register_replica_server
|
|
213
|
-
# This would typically use COM_REGISTER_SLAVE MySQL protocol command
|
|
214
|
-
# For now, we'll use a simplified approach
|
|
215
|
-
log(:debug, 'Registering as replica server', server_id: @server_id)
|
|
216
|
-
|
|
217
|
-
# NOTE: Full implementation would require low-level MySQL protocol handling
|
|
218
|
-
# This is a placeholder for the binary log streaming setup
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def request_binlog_dump
|
|
222
|
-
log(:debug, 'Requesting binary log dump',
|
|
223
|
-
file: @binlog_file,
|
|
224
|
-
position: @binlog_position)
|
|
225
|
-
|
|
226
|
-
# This would use COM_BINLOG_DUMP MySQL protocol command
|
|
227
|
-
# Full implementation requires binary protocol handling
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def process_binlog_stream(&)
|
|
231
|
-
# This would process the binary log event stream
|
|
232
|
-
# Each event would be parsed and converted to a ChangeEvent
|
|
233
|
-
|
|
234
|
-
log(:info, 'Processing binary log stream (placeholder implementation)')
|
|
235
|
-
|
|
236
|
-
# Placeholder: In a real implementation, this would:
|
|
237
|
-
# 1. Read binary log events from the stream
|
|
238
|
-
# 2. Parse event headers and data
|
|
239
|
-
# 3. Convert to ChangeEvent objects
|
|
240
|
-
# 4. Yield each event to the block
|
|
241
|
-
|
|
242
|
-
# For now, we'll simulate with a warning
|
|
243
|
-
log(:warn, 'MySQL binary log streaming requires full protocol implementation')
|
|
244
|
-
|
|
245
|
-
# Yield a placeholder change event to demonstrate the interface
|
|
246
|
-
change_event = ChangeEvent.new(
|
|
247
|
-
table_name: 'example_table',
|
|
248
|
-
action: 'INSERT',
|
|
249
|
-
primary_key: { id: 1 },
|
|
250
|
-
new_data: { id: 1, name: 'test' },
|
|
251
|
-
old_data: nil,
|
|
252
|
-
timestamp: Time.now,
|
|
253
|
-
metadata: { position: current_position },
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
yield(change_event) if block_given?
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
end
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'pg'
|
|
4
|
-
|
|
5
|
-
module Whodunit
|
|
6
|
-
module Chronicles
|
|
7
|
-
module Adapters
|
|
8
|
-
# PostgreSQL logical replication adapter
|
|
9
|
-
#
|
|
10
|
-
# Uses PostgreSQL's logical replication functionality to stream
|
|
11
|
-
# database changes via WAL decoding without impacting application performance.
|
|
12
|
-
class PostgreSQL < Chronicles::StreamAdapter
|
|
13
|
-
DEFAULT_PLUGIN = 'pgoutput'
|
|
14
|
-
|
|
15
|
-
attr_reader :connection, :replication_connection, :publication_name, :slot_name
|
|
16
|
-
|
|
17
|
-
def initialize(
|
|
18
|
-
database_url: Chronicles.config.database_url,
|
|
19
|
-
publication_name: Chronicles.config.publication_name,
|
|
20
|
-
slot_name: Chronicles.config.replication_slot_name,
|
|
21
|
-
logger: Chronicles.logger
|
|
22
|
-
)
|
|
23
|
-
super(logger: logger)
|
|
24
|
-
@database_url = database_url
|
|
25
|
-
@publication_name = publication_name
|
|
26
|
-
@slot_name = slot_name
|
|
27
|
-
@connection = nil
|
|
28
|
-
@replication_connection = nil
|
|
29
|
-
@last_lsn = nil
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Start streaming logical replication changes
|
|
33
|
-
def start_streaming(&)
|
|
34
|
-
raise ArgumentError, 'Block required for processing events' unless block_given?
|
|
35
|
-
|
|
36
|
-
log(:info, 'Starting PostgreSQL logical replication streaming')
|
|
37
|
-
|
|
38
|
-
establish_connections
|
|
39
|
-
ensure_setup
|
|
40
|
-
|
|
41
|
-
self.running = true
|
|
42
|
-
self.position = confirmed_flush_lsn || '0/0'
|
|
43
|
-
|
|
44
|
-
log(:info, 'Starting replication from LSN', lsn: @position)
|
|
45
|
-
|
|
46
|
-
begin
|
|
47
|
-
stream_changes(&)
|
|
48
|
-
rescue StandardError => e
|
|
49
|
-
log(:error, 'Streaming error', error: e.message, backtrace: e.backtrace.first(5))
|
|
50
|
-
raise ReplicationError, "Failed to stream changes: #{e.message}"
|
|
51
|
-
ensure
|
|
52
|
-
self.running = false
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Stop streaming
|
|
57
|
-
def stop_streaming
|
|
58
|
-
log(:info, 'Stopping PostgreSQL logical replication streaming')
|
|
59
|
-
self.running = false
|
|
60
|
-
close_connections
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Get current replication position
|
|
64
|
-
def current_position
|
|
65
|
-
@last_lsn || confirmed_flush_lsn
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Set up logical replication (publication and slot)
|
|
69
|
-
def setup
|
|
70
|
-
log(:info, 'Setting up PostgreSQL logical replication')
|
|
71
|
-
|
|
72
|
-
establish_connection
|
|
73
|
-
create_publication
|
|
74
|
-
create_replication_slot
|
|
75
|
-
|
|
76
|
-
log(:info, 'PostgreSQL setup completed successfully')
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Remove logical replication setup
|
|
80
|
-
def teardown
|
|
81
|
-
log(:info, 'Tearing down PostgreSQL logical replication')
|
|
82
|
-
|
|
83
|
-
establish_connection
|
|
84
|
-
drop_replication_slot
|
|
85
|
-
drop_publication
|
|
86
|
-
|
|
87
|
-
log(:info, 'PostgreSQL teardown completed')
|
|
88
|
-
ensure
|
|
89
|
-
close_connections
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Test database connection
|
|
93
|
-
def test_connection
|
|
94
|
-
establish_connection
|
|
95
|
-
result = @connection.exec('SELECT current_database(), current_user, version()')
|
|
96
|
-
db_info = result.first
|
|
97
|
-
|
|
98
|
-
log(:info, 'Connection test successful',
|
|
99
|
-
database: db_info['current_database'],
|
|
100
|
-
user: db_info['current_user'],
|
|
101
|
-
version: db_info['version'])
|
|
102
|
-
|
|
103
|
-
true
|
|
104
|
-
rescue PG::Error => e
|
|
105
|
-
log(:error, 'Connection test failed', error: e.message)
|
|
106
|
-
false
|
|
107
|
-
ensure
|
|
108
|
-
result&.clear
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
def establish_connections
|
|
114
|
-
establish_connection
|
|
115
|
-
establish_replication_connection
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def establish_connection
|
|
119
|
-
return if @connection && !@connection.finished?
|
|
120
|
-
|
|
121
|
-
@connection = PG.connect(@database_url)
|
|
122
|
-
@connection.type_map_for_results = PG::BasicTypeMapForResults.new(@connection)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def establish_replication_connection
|
|
126
|
-
return if @replication_connection && !@replication_connection.finished?
|
|
127
|
-
|
|
128
|
-
# Parse connection URL and add replication parameter
|
|
129
|
-
uri = URI.parse(@database_url)
|
|
130
|
-
repl_params = URI.decode_www_form(uri.query || '')
|
|
131
|
-
repl_params << %w[replication database]
|
|
132
|
-
uri.query = URI.encode_www_form(repl_params)
|
|
133
|
-
|
|
134
|
-
@replication_connection = PG.connect(uri.to_s)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def close_connections
|
|
138
|
-
@connection&.close
|
|
139
|
-
@replication_connection&.close
|
|
140
|
-
@connection = nil
|
|
141
|
-
@replication_connection = nil
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def ensure_setup
|
|
145
|
-
unless publication_exists?
|
|
146
|
-
raise ReplicationError, "Publication '#{publication_name}' does not exist. Run #setup first."
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
return if replication_slot_exists?
|
|
150
|
-
|
|
151
|
-
raise ReplicationError, "Replication slot '#{slot_name}' does not exist. Run #setup first."
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def stream_changes(&)
|
|
155
|
-
copy_sql = build_copy_statement
|
|
156
|
-
log(:debug, 'Starting COPY command', sql: copy_sql)
|
|
157
|
-
|
|
158
|
-
@replication_connection.exec(copy_sql)
|
|
159
|
-
|
|
160
|
-
while running?
|
|
161
|
-
data = @replication_connection.get_copy_data(async: false)
|
|
162
|
-
break unless data
|
|
163
|
-
|
|
164
|
-
process_wal_data(data, &)
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def build_copy_statement
|
|
169
|
-
options = [
|
|
170
|
-
"proto_version '1'",
|
|
171
|
-
"publication_names '#{publication_name}'",
|
|
172
|
-
].join(', ')
|
|
173
|
-
|
|
174
|
-
"COPY (SELECT * FROM pg_logical_slot_get_changes('#{slot_name}', NULL, NULL, #{options})) TO STDOUT"
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def process_wal_data(data)
|
|
178
|
-
# Parse pgoutput protocol message
|
|
179
|
-
# This is a simplified version - full implementation would need
|
|
180
|
-
# to properly decode the binary protocol
|
|
181
|
-
log(:debug, 'Processing WAL data', size: data.bytesize)
|
|
182
|
-
|
|
183
|
-
# For now, we'll parse text-based logical decoding output
|
|
184
|
-
# In production, this should parse the binary pgoutput format
|
|
185
|
-
change_event = parse_logical_message(data)
|
|
186
|
-
yield(change_event) if change_event
|
|
187
|
-
rescue StandardError => e
|
|
188
|
-
log(:error, 'Error processing WAL data', error: e.message, data: data.inspect)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def parse_logical_message(data)
|
|
192
|
-
# Simplified parser for demonstration
|
|
193
|
-
# Real implementation would parse pgoutput binary protocol
|
|
194
|
-
lines = data.strip.split("\n")
|
|
195
|
-
return unless lines.any?
|
|
196
|
-
|
|
197
|
-
# This is a placeholder - would need full pgoutput protocol parsing
|
|
198
|
-
log(:debug, 'Parsed logical message', lines: lines.size)
|
|
199
|
-
nil
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def create_publication
|
|
203
|
-
if publication_exists?
|
|
204
|
-
log(:info, 'Publication already exists', name: publication_name)
|
|
205
|
-
return
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
sql = "CREATE PUBLICATION #{publication_name} FOR ALL TABLES"
|
|
209
|
-
@connection.exec(sql)
|
|
210
|
-
log(:info, 'Created publication', name: publication_name)
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def drop_publication
|
|
214
|
-
return unless publication_exists?
|
|
215
|
-
|
|
216
|
-
sql = "DROP PUBLICATION IF EXISTS #{publication_name}"
|
|
217
|
-
@connection.exec(sql)
|
|
218
|
-
log(:info, 'Dropped publication', name: publication_name)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def create_replication_slot
|
|
222
|
-
if replication_slot_exists?
|
|
223
|
-
log(:info, 'Replication slot already exists', name: slot_name)
|
|
224
|
-
return
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
sql = "SELECT pg_create_logical_replication_slot('#{slot_name}', '#{DEFAULT_PLUGIN}')"
|
|
228
|
-
result = @connection.exec(sql)
|
|
229
|
-
slot_info = result.first
|
|
230
|
-
|
|
231
|
-
log(:info, 'Created replication slot',
|
|
232
|
-
name: slot_name,
|
|
233
|
-
lsn: slot_info['lsn'])
|
|
234
|
-
ensure
|
|
235
|
-
result&.clear
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def drop_replication_slot
|
|
239
|
-
return unless replication_slot_exists?
|
|
240
|
-
|
|
241
|
-
sql = "SELECT pg_drop_replication_slot('#{slot_name}')"
|
|
242
|
-
@connection.exec(sql)
|
|
243
|
-
log(:info, 'Dropped replication slot', name: slot_name)
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def publication_exists?
|
|
247
|
-
sql = 'SELECT 1 FROM pg_publication WHERE pubname = $1'
|
|
248
|
-
result = @connection.exec_params(sql, [publication_name])
|
|
249
|
-
exists = result.ntuples.positive?
|
|
250
|
-
result.clear
|
|
251
|
-
exists
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def replication_slot_exists?
|
|
255
|
-
sql = 'SELECT 1 FROM pg_replication_slots WHERE slot_name = $1'
|
|
256
|
-
result = @connection.exec_params(sql, [slot_name])
|
|
257
|
-
exists = result.ntuples.positive?
|
|
258
|
-
result.clear
|
|
259
|
-
exists
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def confirmed_flush_lsn
|
|
263
|
-
sql = 'SELECT confirmed_flush_lsn FROM pg_replication_slots WHERE slot_name = $1'
|
|
264
|
-
result = @connection.exec_params(sql, [slot_name])
|
|
265
|
-
|
|
266
|
-
if result.ntuples.positive?
|
|
267
|
-
lsn = result.first['confirmed_flush_lsn']
|
|
268
|
-
result.clear
|
|
269
|
-
lsn
|
|
270
|
-
else
|
|
271
|
-
result.clear
|
|
272
|
-
nil
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
end
|