waldit 0.0.3 → 0.0.5
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/Rakefile +0 -7
- data/lib/waldit/context.rb +0 -13
- data/lib/waldit/postgresql_adapter.rb +0 -1
- data/lib/waldit/railtie.rb +0 -1
- data/lib/waldit/record.rb +0 -12
- data/lib/waldit/sidekiq.rb +0 -1
- data/lib/waldit/version.rb +1 -2
- data/lib/waldit/watcher.rb +82 -73
- data/lib/waldit.rb +1 -15
- data/rbi/waldit.rbi +1 -1
- metadata +4 -60
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b76edf8a202620aff6e09391009e6c00ae0f26a404d1b81741c5cd3e339db66
|
4
|
+
data.tar.gz: f0c7582d6af476ecf536aa4e96846eea620b8c3bd5c215c240d9e68056ade643
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5aee8ef8ff8c9f0b6fc636b866a9fe72a08c1ba98e507df1b7d3771339130808c04c9fb32345a07a12416967d386be92bdfe3bfd1f3215dc723f337baf06278b
|
7
|
+
data.tar.gz: e5f139662394131f2bfbe1af5ac3df85fa7b6202756ec67c02f0e679d11c78e2a62ecdb3a5bb188fa20a25ddee23699824266831f86b09e3284c0ad2584e9d6b
|
data/Rakefile
CHANGED
@@ -3,10 +3,3 @@
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
|
5
5
|
task(:test) { sh "bundle exec rspec" }
|
6
|
-
|
7
|
-
task default: %i[build]
|
8
|
-
|
9
|
-
task("sig/waldit.rbi") { sh "bundle exec parlour" }
|
10
|
-
task("rbi/waldit.rbs" => "sig/waldit.rbi") { sh "rbs prototype rbi rbi/waldit.rbi > sig/waldit.rbs" }
|
11
|
-
|
12
|
-
Rake::Task["build"].enhance(["sig/waldit.rbi", "rbi/waldit.rbs"])
|
data/lib/waldit/context.rb
CHANGED
@@ -1,17 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# typed: true
|
3
2
|
|
4
3
|
module Waldit
|
5
4
|
module Context
|
6
|
-
extend T::Sig
|
7
|
-
|
8
|
-
Context = T.type_alias { T::Hash[T.any(String, Symbol), T.untyped] }
|
9
|
-
|
10
|
-
sig do
|
11
|
-
type_parameters(:U)
|
12
|
-
.params(context: Context, block: T.proc.returns(T.type_parameter(:U)))
|
13
|
-
.returns(T.type_parameter(:U))
|
14
|
-
end
|
15
5
|
def with_context(context, &block)
|
16
6
|
current_context = self.context || {}
|
17
7
|
Thread.current[:waldit_context] ||= []
|
@@ -21,12 +11,10 @@ module Waldit
|
|
21
11
|
Thread.current[:waldit_context].pop
|
22
12
|
end
|
23
13
|
|
24
|
-
sig { returns(T.nilable(Context)) }
|
25
14
|
def context
|
26
15
|
Thread.current[:waldit_context]&.last
|
27
16
|
end
|
28
17
|
|
29
|
-
sig { params(added_context: Context).void }
|
30
18
|
def add_context(added_context)
|
31
19
|
if (context = self.context)
|
32
20
|
context.merge!(added_context.as_json)
|
@@ -35,7 +23,6 @@ module Waldit
|
|
35
23
|
end
|
36
24
|
end
|
37
25
|
|
38
|
-
sig { params(context: Context).void }
|
39
26
|
def new_context(context = {})
|
40
27
|
Thread.current[:waldit_context] ||= []
|
41
28
|
Thread.current[:waldit_context].push(context.as_json)
|
data/lib/waldit/railtie.rb
CHANGED
data/lib/waldit/record.rb
CHANGED
@@ -1,19 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# typed: true
|
3
2
|
|
4
3
|
module Waldit
|
5
4
|
module Record
|
6
|
-
extend T::Sig
|
7
|
-
extend T::Helpers
|
8
|
-
abstract!
|
9
|
-
|
10
|
-
sig { abstract.returns(T::Hash[T.any(String, Symbol), T.untyped]) }
|
11
|
-
def new; end
|
12
|
-
|
13
|
-
sig { abstract.returns(T::Hash[T.any(String, Symbol), T.untyped]) }
|
14
|
-
def old; end
|
15
|
-
|
16
|
-
sig { returns(T::Hash[T.any(String, Symbol), [T.untyped, T.untyped]]) }
|
17
5
|
def diff
|
18
6
|
(old.keys | new.keys).reduce({}.with_indifferent_access) do |diff, key|
|
19
7
|
old[key] != new[key] ? diff.merge(key => [old[key], new[key]]) : diff
|
data/lib/waldit/sidekiq.rb
CHANGED
data/lib/waldit/version.rb
CHANGED
data/lib/waldit/watcher.rb
CHANGED
@@ -1,124 +1,133 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# typed: true
|
3
2
|
|
4
3
|
require "wal"
|
5
4
|
|
6
5
|
module Waldit
|
7
6
|
class Watcher < Wal::StreamingWatcher
|
8
|
-
|
7
|
+
include Wal
|
9
8
|
|
10
|
-
sig { params(event: T.any(InsertEvent, UpdateEvent, DeleteEvent)).void }
|
11
9
|
def audit_event(event)
|
12
10
|
return unless event.primary_key
|
13
11
|
|
14
|
-
audit =
|
15
|
-
transaction_id: event.transaction_id,
|
16
|
-
lsn: event.lsn,
|
17
|
-
context: event.context,
|
18
|
-
table_name: event.table,
|
19
|
-
primary_key: event.primary_key,
|
20
|
-
}
|
21
|
-
|
22
|
-
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]
|
23
13
|
|
24
14
|
case event
|
25
15
|
when InsertEvent
|
26
|
-
|
27
|
-
audit.merge(action: "insert", new: event.new),
|
28
|
-
unique_by:,
|
29
|
-
on_duplicate: :update,
|
30
|
-
)
|
16
|
+
@connection.exec_prepared("waldit_insert", audit + [event.new.to_json])
|
31
17
|
|
32
18
|
when UpdateEvent
|
33
19
|
return if event.diff.without(ignored_columns(event.table)).empty?
|
34
|
-
|
35
|
-
audit.merge(action: "update", old: event.old, new: event.new),
|
36
|
-
unique_by:,
|
37
|
-
on_duplicate: :update,
|
38
|
-
update_only: %w[new],
|
39
|
-
)
|
20
|
+
@connection.exec_prepared("waldit_update", audit + [event.old.to_json, event.new.to_json])
|
40
21
|
|
41
22
|
when DeleteEvent
|
42
|
-
case
|
43
|
-
in ["
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
in ["update", old]
|
49
|
-
# We are deleting a record we updated on this transaction. Here we are making sure we keep the correct previous
|
50
|
-
# state, and not the state at the moment of the deletion
|
51
|
-
record.upsert(
|
52
|
-
audit.merge(action: "delete", old:, new: {}),
|
53
|
-
unique_by:,
|
54
|
-
on_duplicate: :update,
|
55
|
-
)
|
56
|
-
|
57
|
-
in ["delete", _]
|
58
|
-
# This should never happend, we wouldn't be able to delete a record that was already deleted on this transaction
|
59
|
-
|
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])
|
60
28
|
else
|
61
|
-
#
|
62
|
-
record.upsert(
|
63
|
-
audit.merge(action: "delete", old: event.old),
|
64
|
-
unique_by:,
|
65
|
-
on_duplicate: :update,
|
66
|
-
)
|
29
|
+
# Don't need to audit anything on this case
|
67
30
|
end
|
68
31
|
end
|
69
32
|
end
|
70
33
|
|
71
|
-
sig { override.params(events: T::Enumerator[Event]).void }
|
72
34
|
def on_transaction_events(events)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
35
|
+
record.transaction do
|
36
|
+
@connection = record.connection.raw_connection
|
37
|
+
insert_prepared = false
|
38
|
+
update_prepared = false
|
39
|
+
delete_prepared = false
|
40
|
+
|
41
|
+
events.each do |event|
|
42
|
+
case event
|
43
|
+
when CommitTransactionEvent
|
44
|
+
record.where(transaction_id: event.transaction_id).update_all(commited_at: event.timestamp)
|
45
|
+
|
46
|
+
when InsertEvent
|
47
|
+
unless insert_prepared
|
48
|
+
prepare_insert
|
49
|
+
insert_prepared = true
|
50
|
+
end
|
51
|
+
audit_event(event)
|
52
|
+
|
53
|
+
when UpdateEvent
|
54
|
+
unless update_prepared
|
55
|
+
prepare_update
|
56
|
+
update_prepared = true
|
93
57
|
end
|
58
|
+
audit_event(event)
|
59
|
+
|
60
|
+
when DeleteEvent
|
61
|
+
unless delete_prepared
|
62
|
+
prepare_delete
|
63
|
+
prepare_delete_cleanup
|
64
|
+
delete_prepared = true
|
65
|
+
end
|
66
|
+
audit_event(event)
|
94
67
|
end
|
95
68
|
end
|
96
69
|
end
|
97
70
|
end
|
98
71
|
|
99
|
-
sig { params(table: String).returns(T::Boolean) }
|
100
72
|
def should_watch_table?(table)
|
101
73
|
Waldit.watched_tables.call(table)
|
102
74
|
end
|
103
75
|
|
104
|
-
sig { params(prefix: String).returns(T::Boolean) }
|
105
76
|
def valid_context_prefix?(prefix)
|
106
77
|
prefix == Waldit.context_prefix
|
107
78
|
end
|
108
79
|
|
109
|
-
sig { params(table: String).returns(T::Array[String]) }
|
110
80
|
def ignored_columns(table)
|
111
81
|
Waldit.ignored_columns.call(table)
|
112
82
|
end
|
113
83
|
|
114
|
-
sig { returns(Integer) }
|
115
84
|
def max_transaction_size
|
116
85
|
Waldit.max_transaction_size
|
117
86
|
end
|
118
87
|
|
119
|
-
sig { returns(T.class_of(ActiveRecord::Base)) }
|
120
88
|
def record
|
121
89
|
Waldit.model
|
122
90
|
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def prepare_insert
|
95
|
+
@connection.prepare("waldit_insert", <<~SQL)
|
96
|
+
INSERT INTO #{record.table_name} (transaction_id, lsn, table_name, primary_key, action, context, new)
|
97
|
+
VALUES ($1, $2, $3, $4, 'insert'::waldit_action, $5, $6)
|
98
|
+
ON CONFLICT (table_name, primary_key, transaction_id)
|
99
|
+
DO UPDATE SET new = #{record.table_name}.new
|
100
|
+
SQL
|
101
|
+
end
|
102
|
+
|
103
|
+
def prepare_update
|
104
|
+
@connection.prepare("waldit_update", <<~SQL)
|
105
|
+
INSERT INTO #{record.table_name} (transaction_id, lsn, table_name, primary_key, action, context, old, new)
|
106
|
+
VALUES ($1, $2, $3, $4, 'update'::waldit_action, $5, $6, $7)
|
107
|
+
ON CONFLICT (table_name, primary_key, transaction_id)
|
108
|
+
DO UPDATE SET new = excluded.new
|
109
|
+
SQL
|
110
|
+
end
|
111
|
+
|
112
|
+
def prepare_delete
|
113
|
+
@connection.prepare("waldit_delete", <<~SQL)
|
114
|
+
INSERT INTO #{record.table_name} (transaction_id, lsn, table_name, primary_key, action, context, old, new)
|
115
|
+
VALUES ($1, $2, $3, $4, 'delete'::waldit_action, $5, $6, '{}'::jsonb)
|
116
|
+
ON CONFLICT (table_name, primary_key, transaction_id)
|
117
|
+
DO UPDATE SET old = #{record.table_name}.old
|
118
|
+
SQL
|
119
|
+
end
|
120
|
+
|
121
|
+
def prepare_delete_cleanup
|
122
|
+
@connection.prepare("waldit_delete_cleanup", <<~SQL)
|
123
|
+
DELETE FROM #{record.table_name}
|
124
|
+
WHERE
|
125
|
+
transaction_id = $1
|
126
|
+
AND table_name = $2
|
127
|
+
AND primary_key = $3
|
128
|
+
AND action IN ('insert'::waldit_action, 'update'::waldit_action)
|
129
|
+
RETURNING action, old
|
130
|
+
SQL
|
131
|
+
end
|
123
132
|
end
|
124
133
|
end
|
data/lib/waldit.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# typed: true
|
3
2
|
|
4
3
|
require_relative "waldit/version"
|
5
4
|
require_relative "waldit/context"
|
@@ -8,19 +7,11 @@ require_relative "waldit/record"
|
|
8
7
|
require_relative "waldit/watcher"
|
9
8
|
|
10
9
|
module Waldit
|
11
|
-
extend T::Sig
|
12
10
|
extend Waldit::Context
|
13
11
|
|
14
12
|
class << self
|
15
|
-
extend T::Sig
|
16
|
-
|
17
|
-
sig { returns(String) }
|
18
|
-
attr_accessor :context_prefix
|
19
|
-
|
20
|
-
sig { returns(T.proc.params(table: String).returns(T::Boolean)) }
|
21
13
|
attr_reader :watched_tables
|
22
14
|
|
23
|
-
sig { params(tables: T.any(T::Array[String], T.proc.params(table: String).returns(T::Boolean))).void }
|
24
15
|
def watched_tables=(tables)
|
25
16
|
case tables
|
26
17
|
when Array
|
@@ -30,17 +21,12 @@ module Waldit
|
|
30
21
|
end
|
31
22
|
end
|
32
23
|
|
33
|
-
sig { returns(T.proc.params(table: String).returns(T::Array[String])) }
|
34
24
|
attr_accessor :ignored_columns
|
35
|
-
|
36
|
-
sig { returns(Integer) }
|
37
25
|
attr_accessor :max_transaction_size
|
38
|
-
|
39
|
-
sig { returns(T.class_of(ActiveRecord::Base)) }
|
40
26
|
attr_accessor :model
|
27
|
+
attr_accessor :context_prefix
|
41
28
|
end
|
42
29
|
|
43
|
-
sig { params(block: T.proc.params(config: T.class_of(Waldit)).void).void }
|
44
30
|
def self.configure(&block)
|
45
31
|
yield self
|
46
32
|
end
|
data/rbi/waldit.rbi
CHANGED
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.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rodrigo Navarro
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-08-11 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: wal
|
@@ -15,14 +15,14 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - ">="
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: 0.0.
|
18
|
+
version: 0.0.3
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - ">="
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: 0.0.
|
25
|
+
version: 0.0.3
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: activerecord
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -37,62 +37,6 @@ dependencies:
|
|
37
37
|
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '7'
|
40
|
-
- !ruby/object:Gem::Dependency
|
41
|
-
name: rbs
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
43
|
-
requirements:
|
44
|
-
- - ">="
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: '0'
|
47
|
-
type: :development
|
48
|
-
prerelease: false
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
50
|
-
requirements:
|
51
|
-
- - ">="
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '0'
|
54
|
-
- !ruby/object:Gem::Dependency
|
55
|
-
name: sorbet
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
57
|
-
requirements:
|
58
|
-
- - ">="
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: '0'
|
61
|
-
type: :development
|
62
|
-
prerelease: false
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
64
|
-
requirements:
|
65
|
-
- - ">="
|
66
|
-
- !ruby/object:Gem::Version
|
67
|
-
version: '0'
|
68
|
-
- !ruby/object:Gem::Dependency
|
69
|
-
name: tapioca
|
70
|
-
requirement: !ruby/object:Gem::Requirement
|
71
|
-
requirements:
|
72
|
-
- - ">="
|
73
|
-
- !ruby/object:Gem::Version
|
74
|
-
version: '0'
|
75
|
-
type: :development
|
76
|
-
prerelease: false
|
77
|
-
version_requirements: !ruby/object:Gem::Requirement
|
78
|
-
requirements:
|
79
|
-
- - ">="
|
80
|
-
- !ruby/object:Gem::Version
|
81
|
-
version: '0'
|
82
|
-
- !ruby/object:Gem::Dependency
|
83
|
-
name: parlour
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
85
|
-
requirements:
|
86
|
-
- - ">="
|
87
|
-
- !ruby/object:Gem::Version
|
88
|
-
version: '0'
|
89
|
-
type: :development
|
90
|
-
prerelease: false
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
92
|
-
requirements:
|
93
|
-
- - ">="
|
94
|
-
- !ruby/object:Gem::Version
|
95
|
-
version: '0'
|
96
40
|
- !ruby/object:Gem::Dependency
|
97
41
|
name: sidekiq
|
98
42
|
requirement: !ruby/object:Gem::Requirement
|