Files
yovision/deploy/postgres/018_bell_alerts.sql
T
QiuSW 8208118904
Harness governance / validate (pull_request) Has been cancelled
feat(bell): add alert acknowledgement vertical slice
2026-08-11 17:01:53 +08:00

146 lines
6.6 KiB
SQL

-- Bell v6 append-only rule evaluation, Alert identity and state transitions.
CREATE TABLE IF NOT EXISTS bell.rule_versions (
id text PRIMARY KEY,
tenant_id bigint NOT NULL,
site_id bigint,
rule_key text NOT NULL,
version integer NOT NULL,
display_name text NOT NULL,
event_kind text NOT NULL,
minimum_severity text NOT NULL,
enabled boolean NOT NULL,
effective_from timestamptz NOT NULL,
config_hash bytea NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CONSTRAINT bell_rule_version_id CHECK (id ~ '^ruv_[0-9A-HJKMNP-TV-Z]{26}$'),
CONSTRAINT bell_rule_version_scope CHECK (tenant_id >= 1 AND (site_id IS NULL OR site_id >= 1)),
CONSTRAINT bell_rule_version_key CHECK (rule_key ~ '^[a-z][a-z0-9_-]{2,63}$'),
CONSTRAINT bell_rule_version_number CHECK (version >= 1),
CONSTRAINT bell_rule_version_name CHECK (char_length(btrim(display_name)) BETWEEN 1 AND 120),
CONSTRAINT bell_rule_version_kind CHECK (event_kind ~ '^[a-z][a-z0-9_]{2,63}$'),
CONSTRAINT bell_rule_version_severity CHECK (minimum_severity IN ('low','medium','high','critical')),
CONSTRAINT bell_rule_version_hash CHECK (octet_length(config_hash) = 32),
UNIQUE (tenant_id, rule_key, version),
UNIQUE (tenant_id, rule_key, config_hash)
);
ALTER TABLE bell.rule_versions OWNER TO bell_app;
CREATE TABLE IF NOT EXISTS bell.event_rule_sweeps (
event_id text PRIMARY KEY REFERENCES bell.events(id),
swept_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
ALTER TABLE bell.event_rule_sweeps OWNER TO bell_app;
CREATE TABLE IF NOT EXISTS bell.rule_evaluations (
id text PRIMARY KEY,
event_id text NOT NULL REFERENCES bell.events(id),
rule_version_id text NOT NULL REFERENCES bell.rule_versions(id),
result text NOT NULL,
reason text NOT NULL,
evaluated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CONSTRAINT bell_rule_evaluation_id CHECK (id ~ '^eva_[0-9A-HJKMNP-TV-Z]{26}$'),
CONSTRAINT bell_rule_evaluation_result CHECK (result IN ('matched','no_match')),
CONSTRAINT bell_rule_evaluation_reason CHECK (reason IN ('matched','disabled','event_kind','severity')),
UNIQUE (event_id, rule_version_id)
);
ALTER TABLE bell.rule_evaluations OWNER TO bell_app;
CREATE TABLE IF NOT EXISTS bell.alerts (
id text PRIMARY KEY,
tenant_id bigint NOT NULL,
site_id bigint NOT NULL,
rule_evaluation_id text NOT NULL UNIQUE REFERENCES bell.rule_evaluations(id),
rule_version_id text NOT NULL REFERENCES bell.rule_versions(id),
severity text NOT NULL,
title text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CONSTRAINT bell_alert_id CHECK (id ~ '^alt_[0-9A-HJKMNP-TV-Z]{26}$'),
CONSTRAINT bell_alert_scope CHECK (tenant_id >= 1 AND site_id >= 1),
CONSTRAINT bell_alert_severity CHECK (severity IN ('low','medium','high','critical')),
CONSTRAINT bell_alert_title CHECK (char_length(btrim(title)) BETWEEN 1 AND 120)
);
ALTER TABLE bell.alerts OWNER TO bell_app;
CREATE TABLE IF NOT EXISTS bell.alert_events (
alert_id text NOT NULL REFERENCES bell.alerts(id),
event_id text NOT NULL REFERENCES bell.events(id),
linked_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (alert_id, event_id),
UNIQUE (event_id, alert_id)
);
ALTER TABLE bell.alert_events OWNER TO bell_app;
CREATE TABLE IF NOT EXISTS bell.alert_transitions (
id text PRIMARY KEY,
alert_id text NOT NULL REFERENCES bell.alerts(id),
sequence integer NOT NULL,
from_state text,
to_state text NOT NULL,
actor_ref text NOT NULL,
note text,
occurred_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CONSTRAINT bell_alert_transition_id CHECK (id ~ '^trn_[0-9A-HJKMNP-TV-Z]{26}$'),
CONSTRAINT bell_alert_transition_sequence CHECK (sequence >= 1),
CONSTRAINT bell_alert_transition_from CHECK (from_state IS NULL OR from_state IN ('open','acknowledged')),
CONSTRAINT bell_alert_transition_to CHECK (to_state IN ('open','acknowledged','closed')),
CONSTRAINT bell_alert_transition_edge CHECK (
(sequence = 1 AND from_state IS NULL AND to_state = 'open') OR
(sequence > 1 AND from_state = 'open' AND to_state = 'acknowledged') OR
(sequence > 1 AND from_state = 'acknowledged' AND to_state = 'closed')
),
CONSTRAINT bell_alert_transition_actor CHECK (
char_length(actor_ref) BETWEEN 1 AND 80 AND actor_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:@/-]*$'
),
CONSTRAINT bell_alert_transition_note CHECK (note IS NULL OR char_length(note) <= 500),
UNIQUE (alert_id, sequence)
);
ALTER TABLE bell.alert_transitions OWNER TO bell_app;
CREATE TABLE IF NOT EXISTS bell.alert_command_receipts (
tenant_id bigint NOT NULL,
idempotency_key text NOT NULL,
command_hash bytea NOT NULL,
response_status integer NOT NULL,
response_body jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (tenant_id, idempotency_key),
CONSTRAINT bell_alert_command_tenant CHECK (tenant_id >= 1),
CONSTRAINT bell_alert_command_key CHECK (
char_length(idempotency_key) BETWEEN 8 AND 128
AND idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]*$'
),
CONSTRAINT bell_alert_command_hash CHECK (octet_length(command_hash) = 32),
CONSTRAINT bell_alert_command_status CHECK (response_status IN (200,409)),
CONSTRAINT bell_alert_command_body CHECK (jsonb_typeof(response_body) = 'object')
);
ALTER TABLE bell.alert_command_receipts OWNER TO bell_app;
DO $immutable$
DECLARE
table_name text;
BEGIN
FOREACH table_name IN ARRAY ARRAY[
'rule_versions','event_rule_sweeps','rule_evaluations','alerts',
'alert_events','alert_transitions','alert_command_receipts'
] LOOP
EXECUTE format('DROP TRIGGER IF EXISTS %I ON bell.%I', 'bell_' || table_name || '_immutable', table_name);
EXECUTE format(
'CREATE TRIGGER %I BEFORE UPDATE OR DELETE ON bell.%I FOR EACH ROW EXECUTE FUNCTION bell.reject_immutable_change()',
'bell_' || table_name || '_immutable', table_name
);
END LOOP;
END
$immutable$;
CREATE INDEX IF NOT EXISTS bell_rule_versions_effective_idx
ON bell.rule_versions(tenant_id, rule_key, effective_from DESC, version DESC);
CREATE INDEX IF NOT EXISTS bell_alerts_scope_time_idx
ON bell.alerts(tenant_id, site_id, created_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS bell_alert_events_event_idx ON bell.alert_events(event_id, alert_id);
CREATE INDEX IF NOT EXISTS bell_alert_transitions_latest_idx
ON bell.alert_transitions(alert_id, sequence DESC);
INSERT INTO bell.schema_migrations(version) VALUES (6)
ON CONFLICT (version) DO NOTHING;