feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
@@ -0,0 +1,134 @@
-- +goose Up
-- deliveries holds one durable record per accepted logical mail delivery.
-- The (source, idempotency_key) UNIQUE constraint replaces the previous Redis
-- idempotency keyspace: the durable row IS the idempotency reservation.
-- next_attempt_at is populated for deliveries whose active attempt is due in
-- the future and drives the attempt scheduler's `FOR UPDATE SKIP LOCKED` pull.
CREATE TABLE deliveries (
delivery_id text PRIMARY KEY,
resend_parent_delivery_id text NOT NULL DEFAULT '',
source text NOT NULL,
status text NOT NULL,
payload_mode text NOT NULL,
template_id text NOT NULL DEFAULT '',
locale text NOT NULL DEFAULT '',
locale_fallback_used boolean NOT NULL DEFAULT false,
template_variables jsonb,
attachments jsonb,
subject text NOT NULL DEFAULT '',
text_body text NOT NULL DEFAULT '',
html_body text NOT NULL DEFAULT '',
idempotency_key text NOT NULL,
request_fingerprint text NOT NULL,
idempotency_expires_at timestamptz NOT NULL,
attempt_count integer NOT NULL DEFAULT 0,
last_attempt_status text NOT NULL DEFAULT '',
provider_summary text NOT NULL DEFAULT '',
next_attempt_at timestamptz,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
sent_at timestamptz,
suppressed_at timestamptz,
failed_at timestamptz,
dead_lettered_at timestamptz,
CONSTRAINT deliveries_idempotency_unique UNIQUE (source, idempotency_key)
);
-- Drives the scheduler's due-attempt pull. The partial predicate keeps the
-- index narrow: rows in terminal status (sent/suppressed/failed/dead_letter)
-- never appear here.
CREATE INDEX deliveries_due_idx
ON deliveries (next_attempt_at)
WHERE next_attempt_at IS NOT NULL;
-- Drives the recovery pass (deliveries currently held by an in-progress
-- attempt whose worker may have crashed).
CREATE INDEX deliveries_sending_idx
ON deliveries (status)
WHERE status = 'sending';
-- Newest-first listing index used by the operator delivery list surface.
CREATE INDEX deliveries_listing_idx
ON deliveries (created_at DESC, delivery_id DESC);
-- Coarse status / source / template filters used by the operator listing.
CREATE INDEX deliveries_status_idx ON deliveries (status);
CREATE INDEX deliveries_source_idx ON deliveries (source);
CREATE INDEX deliveries_template_id_idx ON deliveries (template_id) WHERE template_id <> '';
-- delivery_recipients normalises the SMTP envelope so future recipient-
-- filtered listing slots in without touching the deliveries row layout.
-- 'reply_to' addresses are stored for round-trip fidelity but excluded from
-- the email index per the prior keyspace rule.
CREATE TABLE delivery_recipients (
delivery_id text NOT NULL REFERENCES deliveries(delivery_id) ON DELETE CASCADE,
kind text NOT NULL,
position integer NOT NULL,
email text NOT NULL,
PRIMARY KEY (delivery_id, kind, position),
CONSTRAINT delivery_recipients_kind_check
CHECK (kind IN ('to', 'cc', 'bcc', 'reply_to'))
);
CREATE INDEX delivery_recipients_email_idx
ON delivery_recipients (email)
WHERE kind <> 'reply_to';
-- attempts stores the immutable execution history of one delivery. attempt_no
-- is monotonically increasing per delivery, starting at 1.
CREATE TABLE attempts (
delivery_id text NOT NULL REFERENCES deliveries(delivery_id) ON DELETE CASCADE,
attempt_no integer NOT NULL,
status text NOT NULL,
scheduled_for timestamptz NOT NULL,
started_at timestamptz,
finished_at timestamptz,
provider_classification text NOT NULL DEFAULT '',
provider_summary text NOT NULL DEFAULT '',
PRIMARY KEY (delivery_id, attempt_no)
);
-- dead_letters holds the operator-visible record for one delivery that
-- exhausted automated handling.
CREATE TABLE dead_letters (
delivery_id text PRIMARY KEY REFERENCES deliveries(delivery_id) ON DELETE CASCADE,
final_attempt_no integer NOT NULL,
failure_classification text NOT NULL,
provider_summary text NOT NULL DEFAULT '',
recovery_hint text NOT NULL DEFAULT '',
created_at timestamptz NOT NULL
);
-- delivery_payloads stores the raw generic-delivery attachment bundle
-- referenced by the delivery row. The payload column carries the
-- acceptgenericdelivery.DeliveryPayload JSON shape; raw attachment bytes
-- remain inside that JSON value as base64 strings.
CREATE TABLE delivery_payloads (
delivery_id text PRIMARY KEY REFERENCES deliveries(delivery_id) ON DELETE CASCADE,
payload jsonb NOT NULL
);
-- malformed_commands stores operator-visible records for stream commands the
-- intake validator could not accept.
CREATE TABLE malformed_commands (
stream_entry_id text PRIMARY KEY,
delivery_id text NOT NULL DEFAULT '',
source 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
);
-- Newest-first listing index used by the operator malformed-command list.
CREATE INDEX malformed_commands_listing_idx
ON malformed_commands (recorded_at DESC, stream_entry_id DESC);
-- +goose Down
DROP TABLE IF EXISTS malformed_commands;
DROP TABLE IF EXISTS delivery_payloads;
DROP TABLE IF EXISTS dead_letters;
DROP TABLE IF EXISTS attempts;
DROP TABLE IF EXISTS delivery_recipients;
DROP TABLE IF EXISTS deliveries;
@@ -0,0 +1,19 @@
// Package migrations exposes the embedded goose migration files used by Mail
// Service to provision its `mail` schema in PostgreSQL.
//
// The embedded filesystem is consumed by `pkg/postgres.RunMigrations` during
// mail-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 Mail Service.
func FS() embed.FS {
return fs
}