wal 0.0.15 → 0.0.16

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: 14c30e66cc9c1425cb894a9cfb558941017659cf4f0ad5a2183782c25df4a5b4
4
- data.tar.gz: 7704871035cecbb90ee47cbefff50ad2fe58b4515f7d2c0d439e40adf31bf3f4
3
+ metadata.gz: fad10e5a8aea823dba89b5f50ba13e60b771d442e9dbe682b0b03c8c0969b1c1
4
+ data.tar.gz: b1a2fff4a202dad05abe78d79fed809d18b6396fd97a8d6aef3438e7a30ab3c8
5
5
  SHA512:
6
- metadata.gz: e47256ec844eace9aabb20a1364fc0d0ab724e81c335f7b0bbb826871a328d8a0dca806d3e2ec891717339acc2286b2f71c2f424ccfed60c906772ded95f10ef
7
- data.tar.gz: 0c584c14ec81977b7a26d66117a42180c73450da3a53ec9fa7dc5fe6ddd11e941833de45024032b3a8c7d31ddab6f7261911eaff7e2d83328031d7b8f4ddf9b2
6
+ metadata.gz: cb9cd1dcdb4afa3611597d6aac89470892d63d0fabe1db2cb1ba6c6e8013cf610cf27e20185a40a8aa0365ee5e5639b44789718aa46cded921dfa4c2c92c7938
7
+ data.tar.gz: 1655d1ee9a327383ddde0483a1e8a432d9b529601b7d3f700a427a0ac1560c01f786230e8fd93d62fc503d688cd4d02a92c69d7e53d05801c62106b2dfcb00da
data/README.md CHANGED
@@ -1,49 +1,126 @@
1
1
  # Wal
2
2
 
3
- Easily hook into Postgres WAL event log from your Rails app.
3
+ Wal is a framework that lets you hook into Postgres WAL events directly from your Rails application.
4
4
 
5
- Proper documentation TBD
5
+ Unlike using database triggers, Wal allows you to keep your logic in your application code while still reacting to persistence events coming from the database.
6
6
 
7
- ## Examples
7
+ Also, unlike ActiveRecord callbacks, these events are guaranteed by Postgre to be 100% consistent, ensuring you never miss one.
8
8
 
9
- ### Watch for model changes using the RecordWatcher DSL
9
+ # Getting started
10
+
11
+ ## Installation
12
+
13
+ Add `wal` to your application's Gemfile:
10
14
 
11
15
  ```ruby
12
- class ProductAvailabilityWatcher < Wal::RecordWatcher
13
- on_save Product, changed: %w[price] do |event|
14
- recalculate_inventory_price(event.primary_key, event.new["price"])
15
- end
16
+ gem "wal"
17
+ ```
18
+
19
+ And then:
20
+
21
+ ```bash
22
+ $ bundle install
23
+ ```
24
+
25
+ ## Getting started
16
26
 
