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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb7ae260f6d1af42f4e6169084c7ba79de48cea93f44ff47be032f5764df23a6
4
- data.tar.gz: 7cf9d0c022ffbb457bcac4380ed635cd65d2df1db24faae5fbd169b50b5aa07c
3
+ metadata.gz: 8b76edf8a202620aff6e09391009e6c00ae0f26a404d1b81741c5cd3e339db66
4
+ data.tar.gz: f0c7582d6af476ecf536aa4e96846eea620b8c3bd5c215c240d9e68056ade643
5
5
  SHA512:
6
- metadata.gz: b54045c2265401d420a276efddb20525b7b72e39d27c25e10da6a7a2c357805ea1956aed900d3c265f5cc88096dc402cd53ea52d0989eff8e3dd965d47c2ec75
7
- data.tar.gz: 8d86ba10a3c94e7ca87bb90a39957663b305f19b6b5a8bfb7062fdae89879b57c82d230989bfd49b5480738f63ca81bf9db7b1093406b730578498bfc001a862
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"])
@@ -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)
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- # typed: ignore
3
2
 
4
3
  require "active_record/connection_adapters/postgresql_adapter"
5
4
 
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- # typed: false
3
2
 
4
3
  require "rails/railtie"
5
4
 
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
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- # typed: false
3
2
 
4
3
  module Waldit
5
4
  module Sidekiq
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
- # typed: true
3
2
 
4
3
  module Waldit
5
- VERSION = "0.0.3"
4
+ VERSION = "0.0.5"
6
5
  end
@@ -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
- extend T::Sig
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
- record.upsert(
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
- record.upsert(
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 record.where(audit.slice(*unique_by)).pluck(:action, :old).first
43
- in ["insert", _]
44
- # We are deleting a record that was inserted on this transaction, which means we don't need to audit anything,
45
- # as the record was never commited
46
- record.where(audit.slice(*unique_by)).delete_all
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
- # Finally the most common case: just deleting a record not created or updated on this transaction
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
- counter = 0
74
- catch :finish do
75
- loop do
76
- record.transaction do
77
- events.each do |event|
78
- case event
79
- when CommitTransactionEvent
80
- record
81
- .where(transaction_id: event.transaction_id)
82
- .update_all(commited_at: event.timestamp) if counter > 0
83
- # Using throw to break the outside loop and finish the thread gracefully
84
- throw :finish
85
-
86
- when InsertEvent, UpdateEvent, DeleteEvent
87
- audit_event(event)
88
-
89
- counter += 1
90
- # We break here to force a commit, so we don't keep a single big transaction pending
91
- break if counter % max_transaction_size == 0
92
- end
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
@@ -2,7 +2,7 @@
2
2
  module Waldit
3
3
  extend T::Sig
4
4
  extend Waldit::Context
5
- VERSION = "0.0.3"
5
+ VERSION = "0.0.4"
6
6
 
7
7
  class << self
8
8
  sig { returns(String) }
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.3
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-07-02 00:00:00.000000000 Z
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.2
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.2
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