-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmigrate.py
More file actions
90 lines (74 loc) · 2.94 KB
/
Copy pathmigrate.py
File metadata and controls
90 lines (74 loc) · 2.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
"""Idempotent schema bootstrap.
The customer's database is empty until they put something in it. The first
thing every teesql-backed Python service does on startup is run the
migration to ensure its schema exists. Re-running is a no-op.
The schema:
events (id BIGSERIAL, topic TEXT, payload JSONB, recorded_at TIMESTAMPTZ)
Plus an `events_notify` trigger that fires `pg_notify('events_channel', ...)`
on every INSERT. The example doesn't currently consume the NOTIFY because
postgres LISTEN/NOTIFY is NOT replicated to streaming standbys — instead
the secondary poller does `WHERE id > last_seen` on a 50ms cadence (see
`poller.py`). The trigger stays anyway so:
1. Forks that prefer LISTEN/NOTIFY against the PRIMARY can flip the
poller into a listener without a schema migration.
2. The cluster's WAL records the trigger fire — operationally useful
to confirm that writes actually committed.
Migrations are forward-only and idempotent: `CREATE TABLE/TRIGGER IF NOT
EXISTS`, `CREATE OR REPLACE FUNCTION`. No `DROP` paths.
"""
from __future__ import annotations
import logging
import psycopg
log = logging.getLogger(__name__)
_MIGRATIONS: list[tuple[str, str]] = [
(
"0001_events",
"""
CREATE TABLE IF NOT EXISTS events (
id BIGSERIAL PRIMARY KEY,
topic TEXT NOT NULL,
payload JSONB NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS events_id_idx
ON events (id);
CREATE INDEX IF NOT EXISTS events_recorded_at_idx
ON events (recorded_at DESC);
""",
),
(
"0002_events_notify_trigger",
"""
CREATE OR REPLACE FUNCTION events_notify() RETURNS trigger AS $func$
BEGIN
PERFORM pg_notify(
'events_channel',
json_build_object(
'id', NEW.id,
'topic', NEW.topic,
'payload', NEW.payload,
'recorded_at', NEW.recorded_at
)::text
);
RETURN NEW;
END;
$func$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS events_notify_after_insert ON events;
CREATE TRIGGER events_notify_after_insert
AFTER INSERT ON events
FOR EACH ROW EXECUTE FUNCTION events_notify();
""",
),
]
def apply_migrations(conn: psycopg.Connection) -> None:
"""Apply every migration in `_MIGRATIONS`. Idempotent.
No bookkeeping table — every DDL is `IF NOT EXISTS` or `OR REPLACE`.
Forks that grow beyond a couple tables should swap this for alembic
and a proper schema_version table.
"""
for name, ddl in _MIGRATIONS:
log.info("applying migration %s", name)
with conn.cursor() as cur:
cur.execute(ddl)
conn.commit()
log.info("migrations complete (%d total)", len(_MIGRATIONS))