refactor(schema): collapse V1..V18 into single V1__init.sql baseline
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m4s
CI / docker (push) Successful in 1m17s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled

The project is still greenfield (no production deployment) so this is
the last safe moment to flatten the migration archaeology before the
checksum history starts mattering for real.

Schema changes
 - 18 migration files (531 lines) → one V1__init.sql (~380 lines)
   declaring the final end-state: RBAC + claim mappings + runtime
   management + config + audit + outbound + alerting, plus seed data
   (system roles, Admins group, default environment).
 - Drops the data-repair statements from V14 (firemode backfill),
   V16 (subjectFingerprint migration), V17 (ACKNOWLEDGED → FIRING
   coercion) — they were no-ops on any DB that starts at V1.
 - Declares condition_kind_enum with AGENT_LIFECYCLE from the start
   (was added retroactively by V18).
 - Declares alert_state_enum with three values only (was five, then
   swapped in V17) and alert_instances with read_at / deleted_at
   columns from day one (was added by V17).
 - alert_reads table never created (V12 created, V17 dropped).
 - alert_instances_open_rule_uq built with the V17 predicate from
   the start.

Test changes
 - Replace V12MigrationIT / V17MigrationIT / V18MigrationIT with one
   SchemaBootstrapIT that asserts the combined invariants: tables
   present, alert_reads absent, enum value sets, alert_instances has
   read_at + deleted_at, open_rule_uq exists and is unique, env-delete
   cascade fires.

Verification
 - pg_dump of the new V1 matches the pg_dump of V1..V18 applied in
   sequence (bytewise modulo column order and Postgres-auto FK names).
 - Full alerting IT suite (53 tests across 6 classes) green against
   the new schema.
 - The 47 pre-existing test failures on main (AgentRegistrationIT,
   SearchControllerIT, ClickHouseStatsStoreIT, …) are unrelated and
   fail identically without this change.

Developer impact
 - Existing local DBs will fail checksum validation on boot. Wipe:
   docker compose down -v  (or drop the tenant_default schema).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-21 20:52:22 +02:00
parent 74bfabf618
commit 90083f886a
24 changed files with 468 additions and 638 deletions

View File

