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 +4 -4
- data/lib/waldit/record.rb +15 -1
- data/lib/waldit/version.rb +1 -1
- data/lib/waldit/watcher.rb +106 -64
- data/lib/waldit.rb +17 -0
- data/rbi/waldit.rbi +7 -1
- metadata +2 -3
- data/sig/waldit.rbs +0 -73
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e8e8ecb8b8fc2c3c4b855fb06b204d858b7cfbf24f12dc016b4c3f5ca10849a2
|
4
|
+
data.tar.gz: b72218c4df4b396e497f1aa9d96fd159305fabb7faf0d481392044536da5141a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
data/lib/waldit/version.rb
CHANGED
data/lib/waldit/watcher.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
41
|
-
in ["
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
#
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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.
|
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
|
+
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-
|
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
|