DCB-compatible event store library in Ruby with support for PostgreSQL. Join the Discord community.
En57 owns its PostgreSQL schema and tracks the installed schema version in
the database. Add the rake tasks to your application's Rakefile:
require "en57/tasks"Then install or update the schema with DATABASE_URL:
DATABASE_URL=postgres://localhost:5432/en57 bundle exec rake en57:migrateTo inspect the current schema status without applying changes:
DATABASE_URL=postgres://localhost:5432/en57 bundle exec rake en57:statusRun en57:migrate before using the event store for the first time.
Use EventStore.for_pg when En57 should own a pg connection.
event_store = En57::EventStore.for_pg("postgres://localhost:5432/en57")Use EventStore.for_sequel when your app already owns a Sequel database.
database = Sequel.connect("postgres://localhost:5432/en57")
event_store = En57::EventStore.for_sequel(database)Use EventStore.for_active_record when your app uses ActiveRecord.
ActiveRecord::Base.establish_connection("postgres://localhost:5432/en57")
event_store = En57::EventStore.for_active_recordevent_store.append(
[
En57::Event.new(
type: "OrderPlaced",
data: { amount: 100 },
tags: ["order_id:123", "customer:42"],
),
],
)events = event_store.read.each.to_aevent, position = event_store.read.each_with_position.firstevents = event_store.read.with_tag("order_id:123", "customer:42").each.to_aevents = event_store.read.after(42).each.to_aorders = event_store.read.of_type("OrderPlaced").with_tag("order_id:123")
price_changes = event_store.read.of_type("PriceChanged")
events = (orders | price_changes).each.to_aExample: consume credits only once per account.
account_scope = event_store.read.with_tag("account:x")
result = event_store.append(
[
En57::Event.new(
type: "CreditsUsed",
data: { amount: 100 },
tags: ["account:x"],
),
],
fail_if: account_scope.of_type("CreditsUsed"),
)
case result
in En57::Success(position:)
# credits consumed at event position
in En57::Failure(position:, conflicting_events:)
# lost the race; conflicting_events contains the events that matched
# the fail_if condition, with position set to the latest conflict
endTo ignore events at or before a known position, scope the fail_if condition
with after.
last_read_event_position = 42
event_store.append(
[En57::Event.new(type: "CreditsUsed", tags: ["account:x"])],
fail_if: event_store.read.of_type("CreditsUsed").after(last_read_event_position),
)Example: ensure no event exists with this email tag before writing.
email_tag = "email:alice@example.com"
result = event_store.append(
[
En57::Event.new(
type: "UserRegistered",
data: { name: "Alice" },
tags: [email_tag],
),
],
fail_if: event_store.read.with_tag(email_tag),
)
case result
in En57::Success(position:)
# user registered at event position
in En57::Failure(position:, conflicting_events:)
# email already used; conflicting_events contains the matching event
endThe development environment is managed with devenv. It pins the Ruby and PostgreSQL toolchain through Nix, so the only prerequisites are Nix (with flakes) and devenv.
Enter the environment:
devenv shellThis provides Ruby, PostgreSQL, and the formatters, and installs the gem
dependencies (bundle install, via the dev:setup task) on entry.
Tasks are run with devenv tasks run:
| Task | What it does |
|---|---|
test |
Run the whole test: namespace (unit, mutation, pg_regress) |
test:unit |
Run the unit tests (bin/m test) |
test:mutate |
Run mutation testing (mutant) for changes since MUTANT_SINCE (defaults to HEAD) |
test:pg |
Run the pg_regress suite against an ephemeral PostgreSQL |
dev:format |
Format Ruby and SQL with treefmt (syntax_tree + sqlfluff) |
pg-regress is also available as a standalone script in the shell.
CI runs devenv tasks run test. The devenv config also wires up Claude Code
hooks: edited files are formatted with treefmt, and the test suite runs at the
end of each agent loop.