17
- on_delete Product do |event|
18
- clear_product_inventory(event.primary_key)
27
+ The core building block in Wal is a `Watcher`. The easiest way to create one is by extending `Wal::RecordWatcher`, which handles most of the boilerplate for you.
28
+
29
+ For example, let's create a watcher that denormalizes `Post` and `Category` models into a `DenormalizedPost`.
30
+
31
+ Create a new file at `app/watchers/denormalize_post_watcher.rb`:
32
+
33
+ ```ruby
34
+ class DenormalizePostWatcher < Wal::RecordWatcher
35
+ # When a new `Post` is created, we create a new `DenormalizedPost` record
36
+ on_insert Post do |event|
37
+ DenormalizedPost.create!(
38
+ post_id: event.primary_key,
39
+ title: event.new["title"],
40
+ body: event.new["body"],
41
+ category_id: event.new["category_id"],
42
+ category_name: Category.find_by(id: event.new["category_id"])&.name,
43
+ )
19
44
  end
20
45
 
21
- on_save Sales, changed: %w[status] do |event|
22
- recalculate_inventory_quantity(event.primary_key)
46
+ # When a `Post` title or body is changed, we update its `DenormalizedPost` record
47
+ on_update Post, changed: [:title, :body] do |event|
48
+ DenormalizedPost.where(post_id: event.primary_key).update_all(
49
+ title: event.new["title"],
50
+ body: event.new["body"],
51
+ )
23
52
  end
24
53
 
25
- def recalculate_inventory_price(product_id, new_price)
26
- # ...
54
+ # When a `Post` category changes, we also update its `DenormalizedPost` record
55
+ on_update Post, changed: [:category_id] do |event|
56
+ DenormalizedPost.where(post_id: event.primary_key).update_all(
57
+ category_id: event.new["category_id"],
58
+ category_name: Category.find_by(id: event.new["category_id"])&.name,
59
+ )
27
60
  end
28
61
 
29
- def clear_product_inventory(product_id)
30
- # ...
62
+ # When a `Category` changes, we update all the `DenormalizedPosts` referencing it
63
+ on_update Category, changed: [:name] do |event|
64
+ DenormalizedPost.where(category_id: event.primary_key).update_all(
65
+ category_name: event.new["name"],
66
+ )
31
67
  end
32
68
 
33
- def recalculate_inventory_quantity(sales_id)
34
- # ...
69
+ # Finally when a `Category` is deleted, we clear all the `DenormalizedPosts` referencing it
70
+ on_update Category, changed: [:name] do |event|
71
+ DenormalizedPost.where(category_id: event.primary_key).update_all(
72
+ category_id: nil,
73
+ category_name: nil,
74
+ )
35
75
  end
36
76
  end
37
77
  ```
38
78
 
39
- ### Basic watcher implementation
79
+ You might wonder: *Why not just use ActiveRecord callbacks for this?*
40
80
 
41
- ```ruby
42
- class LogWatcher
43
- include Wal::Watcher
81
+ While callbacks seem simpler, they are not guaranteed to always run. Depending on the methods you use to perform the changes, it can be skipped.
82
+
83
+ Wal ensures every single change is captured. Even if updates happen directly in the database and bypass Rails entirely. That's the main reason to use it: when you need 100% consistency.
84
+
85
+ ## Configuring the Watcher
44
86
 
45
- def on_event(event)
46
- puts "Wal event received #{event}"
87
+ Wal relies on [Postgres logical replication](https://www.postgresql.org/docs/current/logical-replication.html) to stream changes to your watchers.
88
+
89
+ First, create a [Postgres publication](https://www.postgresql.org/docs/current/logical-replication-publication.html) for the tables your watcher uses. Wal provides a generator for this:
90
+
91
+ ```
92
+ $ rails generate wal:migration DenormalizePostWatcher
93
+ ```
94
+
95
+ This will generate a new migration with all the tables that your watcher uses:
96
+ ```ruby
97
+ class SetDenormalizePostWatcherPublication < ActiveRecord::Migration
98
+ def change
99
+ define_publication :denormalize_post_publication do |p|
100
+ p.table :posts
101
+ p.table :categories
102
+ end
47
103
  end
48
104
  end
