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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 936740af0935d80d35c3d1c4cc92bde29c6a7df587cb0c25d16b7183bdba0009
4
- data.tar.gz: 80b759a323c4e9617c2e0c0258dcf48f51e25582b8238a4b58bf3b00b31cf1de
3
+ metadata.gz: 25baa32e6f2d21ec2e2e4d2f1d1e6cbfa923b50bbb83aef360e3915548ad294f
4
+ data.tar.gz: 70c3b57c2256c7313893c3333e18f7c0e0f3d10bc9c976fe93629779312b32e2
5
5
  SHA512:
6
- metadata.gz: 743d33f09a35783ef50e398597aa53ac159125873874a952b26b0995f8192ae76b950d23ed1d98b97e4c7473b15fe6fc412133a449d0cfd79b1d93982088a5e1
7
- data.tar.gz: 466a0008e372b725aa5723e3a46af578cb6603e816a4bd4e9e0dbaabfbc6a0ca9bcf72e3d69661e1e083d815940747993e4dbd6eca09da8b17b80d6e65d0ed49
6
+ metadata.gz: 00e5fd5953df779aaaead0554987a0441c2a441c0be987489e4b1e50c4e838b3bef659c7168a64ed1a1f9e42c003169621c925e2c77e9631ec75d8729c3238c2
7
+ data.tar.gz: 2ab200a8bcfcc61ca06baf60fc0d5d2a43af8b2660a5b8713a428016b403d47298ad11354f02d4fe650346f9de2b8a4078ba47bce99135de69f0474ea9b9e9e1
@@ -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.bigint :primary_key
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 primary_key]
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
- case @table.where(table_name: event.full_table_name, primary_key: event.primary_key).pluck(:action, :old).first
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, primary_key: event.primary_key).delete_all
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
- primary_key: event.primary_key,
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.primary_key,
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
@@ -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
- # TODO: for now we are forcing an id column here, but that is not really correct
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, :primary_key_colums, :columns)
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
- case primary_key_colums
349
- in [key]
350
- case decoded_row[key]
351
- in Integer => id
352
- id
353
- in String => id
354
- id
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wal
4
- VERSION = "0.0.27"
4
+ VERSION = "0.0.28"
5
5
  end
data/rbi/wal.rbi CHANGED
@@ -7,7 +7,7 @@ module Wal
7
7
  UpdateEvent,
8
8
  DeleteEvent,
9
9
  ) }
10
- VERSION = "0.0.27"
10
+ VERSION = "0.0.28"
11
11
 
12
12
  class << self
13
13
  sig { returns(T.class_of(Logger)) }
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.27
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-14 00:00:00.000000000 Z
10
+ date: 2026-01-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: pg