wal 0.0.27 → 0.0.28
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/lib/wal/record_watcher.rb +18 -6
- data/lib/wal/replicator.rb +64 -14
- data/lib/wal/version.rb +1 -1
- data/rbi/wal.rbi +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25baa32e6f2d21ec2e2e4d2f1d1e6cbfa923b50bbb83aef360e3915548ad294f
|
|
4
|
+
data.tar.gz: 70c3b57c2256c7313893c3333e18f7c0e0f3d10bc9c976fe93629779312b32e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 00e5fd5953df779aaaead0554987a0441c2a441c0be987489e4b1e50c4e838b3bef659c7168a64ed1a1f9e42c003169621c925e2c77e9631ec75d8729c3238c2
|
|
7
|
+
data.tar.gz: 2ab200a8bcfcc61ca06baf60fc0d5d2a43af8b2660a5b8713a428016b403d47298ad11354f02d4fe650346f9de2b8a4078ba47bce99135de69f0474ea9b9e9e1
|
data/lib/wal/record_watcher.rb
CHANGED
|
@@ -244,13 +244,13 @@ module Wal
|
|
|
244
244
|
t.bigint :lsn, null: false
|
|
245
245
|
t.column :action, :string, null: false
|
|
246
246
|
t.string :table_name, null: false
|
|
247
|
-
t.
|
|
247
|
+
t.text :primary_key_data
|
|
248
248
|
t.jsonb :old, null: false, default: {}
|
|
249
249
|
t.jsonb :new, null: false, default: {}
|
|
250
250
|
t.jsonb :context, null: false, default: {}
|
|
251
251
|
end
|
|
252
252
|
|
|
253
|
-
unique_index = %i[table_name
|
|
253
|
+
unique_index = %i[table_name primary_key_data]
|
|
254
254
|
|
|
255
255
|
base_class.connection.add_index table_name, unique_index, unique: true
|
|
256
256
|
|
|
@@ -303,9 +303,10 @@ module Wal
|
|
|
303
303
|
end
|
|
304
304
|
|
|
305
305
|
def on_delete(event)
|
|
306
|
-
|
|
306
|
+
pk_data = serialize_primary_key(event.primary_key)
|
|
307
|
+
case @table.where(table_name: event.full_table_name, primary_key_data: pk_data).pluck(:action, :old).first
|
|
307
308
|
in ["insert", _]
|
|
308
|
-
@table.where(table_name: event.full_table_name,
|
|
309
|
+
@table.where(table_name: event.full_table_name, primary_key_data: pk_data).delete_all
|
|
309
310
|
in ["update", old]
|
|
310
311
|
@table.upsert(serialize(event).merge(old:))
|
|
311
312
|
in ["delete", _]
|
|
@@ -321,12 +322,23 @@ module Wal
|
|
|
321
322
|
self.class.base_active_record_class || ::ActiveRecord::Base
|
|
322
323
|
end
|
|
323
324
|
|
|
325
|
+
def serialize_primary_key(pk)
|
|
326
|
+
return nil if pk.nil?
|
|
327
|
+
Array(pk).to_json
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def deserialize_primary_key(pk_data)
|
|
331
|
+
return nil if pk_data.nil?
|
|
332
|
+
values = JSON.parse(pk_data)
|
|
333
|
+
values.size == 1 ? values.first : values
|
|
334
|
+
end
|
|
335
|
+
|
|
324
336
|
def serialize(event)
|
|
325
337
|
serialized = {
|
|
326
338
|
transaction_id: event.transaction_id,
|
|
327
339
|
lsn: event.lsn,
|
|
328
340
|
table_name: event.full_table_name,
|
|
329
|
-
|
|
341
|
+
primary_key_data: serialize_primary_key(event.primary_key),
|
|
330
342
|
context: event.context,
|
|
331
343
|
}
|
|
332
344
|
case event
|
|
@@ -348,7 +360,7 @@ module Wal
|
|
|
348
360
|
lsn: persisted_event.lsn,
|
|
349
361
|
schema: schema || "public",
|
|
350
362
|
table: table,
|
|
351
|
-
primary_key: persisted_event.
|
|
363
|
+
primary_key: deserialize_primary_key(persisted_event.primary_key_data),
|
|
352
364
|
context: persisted_event.context,
|
|
353
365
|
}
|
|
354
366
|
case persisted_event.action
|
data/lib/wal/replicator.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Wal
|
|
|
12
12
|
@db_config = db_config
|
|
13
13
|
@replication_slot = replication_slot
|
|
14
14
|
@use_temporary_slot = use_temporary_slot
|
|
15
|
+
@primary_key_cache = {}
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def replicate_forever(watcher, publications:)
|
|
@@ -25,6 +26,8 @@ module Wal
|
|
|
25
26
|
@watch_conn&.stop_replication
|
|
26
27
|
@watch_conn&.close
|
|
27
28
|
@watch_conn = nil
|
|
29
|
+
@metadata_conn&.close
|
|
30
|
+
@metadata_conn = nil
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
def replicate(watcher, publications:)
|
|
@@ -37,6 +40,8 @@ module Wal
|
|
|
37
40
|
replication: "database",
|
|
38
41
|
)
|
|
39
42
|
|
|
43
|
+
@metadata_conn = connect_metadata
|
|
44
|
+
|
|
40
45
|
begin
|
|
41
46
|
@watch_conn.query(<<~SQL)
|
|
42
47
|
CREATE_REPLICATION_SLOT #{@replication_slot} #{@use_temporary_slot ? "TEMPORARY" : ""} LOGICAL "pgoutput"
|
|
@@ -53,8 +58,7 @@ module Wal
|
|
|
53
58
|
case msg
|
|
54
59
|
in XLogData(data: PG::Replication::PGOutput::Relation(oid:, name:, columns:, namespace:))
|
|
55
60
|
tables[oid] = Table.new(
|
|
56
|
-
|
|
57
|
-
primary_key_colums: columns.any? { |col| col.name == "id" } ? ["id"] : [],
|
|
61
|
+
primary_key_columns: fetch_primary_key_columns(namespace, name),
|
|
58
62
|
schema: namespace,
|
|
59
63
|
name:,
|
|
60
64
|
columns: columns.map { |col| Column.new(oid: col.oid, name: col.name) },
|
|
@@ -156,6 +160,52 @@ module Wal
|
|
|
156
160
|
end
|
|
157
161
|
end
|
|
158
162
|
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def connect_metadata
|
|
166
|
+
PG.connect(
|
|
167
|
+
dbname: @db_config[:database],
|
|
168
|
+
host: @db_config[:host],
|
|
169
|
+
user: @db_config[:username],
|
|
170
|
+
password: @db_config[:password].presence,
|
|
171
|
+
port: @db_config[:port].presence,
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def fetch_primary_key_columns(schema, table_name)
|
|
176
|
+
result = @metadata_conn.exec_params(<<~SQL, [schema, table_name]).to_a.map { |row| row["attname"] }
|
|
177
|
+
SELECT a.attname
|
|
178
|
+
FROM pg_constraint c
|
|
179
|
+
JOIN pg_class t ON c.conrelid = t.oid
|
|
180
|
+
JOIN pg_namespace n ON t.relnamespace = n.oid
|
|
181
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(c.conkey)
|
|
182
|
+
WHERE n.nspname = $1 AND t.relname = $2 AND c.contype = 'p'
|
|
183
|
+
ORDER BY array_position(c.conkey, a.attnum)
|
|
184
|
+
SQL
|
|
185
|
+
|
|
186
|
+
return result if result.size > 0
|
|
187
|
+
|
|
188
|
+
# Fallback to unique index columns
|
|
189
|
+
result = @metadata_conn.exec_params(<<~SQL, [schema, table_name]).to_a
|
|
190
|
+
SELECT a.attname, i.indexrelid::bigint
|
|
191
|
+
FROM pg_index i
|
|
192
|
+
JOIN pg_class t ON i.indrelid = t.oid
|
|
193
|
+
JOIN pg_namespace n ON t.relnamespace = n.oid
|
|
194
|
+
JOIN unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord) ON true
|
|
195
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
196
|
+
WHERE n.nspname = $1 AND t.relname = $2 AND i.indisunique = true
|
|
197
|
+
ORDER BY i.indisprimary DESC, i.indexrelid, k.ord
|
|
198
|
+
SQL
|
|
199
|
+
|
|
200
|
+
result
|
|
201
|
+
.filter { |row| row["indexrelid"] == result.first["indexrelid"] }
|
|
202
|
+
.map { |row| row["attname"] } if result.size > 0
|
|
203
|
+
|
|
204
|
+
rescue PG::ConnectionBad
|
|
205
|
+
@metadata_conn = connect_metadata
|
|
206
|
+
retry
|
|
207
|
+
end
|
|
208
|
+
|
|
159
209
|
class Column < Data.define(:name, :oid)
|
|
160
210
|
BOOLEAN_DECODER = PG::TextDecoder::Boolean.new
|
|
161
211
|
BYTEA_DECODER = PG::TextDecoder::Bytea.new
|
|
@@ -334,7 +384,7 @@ module Wal
|
|
|
334
384
|
end
|
|
335
385
|
end
|
|
336
386
|
|
|
337
|
-
class Table < Data.define(:schema, :name, :
|
|
387
|
+
class Table < Data.define(:schema, :name, :primary_key_columns, :columns)
|
|
338
388
|
def full_table_name
|
|
339
389
|
case schema
|
|
340
390
|
in "public"
|
|
@@ -345,21 +395,21 @@ module Wal
|
|
|
345
395
|
end
|
|
346
396
|
|
|
347
397
|
def primary_key(decoded_row)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
398
|
+
return nil if primary_key_columns.empty?
|
|
399
|
+
|
|
400
|
+
values = primary_key_columns.filter_map do |col_name|
|
|
401
|
+
value = decoded_row[col_name]
|
|
402
|
+
case value
|
|
403
|
+
when Integer, String
|
|
404
|
+
value
|
|
355
405
|
else
|
|
356
|
-
# Only supporting string and integer primary keys for now
|
|
357
406
|
nil
|
|
358
407
|
end
|
|
359
|
-
else
|
|
360
|
-
# Not supporting coumpound primary keys
|
|
361
|
-
nil
|
|
362
408
|
end
|
|
409
|
+
|
|
410
|
+
return nil if values.size != primary_key_columns.size
|
|
411
|
+
|
|
412
|
+
values.size == 1 ? values.first : values
|
|
363
413
|
end
|
|
364
414
|
|
|
365
415
|
def decode_row(values)
|
data/lib/wal/version.rb
CHANGED
data/rbi/wal.rbi
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wal
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.28
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rodrigo Navarro
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-01-
|
|
10
|
+
date: 2026-01-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: pg
|