49
105
  ```
106
+
107
+ Next, create a `config/wal.yml` configuration file to link the `Watcher` to its publication:
108
+
109
+ ```yaml
110
+ slots:
111
+ denormalize_posts:
112
+ watcher: DenormalizePostWatcher
113
+ publications:
114
+ - denormalize_post_publication
115
+ ```
116
+ This associates your watcher with the `denormalize_post_publication` and with the `denormalize_posts` [Postgres replication slot](https://www.postgresql.org/docs/9.4/warm-standby.html#STREAMING-REPLICATION-SLOTS).
117
+
118
+ ## Running the Watcher
119
+
120
+ With everything configured, start the Wal process:
121
+
122
+ ```bash
123
+ bundle exec wal start config/wal.yaml
124
+ ```
125
+
126
+ Wal will now process your replication slot and run the `DenormalizePostWatcher` whenever a change occur.
@@ -32,7 +32,7 @@ module Wal
32
32
  end
33
33
 
34
34
  def publication_name
35
- "#{watcher.underscore}_publication"
35
+ "#{class_name.gsub("Watcher", "").underscore}_publication"
36
36
  end
37
37
 
38
38
  def class_name
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.15"
4
+ VERSION = "0.0.16"
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.15"
10
+ VERSION = "0.0.16"
11
11
 
12
12
  class BeginTransactionEvent < T::Struct
13
13
  prop :transaction_id, Integer, immutable: true
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.15
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Navarro
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-09-16 00:00:00.000000000 Z
10
+ date: 2025-09-17 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: pg
@@ -104,7 +104,6 @@ files:
104
104
  - lib/wal/version.rb
105
105
  - lib/wal/watcher.rb
106
106
  - rbi/wal.rbi
107
- - sig/wal.rbs
108
107
  licenses:
109
108
  - MIT
110
109
  metadata:
data/sig/wal.rbs DELETED
@@ -1,186 +0,0 @@
1
- # typed: strong
2
- module Wal
3
- end
4
-
5
- Wal::Event: untyped
6
-
7
- Wal::VERSION: untyped
8
-
9
- class Wal::BeginTransactionEvent < T::Struct
10
- def estimated_size: () -> Integer
11
- end
12
-
13
- class Wal::CommitTransactionEvent < T::Struct
14
- end
15
-
16
- module Wal::ChangeEvent
17
- def diff: () -> ::Hash[String, [ untyped, untyped ]]
18
-
19
- def changed_attribute?: (Symbol | String attribute) -> bool
20
-
21
- def attribute: (Symbol | String attribute) -> untyped
22
-
23
- def attribute_changes: (Symbol | String attribute) -> [ untyped, untyped ]?
24
-
25
- def attribute_was: (Symbol | String attribute) -> untyped
26
- end
27
-
28
- class Wal::InsertEvent < T::Struct
29
- include ::Wal::ChangeEvent
30
-
31
- def diff: () -> ::Hash[String, [ untyped, untyped ]]
32
- end
33
-
34
- class Wal::UpdateEvent < T::Struct
35
- include ::Wal::ChangeEvent
36
-
37
- def diff: () -> ::Hash[String, [ untyped, untyped ]]
38
- end
39
-
40
- class Wal::DeleteEvent < T::Struct
41
- include ::Wal::ChangeEvent
42
-
43
- def diff: () -> ::Hash[String, [ untyped, untyped ]]
44
- end
45
-
46
- module Wal::ActiveRecordContextExtension
47
- def set_wal_watcher_context: (untyped context, ?prefix: untyped prefix) -> untyped
48
- end
49
-
50
- class Wal::NoopWatcher
51
- include Wal::Watcher
52
-
53
- def on_event: (Event event) -> void
54
- end
55
-
56
- class Wal::RecordWatcher
57
- include Wal::Watcher
58
-
59
- extend T::Helpers
60
-
61
- def self.inherited: (untyped subclass) -> untyped
62
-
63
- def self.on_insert: (String | singleton(::ActiveRecord::Base) table) { (InsertEvent event) -> void } -> void
64
-
65
- def self.on_update: (String | singleton(::ActiveRecord::Base) table, ?changed: ::Array[String | Symbol]? changed) { (UpdateEvent event) -> void } -> void
66
-
67
- def self.on_save: (String | singleton(::ActiveRecord::Base) table, ?changed: ::Array[String | Symbol]? changed) { (InsertEvent | UpdateEvent event) -> void } -> void
68
-
69
- def self.on_destroy: (String | singleton(::ActiveRecord::Base) table) { (DeleteEvent event) -> void } -> void
70
-
71
- def on_record_changed: (RecordEvent event) -> void
72
-
73
- def should_watch_table?: (String table) -> bool
74
-
75
- def aggregation_strategy: (BeginTransactionEvent event) -> Symbol
76
-
77
- def on_event: (Event event) -> void
78
- end
79
-
80
- Wal::Wal::RecordWatcher::RecordEvent: untyped
81
-
82
- class Wal::Wal::RecordWatcher::MemoryRecordWatcher
83
- include Wal::Watcher
84
-
85
- include Wal::Watcher::SeparatedEvents
86
-
87
- extend T::Helpers
88
-
89
- def initialize: (untyped watcher) -> void
90
-
91
- def on_begin: (BeginTransactionEvent event) -> void
92
-
93
- def on_commit: (untyped _event) -> untyped
94
-
95
- def on_insert: (InsertEvent event) -> void
96
-
97
- def on_update: (UpdateEvent event) -> void
98
-
99
- def on_delete: (DeleteEvent event) -> void
100
- end
101
-
102
- Wal::Wal::RecordWatcher::Wal::Wal::RecordWatcher::MemoryRecordWatcher::RecordsStorage: untyped
103
-
104
- class Wal::Wal::RecordWatcher::TemporaryTableRecordWatcher
105
- include Wal::Watcher
106
-
107
- include Wal::Watcher::SeparatedEvents
108
-
109
- extend T::Helpers
110
-
111
- def initialize: (untyped watcher, ?batch_size: untyped batch_size) -> void
112
-
113
- def on_begin: (BeginTransactionEvent event) -> void
114
-
115
- def on_commit: (untyped _event) -> untyped
116
-
117
- def on_insert: (InsertEvent event) -> void
118
-
119
- def on_update: (UpdateEvent event) -> void
120
-
121
- def on_delete: (DeleteEvent event) -> void
122
-
123
- def base_class: () -> singleton(::ActiveRecord::Base)
124
-
125
- def serialize: (untyped event) -> untyped
126
-
127
- def deserialize: (untyped persisted_event) -> untyped
128
- end
129
-
130
- class Wal::Replicator
131
- include PG::Replication::Protocol
132
-
133
- def initialize: (?replication_slot: String replication_slot, ?use_temporary_slot: bool use_temporary_slot, ?db_config: ::Hash[Symbol, untyped] db_config) -> void
134
-
135
- def replicate_forever: (Watcher watcher, ?publications: ::Array[String] publications) -> void
136
-
137
- def replicate: (Watcher watcher, ?publications: ::Array[String] publications) -> ::Enumerator::Lazy[Event]
138
- end
139
-
140
- class Wal::Wal::Replicator::Column < T::Struct
141
- def decode: (untyped value) -> untyped
142
- end
143
-
144
- class Wal::Wal::Replicator::Table < T::Struct
145
- def primary_key: (untyped decoded_row) -> untyped
146
-
147
- def decode_row: (untyped values) -> untyped
148
- end
149
-
150
- class Wal::StreamingWatcher
151
- include Wal::Watcher
152
-
153
- extend T::Helpers
154
-
155
- def on_transaction_events: (::Enumerator[Event] events) -> void
156
-
157
- def queue_size: (BeginTransactionEvent event) -> Integer
158
-
159
- def on_event: (Event event) -> void
160
- end
161
-
162
- module Wal::Watcher
163
- include Wal
164
-
165
- extend T::Helpers
166
-
167
- def on_event: (Event event) -> void
168
-
169
- def should_watch_table?: (String table) -> bool
170
-
171
- def valid_context_prefix?: (String prefix) -> bool
172
- end
173
-
174
- module Wal::Wal::Watcher::SeparatedEvents
175
- def on_event: (Event event) -> void
176
-
177
- def on_begin: (BeginTransactionEvent event) -> void
178
-
179
- def on_insert: (InsertEvent event) -> void
180
-
181
- def on_update: (UpdateEvent event) -> void
182
-
183
- def on_delete: (DeleteEvent event) -> void
184
-
185
- def on_commit: (CommitTransactionEvent event) -> void
186
- end