@@ -0,0 +1,147 @@
package com.cameleer.server.app.alerting.storage;
import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.search.ClickHouseLogStore;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Invariants of the consolidated V1 bootstrap schema. Replaces the per-migration
* ITs (V12/V17/V18) that existed while the schema evolved across 18 files.
*/
class SchemaBootstrapIT extends AbstractPostgresIT {
@MockBean(name = "clickHouseLogStore")
ClickHouseLogStore clickHouseLogStore;
private UUID testEnvId;
private String testUserId;
@AfterEach
void cleanup() {
if (testEnvId != null) jdbcTemplate.update("DELETE FROM environments WHERE id = ?", testEnvId);
if (testUserId != null) jdbcTemplate.update("DELETE FROM users WHERE user_id = ?", testUserId);
}
@Test
void all_alerting_tables_exist() {
var tables = jdbcTemplate.queryForList("""
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('alert_rules','alert_rule_targets','alert_instances',
'alert_silences','alert_notifications')
""", String.class);
assertThat(tables).containsExactlyInAnyOrder(
"alert_rules", "alert_rule_targets", "alert_instances",
"alert_silences", "alert_notifications");
}
@Test
void alert_reads_table_absent() {
Integer count = jdbcTemplate.queryForObject("""
SELECT COUNT(*)::int FROM information_schema.tables
WHERE table_name = 'alert_reads'
""", Integer.class);
assertThat(count).isZero();
}
@Test
void alerting_enums_exist() {
var enums = jdbcTemplate.queryForList("""
SELECT typname FROM pg_type
WHERE typname IN ('severity_enum','condition_kind_enum','alert_state_enum',
'target_kind_enum','notification_status_enum')
""", String.class);
assertThat(enums).containsExactlyInAnyOrder(
"severity_enum", "condition_kind_enum", "alert_state_enum",
"target_kind_enum", "notification_status_enum");
}
@Test
void alert_state_enum_values() {
var values = jdbcTemplate.queryForList("""
SELECT unnest(enum_range(NULL::alert_state_enum))::text
""", String.class);
assertThat(values).containsExactlyInAnyOrder("PENDING", "FIRING", "RESOLVED");
}
@Test
void condition_kind_enum_values() {
var values = jdbcTemplate.queryForList("""
SELECT unnest(enum_range(NULL::condition_kind_enum))::text
""", String.class);
assertThat(values).containsExactlyInAnyOrder(
"ROUTE_METRIC", "EXCHANGE_MATCH", "AGENT_STATE", "AGENT_LIFECYCLE",
"DEPLOYMENT_STATE", "LOG_PATTERN", "JVM_METRIC");
}
@Test
void alert_instances_has_read_at_and_deleted_at() {
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 open_rule_unique_index_exists() {
Integer count = jdbcTemplate.queryForObject("""
SELECT COUNT(*)::int FROM pg_indexes
WHERE indexname = 'alert_instances_open_rule_uq'
AND tablename = 'alert_instances'
""", Integer.class);
assertThat(count).isEqualTo(1);
Boolean isUnique = jdbcTemplate.queryForObject("""
SELECT indisunique FROM pg_index
JOIN pg_class ON pg_class.oid = pg_index.indexrelid
WHERE pg_class.relname = 'alert_instances_open_rule_uq'
""", Boolean.class);
assertThat(isUnique).isTrue();
}
@Test
void deleting_environment_cascades_alerting_rows() {
testEnvId = UUID.randomUUID();
testUserId = UUID.randomUUID().toString();
jdbcTemplate.update(
"INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)",
testEnvId, "test-cascade-env-" + testEnvId, "Test Cascade Env");
jdbcTemplate.update(
"INSERT INTO users (user_id, provider, email) VALUES (?, ?, ?)",
testUserId, "local", "cascade@example.com");
var ruleId = UUID.randomUUID();
jdbcTemplate.update(
"INSERT INTO alert_rules (id, environment_id, name, severity, condition_kind, condition, " +
"notification_title_tmpl, notification_message_tmpl, created_by, updated_by) " +
"VALUES (?, ?, 'r', 'WARNING', 'AGENT_STATE', '{}'::jsonb, 't', 'm', ?, ?)",
ruleId, testEnvId, testUserId, testUserId);
var instanceId = UUID.randomUUID();
jdbcTemplate.update(
"INSERT INTO alert_instances (id, rule_id, rule_snapshot, environment_id, state, severity, " +
"fired_at, context, title, message) VALUES (?, ?, '{}'::jsonb, ?, 'FIRING', 'WARNING', " +
"now(), '{}'::jsonb, 't', 'm')",
instanceId, ruleId, testEnvId);
jdbcTemplate.update("DELETE FROM environments WHERE id = ?", testEnvId);
assertThat(jdbcTemplate.queryForObject(
"SELECT count(*) FROM alert_rules WHERE environment_id = ?",
Integer.class, testEnvId)).isZero();
assertThat(jdbcTemplate.queryForObject(
"SELECT count(*) FROM alert_instances WHERE environment_id = ?",
Integer.class, testEnvId)).isZero();
testEnvId = null; // already deleted; skip @AfterEach
}
}

View File

@@ -1,83 +0,0 @@
package com.cameleer.server.app.alerting.storage;
import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.search.ClickHouseLogStore;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.assertj.core.api.Assertions.assertThat;
class V12MigrationIT extends AbstractPostgresIT {
@MockBean(name = "clickHouseLogStore") ClickHouseLogStore clickHouseLogStore;
private java.util.UUID testEnvId;
private String testUserId;
@AfterEach
void cleanup() {
if (testEnvId != null) jdbcTemplate.update("DELETE FROM environments WHERE id = ?", testEnvId);
if (testUserId != null) jdbcTemplate.update("DELETE FROM users WHERE user_id = ?", testUserId);
}
@Test
void allAlertingTablesAndEnumsExist() {
// Note: alert_reads was created in V12 but dropped by V17 (superseded by read_at column).
var tables = jdbcTemplate.queryForList(
"SELECT table_name FROM information_schema.tables WHERE table_schema='public' " +
"AND table_name IN ('alert_rules','alert_rule_targets','alert_instances'," +
"'alert_silences','alert_notifications')",
String.class);
assertThat(tables).containsExactlyInAnyOrder(
"alert_rules","alert_rule_targets","alert_instances",
"alert_silences","alert_notifications");
var enums = jdbcTemplate.queryForList(
"SELECT typname FROM pg_type WHERE typname IN " +
"('severity_enum','condition_kind_enum','alert_state_enum'," +
"'target_kind_enum','notification_status_enum')",
String.class);
assertThat(enums).containsExactlyInAnyOrder(
"severity_enum", "condition_kind_enum", "alert_state_enum",
"target_kind_enum", "notification_status_enum");
}
@Test
void deletingEnvironmentCascadesAlertingRows() {
testEnvId = java.util.UUID.randomUUID();
testUserId = java.util.UUID.randomUUID().toString();
jdbcTemplate.update(
"INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)",
testEnvId, "test-cascade-env-" + testEnvId, "Test Cascade Env");
jdbcTemplate.update(
"INSERT INTO users (user_id, provider, email) VALUES (?, ?, ?)",
testUserId, "local", "test@example.com");
var ruleId = java.util.UUID.randomUUID();
jdbcTemplate.update(
"INSERT INTO alert_rules (id, environment_id, name, severity, condition_kind, condition, " +
"notification_title_tmpl, notification_message_tmpl, created_by, updated_by) " +
"VALUES (?, ?, 'r', 'WARNING', 'AGENT_STATE', '{}'::jsonb, 't', 'm', ?, ?)",
ruleId, testEnvId, testUserId, testUserId);
var instanceId = java.util.UUID.randomUUID();
jdbcTemplate.update(
"INSERT INTO alert_instances (id, rule_id, rule_snapshot, environment_id, state, severity, " +
"fired_at, context, title, message) VALUES (?, ?, '{}'::jsonb, ?, 'FIRING', 'WARNING', " +
"now(), '{}'::jsonb, 't', 'm')",
instanceId, ruleId, testEnvId);
jdbcTemplate.update("DELETE FROM environments WHERE id = ?", testEnvId);
assertThat(jdbcTemplate.queryForObject(
"SELECT count(*) FROM alert_rules WHERE environment_id = ?",
Integer.class, testEnvId)).isZero();
assertThat(jdbcTemplate.queryForObject(
"SELECT count(*) FROM alert_instances WHERE environment_id = ?",
Integer.class, testEnvId)).isZero();
// testEnvId already deleted; null it so @AfterEach doesn't attempt a no-op delete
testEnvId = null;
}
}

View File

@@ -1,58 +0,0 @@
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_exists_and_is_unique() {
// Structural check only — the pg_get_indexdef pretty-printer varies across
// Postgres versions. Predicate semantics (ack doesn't close; soft-delete
// frees the slot; RESOLVED excluded) are covered behaviorally by
// PostgresAlertInstanceRepositoryIT#findOpenForRule_* and
// #save_rejectsSecondOpenInstanceForSameRuleAndExchange.
Integer count = jdbcTemplate.queryForObject("""
SELECT COUNT(*)::int FROM pg_indexes
WHERE indexname = 'alert_instances_open_rule_uq'
AND tablename = 'alert_instances'
""", Integer.class);
assertThat(count).isEqualTo(1);
Boolean isUnique = jdbcTemplate.queryForObject("""
SELECT indisunique FROM pg_index
JOIN pg_class ON pg_class.oid = pg_index.indexrelid
WHERE pg_class.relname = 'alert_instances_open_rule_uq'
""", Boolean.class);
assertThat(isUnique).isTrue();
}
}

View File

@@ -1,20 +0,0 @@
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 V18MigrationIT extends AbstractPostgresIT {
@Test
void condition_kind_enum_includes_agent_lifecycle() {
var values = jdbcTemplate.queryForList("""
SELECT unnest(enum_range(NULL::condition_kind_enum))::text AS v
""", String.class);
assertThat(values).contains("AGENT_LIFECYCLE");
assertThat(values).containsExactlyInAnyOrder(
"ROUTE_METRIC", "EXCHANGE_MATCH", "AGENT_STATE", "AGENT_LIFECYCLE",
"DEPLOYMENT_STATE", "LOG_PATTERN", "JVM_METRIC");
}
}