This is the canonical 1.x to 2.0 upgrade guide for Mailglass.
The one change that matters in 2.0 is where mailglass keeps its tables. In
1.x, mailglass installed its four domain tables into your public Postgres
schema alongside your own. In 2.0, they default to a dedicated mailglass
Postgres schema instead. This isolates mailglass from your public namespace so
its tables, trigger, and function no longer share a schema with your application.
The four tables are mailglass_events, mailglass_deliveries,
mailglass_suppressions, and mailglass_webhook_events.
There are two supported upgrade routes. Read both, pick one, and execute it. Most existing installs should reach for Route A first; new-schema adoption (Route B) is the recommended long-term shape but it is a real migration, so choose it deliberately.
Who this guide is for
Use this guide if you run an existing 1.x mailglass deployment — your four
mailglass tables already live in public — and you are moving onto the 2.0
contract. If you are installing mailglass fresh on 2.0, you do not need this
guide: the default mailglass schema is created for you and you never touch
public.
Route A — keep public (zero data movement)
If you do not want to move data, opt out of the new default with one line:
config :mailglass, :schema, "public"That is the whole upgrade. Everything keeps working exactly as it did on 1.x
because your tables never move. This is the explicit public opt-out — it is
the safest path and the recommended advice for large existing installs where a
schema move is not worth the coordination.
Choose Route A when:
- you want the smallest possible
2.0diff - you cannot schedule even a brief migration window
- other systems read
mailglass_*tables by theirpublic-qualified names and you are not ready to requalify them (see the grep checklist below)
Route A is a conscious, permanent choice, not a temporary bridge. You can adopt
the mailglass schema later via Route B whenever it suits you.
Route B — adopt the mailglass schema (recommended long-term)
Route B moves your four existing tables from public into the mailglass
schema. Mailglass ships a first-class Mix task that writes the migration for you —
you review it, then run it like any other migration.
mix mailglass.upgrade.v2_schema
mix ecto.migrate
mix mailglass.upgrade.v2_schema generates a MoveMailglassToSchema migration in
your priv/repo/migrations/. Read the generated file before you migrate. It:
- opens with
SET LOCAL lock_timeout = '5s'so the moves fail fast instead of queue-blocking (see the locking posture below) - runs
CREATE SCHEMA IF NOT EXISTS "mailglass" - runs
ALTER TABLE public.<table> SET SCHEMA "mailglass"for all four tables - recreates the append-only immutability function and trigger, schema-qualified
under
mailglass, byte-identical to a fresh2.0install - ships a real
down/0that reverses the move back intopublic
Why the move is safe
ALTER TABLE … SET SCHEMAis metadata-only — it swaps the table's schema reference and is instant regardless of table size. There is no table rewrite.- Indexes, constraints, and column-owned sequences move with the table. mailglass primary keys are UUIDs, so there are no standalone sequences to worry about.
- Foreign keys, views, and functions that reference the tables resolve by
internal identity, so they keep working across the move. The one thing that
does not follow automatically is any literal
public.mailglass_*string in your own SQL — see the grep checklist. - The
citextextension stays inpublic. The migration never touches it; only the tables move. Case-insensitive address matching keeps working. - The version-marker comment survives the move, so mailglass still reports the correct installed schema version after the migration and will not try to re-run earlier migrations.
- The immutability function is recreated, not moved. In
1.xthe trigger's function was created unqualified inpublic; the generated migration drops it and recreates it qualified undermailglass. This is the single manual step, and the task does it for you.
The public.mailglass_* grep checklist
Schema qualification cannot rescue hard-coded strings. If any of your own SQL,
views, functions, or dashboards reference a mailglass table by its
public-qualified name — for example public.mailglass_events — that reference
will break after the move because the table is no longer in public.
Before you migrate, grep your codebase:
grep -rn "public.mailglass_" .
For each hit, either requalify it to mailglass.<table> or drop the schema
prefix entirely and let search_path resolve it. This is the only application-
side breakage the move can cause, and it is entirely mechanical to fix.
Locking posture and retry
ALTER TABLE … SET SCHEMA takes an ACCESS EXCLUSIVE lock. The lock is cheap to
hold — the move is metadata-only and instant once granted — but it queues behind
long-running readers and blocks new queries once it is waiting. The generated
migration guards against this with SET LOCAL lock_timeout = '5s': the schema
moves fail fast with SQLSTATE 55P03 (lock_not_available) instead of stalling
your traffic behind a long reader.
The whole migration runs in a single transaction, so a lock timeout rolls the
entire move back cleanly — you never end up with tables split across two schemas.
If the migration aborts with 55P03, it means a long-running query held one of
the tables. Retry mix ecto.migrate off-peak; the move is metadata-only and
completes instantly once the lock is granted.
Do not add @disable_ddl_transaction to the generated migration. SET LOCAL
only takes effect inside a transaction, and the transactional wrapper is what
makes a lock abort roll back safely.
Locked-down production roles (create_schema: false)
If your application's database role lacks CREATE privilege on the database — a
common hardened-production posture — a DBA must pre-create the schema and grant
access, then you run the migration with schema creation skipped:
CREATE SCHEMA mailglass AUTHORIZATION app_role;
GRANT USAGE ON SCHEMA mailglass TO app_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA mailglass GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_role;Then invoke the migration path with create_schema: false so mailglass skips the
CREATE SCHEMA statement (the DBA already made it):
Mailglass.Migration.up(create_schema: false)This mirrors the same create_schema: false posture Oban and other Ecto-based
libraries use for roles that cannot create schemas themselves.
Rollback
- Route A: revert the one
config :mailglass, :schema, "public"line. - Route B: the generated migration ships a real
down/0that moves the four tables back intopublicand restores thepublic-qualified immutability trigger and function. Because the moves are metadata-only, rollback is fast. You can also stop after a partial rollback and stay onconfig :mailglass, :schema, "public"(Route A) indefinitely.
Verification before shipping
After you migrate, confirm the tables landed where you expect:
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_name LIKE 'mailglass_%';All four tables should report mailglass as their table_schema (Route B) or
public (Route A). Then run your normal test and compile checks before you ship.
Upgrade outcome
You are 2.0-ready when:
- you have chosen Route A or Route B deliberately, not by default
- if you took Route B, all four tables report the
mailglassschema and yourpublic.mailglass_*grep is clean - if you took Route A, the explicit
config :mailglass, :schema, "public"opt-out is in place and your tables are untouched