feat: use postgres
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
-- +goose Up
|
||||
-- records holds one durable notification record per accepted intent. The
|
||||
-- (producer, idempotency_key) UNIQUE constraint replaces the previous Redis
|
||||
-- idempotency keyspace: the durable row IS the idempotency reservation.
|
||||
CREATE TABLE records (
|
||||
notification_id text PRIMARY KEY,
|
||||
notification_type text NOT NULL,
|
||||
producer text NOT NULL,
|
||||
audience_kind text NOT NULL,
|
||||
recipient_user_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
payload_json text NOT NULL,
|
||||
idempotency_key text NOT NULL,
|
||||
request_fingerprint text NOT NULL,
|
||||
request_id text NOT NULL DEFAULT '',
|
||||
trace_id text NOT NULL DEFAULT '',
|
||||
occurred_at timestamptz NOT NULL,
|
||||
accepted_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
idempotency_expires_at timestamptz NOT NULL,
|
||||
CONSTRAINT records_idempotency_unique UNIQUE (producer, idempotency_key)
|
||||
);
|
||||
|
||||
-- Newest-first listing index used by operator/audit reads.
|
||||
CREATE INDEX records_listing_idx
|
||||
ON records (accepted_at DESC, notification_id DESC);
|
||||
|
||||
-- routes stores one row per (notification_id, route_id). next_attempt_at is
|
||||
-- non-NULL only while the row is a scheduling candidate (status pending or
|
||||
-- failed); the partial index keeps the scheduler scan tight.
|
||||
CREATE TABLE routes (
|
||||
notification_id text NOT NULL
|
||||
REFERENCES records(notification_id) ON DELETE CASCADE,
|
||||
route_id text NOT NULL,
|
||||
channel text NOT NULL,
|
||||
recipient_ref text NOT NULL,
|
||||
status text NOT NULL,
|
||||
attempt_count integer NOT NULL DEFAULT 0,
|
||||
max_attempts integer NOT NULL,
|
||||
next_attempt_at timestamptz,
|
||||
resolved_email text NOT NULL DEFAULT '',
|
||||
resolved_locale text NOT NULL DEFAULT '',
|
||||
last_error_classification text NOT NULL DEFAULT '',
|
||||
last_error_message text NOT NULL DEFAULT '',
|
||||
last_error_at timestamptz,
|
||||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
published_at timestamptz,
|
||||
dead_lettered_at timestamptz,
|
||||
skipped_at timestamptz,
|
||||
PRIMARY KEY (notification_id, route_id)
|
||||
);
|
||||
|
||||
-- Drives the publishers' due-route pull. Partial predicate keeps the index
|
||||
-- narrow: terminal rows (published / dead_letter / skipped) never appear.
|
||||
CREATE INDEX routes_due_idx
|
||||
ON routes (next_attempt_at)
|
||||
WHERE next_attempt_at IS NOT NULL;
|
||||
|
||||
-- Coarse status / channel filters used by operator views.
|
||||
CREATE INDEX routes_status_idx ON routes (status);
|
||||
CREATE INDEX routes_channel_idx ON routes (channel);
|
||||
|
||||
-- dead_letters carries the operator-visible record for one route that
|
||||
-- exhausted automated handling. Cascade tied to the parent route row so a
|
||||
-- record-level retention DELETE clears dependent dead-letter rows naturally.
|
||||
CREATE TABLE dead_letters (
|
||||
notification_id text NOT NULL,
|
||||
route_id text NOT NULL,
|
||||
channel text NOT NULL,
|
||||
recipient_ref text NOT NULL,
|
||||
final_attempt_count integer NOT NULL,
|
||||
max_attempts integer NOT NULL,
|
||||
failure_classification text NOT NULL,
|
||||
failure_message text NOT NULL,
|
||||
recovery_hint text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL,
|
||||
PRIMARY KEY (notification_id, route_id),
|
||||
FOREIGN KEY (notification_id, route_id)
|
||||
REFERENCES routes(notification_id, route_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX dead_letters_listing_idx
|
||||
ON dead_letters (created_at DESC, notification_id DESC, route_id DESC);
|
||||
|
||||
-- malformed_intents stores operator-visible records for stream entries the
|
||||
-- intent validator could not accept. Independent retention pass.
|
||||
CREATE TABLE malformed_intents (
|
||||
stream_entry_id text PRIMARY KEY,
|
||||
notification_type text NOT NULL DEFAULT '',
|
||||
producer text NOT NULL DEFAULT '',
|
||||
idempotency_key text NOT NULL DEFAULT '',
|
||||
failure_code text NOT NULL,
|
||||
failure_message text NOT NULL,
|
||||
raw_fields jsonb NOT NULL,
|
||||
recorded_at timestamptz NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX malformed_intents_listing_idx
|
||||
ON malformed_intents (recorded_at DESC, stream_entry_id DESC);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE IF EXISTS malformed_intents;
|
||||
DROP TABLE IF EXISTS dead_letters;
|
||||
DROP TABLE IF EXISTS routes;
|
||||
DROP TABLE IF EXISTS records;
|
||||
@@ -0,0 +1,19 @@
|
||||
// Package migrations exposes the embedded goose migration files used by
|
||||
// Notification Service to provision its `notification` schema in PostgreSQL.
|
||||
//
|
||||
// The embedded filesystem is consumed by `pkg/postgres.RunMigrations` during
|
||||
// notification-service startup and by `cmd/jetgen` when regenerating the
|
||||
// `internal/adapters/postgres/jet/` code against a transient PostgreSQL
|
||||
// instance.
|
||||
package migrations
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.sql
|
||||
var fs embed.FS
|
||||
|
||||
// FS returns the embedded filesystem containing every numbered goose
|
||||
// migration shipped with Notification Service.
|
||||
func FS() embed.FS {
|
||||
return fs
|
||||
}
|
||||
Reference in New Issue
Block a user