waldit 0.0.4 → 0.0.6

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: 81634f3ce5d00503e50882a14495a960dfc8fb1072b53c92e3cd2a08441666c9
4
- data.tar.gz: b235c7ee87fda30c747db3a678a3b9f6c7b5f5b95861f998b037a63418bee29b
3
+ metadata.gz: e8e8ecb8b8fc2c3c4b855fb06b204d858b7cfbf24f12dc016b4c3f5ca10849a2
4
+ data.tar.gz: b72218c4df4b396e497f1aa9d96fd159305fabb7faf0d481392044536da5141a
5
5
  SHA512:
6
- metadata.gz: ad16ce87f467600879c9358540143cb1ad4eeeb1fe278472647aad21e28db4420a5238b13e6d70c02a65014d72251d54acfdfb0fe6c448191644871a28d4ef45
7
- data.tar.gz: e932e94f85304d7b8f756a5fce3a3ba0e2c07ca302766e0f8ff3563b4acac9e14b7943ae0bcc3030ee68567571315e635fa172c02c945e70d68e45c46fba4d41
6
+ metadata.gz: 5471e11fa41cb7c3576dba2e0c1095ee3b2a7b7fd4eda88cd46e10523cf54ecdcda5a2d3470fdb9e97f3aebc066ce84bba7aa63be8507a0f0e37927b6604520f
7
+ data.tar.gz: 11dd782e95115cb77e8df0bed87f4dd8422454f5ef6ce84c35b9fe39af931c9855b37b07685cb3833ace964757c7d81b3d64a55a23330b52177ada7b60dd7ea8
data/lib/waldit/record.rb CHANGED
@@ -2,8 +2,22 @@
2
2
 
3
3
  module Waldit
4
4
  module Record
5
+ def new
6
+ return self[:new] if self[:new]
7
+
8
+ (self[:diff] || {}).transform_values { |_old, new| new }
9
+ end
10
+
11
+ def old
12
+ return self[:old] if self[:old]
13
+
14
+ (self[:diff] || {}).transform_values { |old, _new| old }
15
+ end
16
+
5
17
  def diff
6
- (old.keys | new.keys).reduce({}.with_indifferent_access) do |diff, key|
18
+ return self[:diff] if self[:diff]
19
+
20
+ (old.keys | new.keys).reduce({}) do |diff, key|
7
21
  old[key] != new[key] ? diff.merge(key => [old[key], new[key]]) : diff
8
22
  end
9
23
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Waldit
4
- VERSION = "0.0.4"
4
+ VERSION = "0.0.6"
5
5
  end
@@ -9,85 +9,86 @@ module Waldit
9
9
  def audit_event(event)
10
10
  return unless event.primary_key
11
11
 
12
- audit = {
13
- transaction_id: event.transaction_id,
14
- lsn: event.lsn,
15
- context: event.context,
16
- table_name: event.table,
17
- primary_key: event.primary_key,
18
- }
19
-
20
- unique_by = %i[table_name primary_key transaction_id]
12
+ audit = [event.transaction_id, event.lsn, event.table, event.primary_key, event.context.to_json]
21
13
 
22
14
  case event
23
15
  when InsertEvent
24
- record.upsert(
25
- audit.merge(action: "insert", new: event.new),
26
- unique_by:,
27
- on_duplicate: :update,
28
- )
16
+ @connection.exec_prepared("waldit_insert", audit + [event.new.to_json])
29
17
 
30
18
  when UpdateEvent
31
19
  return if event.diff.without(ignored_columns(event.table)).empty?
32
- record.upsert(
33
- audit.merge(action: "update", old: event.old, new: event.new),
34
- unique_by:,
35
- on_duplicate: :update,
36
- update_only: %w[new],
37
- )
20
+ @connection.exec_prepared("waldit_update", audit + [event.old.to_json, event.new.to_json])
38
21
 
39
22
  when DeleteEvent
