diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java index 2726cf88..54a79afb 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java @@ -151,11 +151,7 @@ public class AlertController { } private List inEnvLiveIds(List ids, UUID envId) { - return ids.stream() - .filter(id -> instanceRepo.findById(id) - .map(i -> i.environmentId().equals(envId) && i.deletedAt() == null) - .orElse(false)) - .toList(); + return instanceRepo.filterInEnvLive(ids, envId); } private String currentUserId() { diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java index e76fab97..395296dc 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java @@ -196,7 +196,7 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository c.createArrayOf("uuid", ids.toArray())); jdbc.update(""" UPDATE alert_instances SET read_at = ? - WHERE id = ANY(?) AND read_at IS NULL + WHERE id = ANY(?) AND read_at IS NULL AND deleted_at IS NULL """, Timestamp.from(when), idArray); } @@ -262,6 +262,17 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository """, rowMapper(), Timestamp.from(now)); } + @Override + public List filterInEnvLive(List ids, UUID environmentId) { + if (ids == null || ids.isEmpty()) return List.of(); + Array idArray = jdbc.execute((ConnectionCallback) c -> + c.createArrayOf("uuid", ids.toArray())); + return jdbc.query(""" + SELECT id FROM alert_instances + WHERE id = ANY(?) AND environment_id = ? AND deleted_at IS NULL + """, (rs, i) -> (UUID) rs.getObject("id"), idArray, environmentId); + } + @Override public void deleteResolvedBefore(Instant cutoff) { jdbc.update(""" diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java index 6e53a91c..5ee591a0 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java @@ -372,6 +372,18 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { assertThat(rows).extracting(AlertInstance::id).contains(inst.id()); } + @Test + void filterInEnvLive_excludes_other_env_and_soft_deleted() { + var a = insertFreshFiring(); // env envId, live + var b = insertFreshFiring(); // env envId, will be soft-deleted + repo.softDelete(b.id(), Instant.now()); + + UUID unknownId = UUID.randomUUID(); // not in DB at all + + var kept = repo.filterInEnvLive(List.of(a.id(), b.id(), unknownId), envId); + assertThat(kept).containsExactly(a.id()); + } + // ------------------------------------------------------------------------- /** Creates and saves a fresh FIRING instance targeted at the test userId with its own rule. */ diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V12MigrationIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V12MigrationIT.java index 5f59e421..a6d5bcd7 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V12MigrationIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V12MigrationIT.java @@ -22,14 +22,15 @@ class V12MigrationIT extends AbstractPostgresIT { @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','alert_reads')", + "'alert_silences','alert_notifications')", String.class); assertThat(tables).containsExactlyInAnyOrder( "alert_rules","alert_rule_targets","alert_instances", - "alert_silences","alert_notifications","alert_reads"); + "alert_silences","alert_notifications"); var enums = jdbcTemplate.queryForList( "SELECT typname FROM pg_type WHERE typname IN " + diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java index 964cffe6..2522b3b4 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java @@ -73,4 +73,10 @@ public interface AlertInstanceRepository { void bulkAck(List ids, String userId, Instant when); List listFiringDueForReNotify(Instant now); + + /** + * Filter the given IDs to those that exist in the given environment and are not + * soft-deleted. Single SQL round-trip — avoids N+1 in bulk operations. + */ + List filterInEnvLive(List ids, UUID environmentId); }