Project

wal

0.0
The project is in a healthy, maintained state
Hook up into Postgres WAL log
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies
 Project Readme

Wal

Wal is a framework that lets you hook into Postgres WAL events directly from your Rails application.

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.

Also, unlike ActiveRecord callbacks, these events are guaranteed by Postgres to be 100% consistent, ensuring you never miss one.

Getting started

Installation

Add wal to your application's Gemfile:

gem "wal"

And then:

$ bundle install

Getting started

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.

For example, let's create a watcher that denormalizes Post and Category models into a DenormalizedPost.

Create a new file at app/watchers/denormalize_post_watcher.rb:

class DenormalizePostWatcher < Wal::RecordWatcher
  # When a new `Post` is created, we create a new `DenormalizedPost` record
  on_insert Post do |event|
    DenormalizedPost.create!(
      post_id: event.primary_key,
      title: event.new["title"],
      body: event.new["body"],
      category_id: event.new["category_id"],
      category_name: Category.find_by(id: event.new["category_id"])&.name,
    )
  end

  # When a `Post` title or body is changed, we update its `DenormalizedPost` record
  on_update Post, changed: [:title, :body] do |event|
    DenormalizedPost
      .where(post_id: event.primary_key)
      .update_all(
        title: event.new["title"],
        body: event.new["body"],
      )
  end

  # When a `Post` category changes, we also update its `DenormalizedPost` record
  on_update Post, changed: [:category_id] do |event|
    DenormalizedPost
      .where(post_id: event.primary_key)
      .update_all(
        category_id: event.new["category_id"],
        category_name: Category.find_by(id: event.new["category_id"])&.name,
      )
  end

  # When a `Category` changes, we update all the `DenormalizedPosts` referencing it
  on_update Category, changed: [:name] do |event|
    DenormalizedPost
      .where(category_id: event.primary_key)
      .update_all(
        category_name: event.new["name"],
      )
  end

  # Finally when a `Category` is deleted, we clear all the `DenormalizedPosts` referencing it
  on_delete Category do |event|
    DenormalizedPost
      .where(category_id: event.primary_key)
      .update_all(
        category_id: nil,
        category_name: nil,
      )
  end
end

You might wonder: Why not just use ActiveRecord callbacks for this?

And while it is hard to justify that for our simple example, ActiveRecord callbacks are not guaranteed to always run. Depending on the methods you use to perform the changes, they can be skipped.

Wal ensures every single change is captured. Even when updates happen directly in the database and bypass Rails entirely. That's the main reason to use it: when you need 100% consistency.

Usually one could resort into database triggers when full consistency is required, but running and maintaining application level code on the database tends to be painful. Wal let's you do the same but at the application level.

Configuring the Watcher

Wal relies on Postgres logical replication to stream changes to your watchers.

First, create a Postgres publication for the tables your watcher uses. Wal provides a generator for this:

$ rails generate wal:migration DenormalizePostWatcher

This will generate a new migration with all the tables that your watcher uses:

class SetDenormalizePostWatcherPublication < ActiveRecord::Migration
  def change
    define_publication :denormalize_post_publication do |p|
      p.table :posts
      p.table :categories
    end
  end
end

Next, create a config/wal.yml configuration file to link the Watcher to its publication:

slots:
  denormalize_posts:
    watcher: DenormalizePostWatcher
    publications:
      - denormalize_post_publication

This associates your watcher with the denormalize_post_publication and with the denormalize_posts Postgres replication slot.

Running the Watcher

With everything configured, start the Wal process:

bundle exec wal start config/wal.yaml

Wal will now process your replication slot and run the DenormalizePostWatcher whenever a change occur.