40
- case record.where(audit.slice(*unique_by)).pluck(:action, :old).first
41
- in ["insert", _]
42
- # We are deleting a record that was inserted on this transaction, which means we don't need to audit anything,
43
- # as the record was never commited
44
- record.where(audit.slice(*unique_by)).delete_all
45
-
46
- in ["update", old]
47
- # We are deleting a record we updated on this transaction. Here we are making sure we keep the correct previous
48
- # state, and not the state at the moment of the deletion
49
- record.upsert(
50
- audit.merge(action: "delete", old:, new: {}),
51
- unique_by:,
52
- on_duplicate: :update,
53
- )
54
-
55
- in ["delete", _]
56
- # This should never happend, we wouldn't be able to delete a record that was already deleted on this transaction
57
-
23
+ case @connection.exec_prepared("waldit_delete_cleanup", [event.transaction_id, event.table, event.primary_key]).values
24
+ in [["update", previous_old]]
25
+ @connection.exec_prepared("waldit_delete", audit + [previous_old])
26
+ in []
27
+ @connection.exec_prepared("waldit_delete", audit + [event.old.to_json])
58
28
  else
59
- # Finally the most common case: just deleting a record not created or updated on this transaction
60
- record.upsert(
61
- audit.merge(action: "delete", old: event.old),
62
- unique_by:,
63
- on_duplicate: :update,
64
- )
29
+ # Don't need to audit anything on this case
65
30
  end
66
31
  end
67
32
  end
68
33
 
69
34
  def on_transaction_events(events)
