Basic usage

Simply add use Lazarus to your repo module:

defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.Postgres
  use Lazarus
end

That does three things:

  • Blocks direct Repo.delete* and Repo.delete_all* calls unless the schema or table is bypassed
  • Adds explicit Repo.soft_delete* and Repo.hard_delete* functions
  • Hides soft-deleted rows from normal reads and skips them in updates by default

Bypassing schemas and tables

If your repo absolutely NEEDS to leave specific schemas/tables alone, you can whitelist schema modules and table names by passing in a list of schemas as bypass_schemas and a list of table names as bypass_tables.

Bypassed sources are left alone by Lazarus. Reads, updates, and deletes against them use ordinary Ecto behavior including Repo.delete, and Repo.delete_all where those operations apply. These options were designed with third party modules/libraries in mind that run queries, updates, and deletes on your repo (e.g. when Oban reads from, updates, or deletes jobs from the oban_jobs table).

When a bypassed source is the root of an Ecto query, Lazarus leaves that query alone, including fragments inside it. A bypassed join source is allowed, but it does not make unrelated fragments elsewhere in the query safe.

Bypasses do not apply to direct raw SQL calls such as Repo.query!/3 because Lazarus cannot inspect contents of raw SQL queries. See Overcoming Limitations for more info.

Example

use Lazarus,
  bypass_schemas: [SessionToken],
  bypass_tables: ["audit_logs"]

Which option should you use?

  • bypass_schemas whitelists schemas and their source tables
  • bypass_tables whitelists tables only (useful for schema-less tables)

If you have a schema module SessionToken with a table "session_tokens", bypassing just the schema module is sufficient enough, e.g. bypass_schemas: [SessionToken]. Adding the table name to bypass_tables is redundant.

If your database has a table and no schema for it, you can bypass it by name, e.g. bypass_tables: ["audit_logs"].

If you have a schema for your table, and you decide to only bypass the table name and not the schema module itself (for whatever reason), you might end up with some inconsistent bypass results:

# Success (query operations)
Repo.update_all(
  from(t in SessionToken, where: t.expires_at < ^DateTime.utc_now()),
  set: [expired: true]
)

Repo.update_all(
  from(t in "session_tokens", where: field(t, :expires_at) < ^DateTime.utc_now()),
  set: [expired: true]
)

Repo.delete_all(from t in SessionToken, where: t.expires_at < ^DateTime.utc_now())
Repo.delete_all(from t in "session_tokens", where: field(t, :expires_at) < ^DateTime.utc_now())

# Raises (schemas, changesets)
Repo.delete(session_token)
Repo.delete(session_token_changeset)
Repo.delete_all(SessionToken)

Real-world example with Oban

Bypasses Oban.Job schema with "oban_jobs" table, and a schema-less "oban_peers" table

use Lazarus,
  bypass_schemas: [Oban.Job],
  bypass_tables: ["oban_peers"]