feat(alerts): V17 migration — drop ACKNOWLEDGED, add read_at + deleted_at
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
-- V17 — Alerts: drop ACKNOWLEDGED state, add read_at/deleted_at, drop alert_reads,
|
||||||
|
-- rework open-rule unique index predicate to survive ack (acked no longer "closed").
|
||||||
|
|
||||||
|
-- 1. Coerce ACKNOWLEDGED rows → FIRING (acked_at already set on these rows)
|
||||||
|
UPDATE alert_instances SET state = 'FIRING' WHERE state = 'ACKNOWLEDGED';
|
||||||
|
|
||||||
|
-- 2. Swap alert_state_enum to remove ACKNOWLEDGED (Postgres can't drop enum values in place)
|
||||||
|
-- First drop all indexes that reference alert_state_enum so ALTER COLUMN can proceed.
|
||||||
|
DROP INDEX IF EXISTS alert_instances_open_rule_uq;
|
||||||
|
DROP INDEX IF EXISTS alert_instances_inbox_idx;
|
||||||
|
DROP INDEX IF EXISTS alert_instances_open_rule_idx;
|
||||||
|
DROP INDEX IF EXISTS alert_instances_resolved_idx;
|
||||||
|
|
||||||
|
CREATE TYPE alert_state_enum_v2 AS ENUM ('PENDING','FIRING','RESOLVED');
|
||||||
|
ALTER TABLE alert_instances
|
||||||
|
ALTER COLUMN state TYPE alert_state_enum_v2
|
||||||
|
USING state::text::alert_state_enum_v2;
|
||||||
|
DROP TYPE alert_state_enum;
|
||||||
|
ALTER TYPE alert_state_enum_v2 RENAME TO alert_state_enum;
|
||||||
|
|
||||||
|
-- Recreate the non-unique indexes that were dropped above
|
||||||
|
CREATE INDEX alert_instances_inbox_idx ON alert_instances (environment_id, state, fired_at DESC);
|
||||||
|
CREATE INDEX alert_instances_open_rule_idx ON alert_instances (rule_id, state) WHERE rule_id IS NOT NULL;
|
||||||
|
CREATE INDEX alert_instances_resolved_idx ON alert_instances (resolved_at) WHERE state = 'RESOLVED';
|
||||||
|
|
||||||
|
-- 3. New orthogonal flag columns
|
||||||
|
ALTER TABLE alert_instances
|
||||||
|
ADD COLUMN read_at timestamptz NULL,
|
||||||
|
ADD COLUMN deleted_at timestamptz NULL;
|
||||||
|
|
||||||
|
CREATE INDEX alert_instances_unread_idx
|
||||||
|
ON alert_instances (environment_id, read_at)
|
||||||
|
WHERE read_at IS NULL AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX alert_instances_deleted_idx
|
||||||
|
ON alert_instances (deleted_at)
|
||||||
|
WHERE deleted_at IS NOT NULL;
|
||||||
|
|
||||||
|
-- 4. Rework the V13/V15/V16 open-rule uniqueness index:
|
||||||
|
-- - drop ACKNOWLEDGED from the predicate (ack no longer "closes")
|
||||||
|
-- - add "AND deleted_at IS NULL" so a soft-deleted row frees the slot
|
||||||
|
DROP INDEX IF EXISTS alert_instances_open_rule_uq;
|
||||||
|
CREATE UNIQUE INDEX alert_instances_open_rule_uq
|
||||||
|
ON alert_instances (rule_id, (COALESCE(
|
||||||
|
context->>'_subjectFingerprint',
|
||||||
|
context->'exchange'->>'id',
|
||||||
|
'')))
|
||||||
|
WHERE rule_id IS NOT NULL
|
||||||
|
AND state IN ('PENDING','FIRING')
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- 5. Drop the per-user reads table — read is now global on alert_instances.read_at
|
||||||
|
DROP TABLE alert_reads;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.cameleer.server.app.alerting.storage;
|
||||||
|
|
||||||
|
import com.cameleer.server.app.AbstractPostgresIT;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class V17MigrationIT extends AbstractPostgresIT {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void alert_state_enum_drops_acknowledged() {
|
||||||
|
var values = jdbcTemplate.queryForList("""
|
||||||
|
SELECT unnest(enum_range(NULL::alert_state_enum))::text AS v
|
||||||
|
""", String.class);
|
||||||
|
assertThat(values).containsExactlyInAnyOrder("PENDING", "FIRING", "RESOLVED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void read_at_and_deleted_at_columns_exist() {
|
||||||
|
var cols = jdbcTemplate.queryForList("""
|
||||||
|
SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_name = 'alert_instances'
|
||||||
|
AND column_name IN ('read_at','deleted_at')
|
||||||
|
""", String.class);
|
||||||
|
assertThat(cols).containsExactlyInAnyOrder("read_at", "deleted_at");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void alert_reads_table_is_gone() {
|
||||||
|
Integer count = jdbcTemplate.queryForObject("""
|
||||||
|
SELECT COUNT(*)::int FROM information_schema.tables
|
||||||
|
WHERE table_name = 'alert_reads'
|
||||||
|
""", Integer.class);
|
||||||
|
assertThat(count).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void open_rule_index_predicate_is_reworked() {
|
||||||
|
String def = jdbcTemplate.queryForObject("""
|
||||||
|
SELECT pg_get_indexdef(indexrelid)
|
||||||
|
FROM pg_index
|
||||||
|
JOIN pg_class ON pg_class.oid = pg_index.indexrelid
|
||||||
|
WHERE pg_class.relname = 'alert_instances_open_rule_uq'
|
||||||
|
""", String.class);
|
||||||
|
assertThat(def).contains("state = ANY (ARRAY['PENDING'::alert_state_enum, 'FIRING'::alert_state_enum])");
|
||||||
|
assertThat(def).contains("deleted_at IS NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user