70
- counter = 0
71
- catch :finish do
72
- loop do
73
- record.transaction do
74
- events.each do |event|
75
- case event
76
- when CommitTransactionEvent
77
- record
78
- .where(transaction_id: event.transaction_id)
79
- .update_all(commited_at: event.timestamp) if counter > 0
80
- # Using throw to break the outside loop and finish the thread gracefully
81
- throw :finish
82
-
83
- when InsertEvent, UpdateEvent, DeleteEvent
84
- audit_event(event)
85
-
86
- counter += 1
87
- # We break here to force a commit, so we don't keep a single big transaction pending
88
- break if counter % max_transaction_size == 0
89
- end
35
+ record.transaction do
36
+ @connection = record.connection.raw_connection
37
+ tables = Set.new
38
+ insert_prepared = false
39
+ update_prepared = false
40
+ delete_prepared = false
41
+
42
+ events.each do |event|
43
+ case event
44
+ when CommitTransactionEvent
45
+ record.where(transaction_id: event.transaction_id).update_all(commited_at: event.timestamp)
46
+
47
+ log_new = tables.filter { |table| Waldit.store_changes.call(table).include? :new }
48
+ log_old = tables.filter { |table| Waldit.store_changes.call(table).include? :old }
49
+ log_diff = tables.filter { |table| Waldit.store_changes.call(table).include? :diff }
50
+ record.where(transaction_id: event.transaction_id).update_all(<<~SQL)
51
+ new = CASE WHEN table_name = ANY (ARRAY[#{log_new.map { |table| "'#{table}'" }.join(",")}]::varchar[]) THEN new ELSE null END,
52
+ old = CASE WHEN table_name = ANY (ARRAY[#{log_old.map { |table| "'#{table}'" }.join(",")}]::varchar[]) THEN old ELSE null END,
53
+ diff =
54
+ CASE WHEN table_name = ANY (ARRAY[#{log_diff.map { |table| "'#{table}'" }.join(",")}]::varchar[]) THEN (
55
+ SELECT
56
+ jsonb_object_agg(
57
+ coalesce(old_kv.key, new_kv.key),
58
+ jsonb_build_array(old_kv.value, new_kv.value)
59
+ )
60
+ FROM jsonb_each(old) AS old_kv
61
+ FULL OUTER JOIN jsonb_each(new) AS new_kv ON old_kv.key = new_kv.key
62
+ WHERE old_kv.value IS DISTINCT FROM new_kv.value
63
+ )
64
+ ELSE null
65
+ END
66
+ SQL
67
+
68
+ when InsertEvent
69
+ tables << event.table
70
+ unless insert_prepared
71
+ prepare_insert
72
+ insert_prepared = true
73
+ end
74
+ audit_event(event)
75
+
76
+ when UpdateEvent
77
+ tables << event.table
78
+ unless update_prepared
79
+ prepare_update
80
+ update_prepared = true
90
81
  end
82
+ audit_event(event)
83
+
84
+ when DeleteEvent
85
+ tables << event.table
86
+ unless delete_prepared
87
+ prepare_delete
88
+ prepare_delete_cleanup
89
+ delete_prepared = true
90
+ end
91
+ audit_event(event)
91
92
  end
92
93
  end
93
94
  end
@@ -112,5 +113,46 @@ module Waldit
112
113
  def record
113
114
  Waldit.model
114
115
  end
116
+
117
+ private
118
+
119
+ def prepare_insert
120
+ @connection.prepare("waldit_insert", <<~SQL)
121
+ INSERT INTO #{record.table_name} (transaction_id, lsn, table_name, primary_key, action, context, new)
122
+ VALUES ($1, $2, $3, $4, 'insert'::waldit_action, $5, $6)
123
+ ON CONFLICT (table_name, primary_key, transaction_id)
124
+ DO UPDATE SET new = #{record.table_name}.new
125
+ SQL
126
+ end
127
+
128
+ def prepare_update
129
+ @connection.prepare("waldit_update", <<~SQL)
130
+ INSERT INTO #{record.table_name} (transaction_id, lsn, table_name, primary_key, action, context, old, new)
131
+ VALUES ($1, $2, $3, $4, 'update'::waldit_action, $5, $6, $7)
132
+ ON CONFLICT (table_name, primary_key, transaction_id)
133
+ DO UPDATE SET new = excluded.new
134
+ SQL
135
+ end
136
+
137
+ def prepare_delete
138
+ @connection.prepare("waldit_delete", <<~SQL)
139
+ INSERT INTO #{record.table_name} (transaction_id, lsn, table_name, primary_key, action, context, old)
140
+ VALUES ($1, $2, $3, $4, 'delete'::waldit_action, $5, $6)
141
+ ON CONFLICT (table_name, primary_key, transaction_id)
142
+ DO UPDATE SET old = #{record.table_name}.old
143
+ SQL
144
+ end
145
+
146
+ def prepare_delete_cleanup
147
+ @connection.prepare("waldit_delete_cleanup", <<~SQL)
148
+ DELETE FROM #{record.table_name}
149
+ WHERE
150
+ transaction_id = $1
151
+ AND table_name = $2
152
+ AND primary_key = $3
153
+ AND action IN ('insert'::waldit_action, 'update'::waldit_action)
154
+ RETURNING action, old
155
+ SQL
156
+ end
115
157
  end
116
158
  end
data/lib/waldit.rb CHANGED
@@ -21,6 +21,21 @@ module Waldit
21
21
  end
22
22
  end
23
23
 
24
+ attr_reader :store_changes
25
+
26
+ def store_changes=(changes)
27
+ case changes
28
+ when Symbol
29
+ changes = [changes].to_set
30
+ @store_changes = -> { changes }
31
+ when Array
32
+ changes = changes.map(&:to_sym).to_set
33
+ @store_changes = -> { changes }
34
+ else
35
+ @store_changes = changes
36
+ end
37
+ end
38
+
24
39
  attr_accessor :ignored_columns
25
40
  attr_accessor :max_transaction_size
26
41
  attr_accessor :model
@@ -36,6 +51,8 @@ module Waldit
36
51
 
37
52
  config.watched_tables = -> table { table != "waldit" }
38
53
 
54
+ config.store_changes = -> table { %i[old new diff] }
55
+
39
56
  config.ignored_columns = -> table { %w[created_at updated_at] }
40
57
 
41
58
  config.max_transaction_size = 10_000
data/rbi/waldit.rbi CHANGED
@@ -2,7 +2,7 @@
2
2
  module Waldit
3
3
  extend T::Sig
4
4
  extend Waldit::Context
5
- VERSION = "0.0.4"
5
+ VERSION = "0.0.6"
6
6
 
7
7
  class << self
8
8
  sig { returns(String) }
@@ -11,6 +11,9 @@ module Waldit
11
11
  sig { returns(T.proc.params(table: String).returns(T::Boolean)) }
12
12
  attr_reader :watched_tables
13
13
 
14
+ sig { returns(T.proc.params(table: String).returns(T::Array[Symbol])) }
15
+ attr_reader :store_changes
16
+
14
17
  sig { returns(T.proc.params(table: String).returns(T::Array[String])) }
15
18
  attr_accessor :ignored_columns
16
19
 
@@ -24,6 +27,9 @@ module Waldit
24
27
  sig { params(tables: T.any(T::Array[String], T.proc.params(table: String).returns(T::Boolean))).void }
25
28
  def self.watched_tables=(tables); end
26
29
 
30
+ sig { params(changes: T.any(Symbol, T::Array[Symbol], T.proc.params(table: String).returns(T::Array[Symbol]))).void }
31
+ def self.store_changes=(changes); end
32
+
27
33
  sig { params(block: T.proc.params(config: T.class_of(Waldit)).void).void }
28
34
  def self.configure(&block); end
29
35
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waldit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Navarro
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-08-02 00:00:00.000000000 Z
10
+ date: 2025-08-12 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: wal
@@ -71,7 +71,6 @@ files:
71
71
  - lib/waldit/version.rb
72
72
  - lib/waldit/watcher.rb
73
73
  - rbi/waldit.rbi
74
- - sig/waldit.rbs
75
74
  homepage: https://github.com/reu/waldit
76
75
  licenses:
77
76
  - MIT
data/sig/waldit.rbs DELETED
@@ -1,73 +0,0 @@
1
- # typed: strong
2
- module Waldit
3
- extend Waldit::Context
4
-
5
- def self.watched_tables=: () -> String
6
- | () -> ^(String table) -> bool
7
- | () -> ^(String table) -> ::Array[String]
8
- | () -> Integer
9
- | () -> singleton(ActiveRecord::Base)
10
- | (::Array[String] | ^(String table) -> bool tables) -> void
11
-
12
- def self.configure: () { (singleton(Waldit) config) -> void } -> void
13
- end
14
-
15
- Waldit::VERSION: untyped
16
-
17
- module Waldit::Context
18
- def with_context: [U] (Context context) { () -> U } -> U
19
-
20
- def context: () -> Context?
21
-
22
- def add_context: (Context added_context) -> void
23
-
24
- def new_context: (?Context context) -> void
25
- end
26
-
27
- Waldit::Waldit::Context::Context: untyped
28
-
29
- class Waldit::Railtie < Rails::Railtie
30
- end
31
-
32
- module Waldit::Record
33
- extend T::Helpers
34
-
35
- def new: () -> ::Hash[String | Symbol, untyped]
36
-
37
- def old: () -> ::Hash[String | Symbol, untyped]
38
-
39
- def diff: () -> ::Hash[String | Symbol, [ untyped, untyped ]]
40
- end
41
-
42
- module Waldit::Sidekiq
43
- end
44
-
45
- class Waldit::Waldit::Sidekiq::SaveContext
46
- include ::Sidekiq::ClientMiddleware
47
-
48
- def call: (untyped job_class, untyped job, untyped queue, untyped redis) -> untyped
49
- end
50
-
51
- class Waldit::Waldit::Sidekiq::LoadContext
52
- include ::Sidekiq::ServerMiddleware
53
-
54
- def call: (untyped job_instance, untyped job, untyped queue) { () -> untyped } -> untyped
55
-
56
- def deserialize_context: (untyped job) -> untyped
57
- end
58
-
59
- class Waldit::Watcher < Wal::StreamingWatcher
60
- def audit_event: (InsertEvent | UpdateEvent | DeleteEvent event) -> void
61
-
62
- def on_transaction_events: (::Enumerator[Event] events) -> void
63
-
64
- def should_watch_table?: (String table) -> bool
65
-
66
- def valid_context_prefix?: (String prefix) -> bool
67
-
68
- def ignored_columns: (String table) -> ::Array[String]
69
-
70
- def max_transaction_size: () -> Integer
71
-
72
- def record: () -> singleton(ActiveRecord::Base)
73
- end