//! E2E tests for HAVING clause transition correctness (F25: G6.2). //! //! Validates that groups crossing the HAVING threshold in and out, //! and rows migrating between groups, produce correct differential results. //! //! Prerequisites: `./tests/build_e2e_image.sh` mod e2e; use e2e::E2eDb; // ═══════════════════════════════════════════════════════════════════════ // Group crosses HAVING threshold: appears / disappears // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_having_group_crosses_threshold_up() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE hv_up (id SERIAL PRIMARY KEY, grp TEXT, val INT)") .await; db.execute("INSERT INTO hv_up (grp, val) VALUES ('a', 10), ('b', 5)") .await; let q = "SELECT grp, SUM(val) AS total FROM hv_up GROUP BY grp HAVING SUM(val) > 20"; db.create_st("hv_up_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("hv_up_st", q).await; // Group 'a' crosses threshold (10 + 15 = 25 > 20) db.execute("INSERT INTO hv_up (grp, val) VALUES ('a', 15)") .await; db.refresh_st("hv_up_st").await; db.assert_st_matches_query("hv_up_st", q).await; // Group 'b' also crosses db.execute("INSERT INTO hv_up (grp, val) VALUES ('b', 20)") .await; db.refresh_st("hv_up_st").await; db.assert_st_matches_query("hv_up_st", q).await; } #[tokio::test] async fn test_having_group_crosses_threshold_down() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE hv_down (id SERIAL PRIMARY KEY, grp TEXT, val INT)") .await; db.execute("INSERT INTO hv_down (grp, val) VALUES ('a', 30), ('a', 10), ('b', 50)") .await; let q = "SELECT grp, SUM(val) AS total FROM hv_down GROUP BY grp HAVING SUM(val) > 20"; db.create_st("hv_down_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("hv_down_st", q).await; // Remove from 'a' so sum drops below threshold (30→10) db.execute("DELETE FROM hv_down WHERE grp = 'a' AND val = 30") .await; db.refresh_st("hv_down_st").await; db.assert_st_matches_query("hv_down_st", q).await; // Reduce further — 'a' total = 10, still below db.execute("UPDATE hv_down SET val = 5 WHERE grp = 'a' AND val = 10") .await; db.refresh_st("hv_down_st").await; db.assert_st_matches_query("hv_down_st", q).await; } // ═══════════════════════════════════════════════════════════════════════ // Group oscillates across threshold (up → down → up) // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_having_threshold_oscillation() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE hv_osc (id SERIAL PRIMARY KEY, grp TEXT, val INT)") .await; db.execute("INSERT INTO hv_osc (grp, val) VALUES ('a', 15), ('a', 10)") .await; let q = "SELECT grp, SUM(val) AS total FROM hv_osc GROUP BY grp HAVING SUM(val) >= 30"; db.create_st("hv_osc_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("hv_osc_st", q).await; // Cross up: 15+10+10 = 35 >= 30 db.execute("INSERT INTO hv_osc (grp, val) VALUES ('a', 10)") .await; db.refresh_st("hv_osc_st").await; db.assert_st_matches_query("hv_osc_st", q).await; // Cross down: delete 15 → 10+10 = 20 < 30 db.execute("DELETE FROM hv_osc WHERE val = 15").await; db.refresh_st("hv_osc_st").await; db.assert_st_matches_query("hv_osc_st", q).await; // Cross up again: +20 → 10+10+20 = 40 >= 30 db.execute("INSERT INTO hv_osc (grp, val) VALUES ('a', 20)") .await; db.refresh_st("hv_osc_st").await; db.assert_st_matches_query("hv_osc_st", q).await; } // ═══════════════════════════════════════════════════════════════════════ // Row migrates between groups (UPDATE grp column) // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_having_row_group_migration() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE hv_mig (id SERIAL PRIMARY KEY, grp TEXT, val INT)") .await; db.execute("INSERT INTO hv_mig (grp, val) VALUES ('a', 25), ('a', 10), ('b', 5)") .await; let q = "SELECT grp, SUM(val) AS total FROM hv_mig GROUP BY grp HAVING SUM(val) > 20"; db.create_st("hv_mig_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("hv_mig_st", q).await; // Move val=25 from 'a' to 'b': a drops (10 ≤ 20), b rises (5+25=30 > 20) db.execute("UPDATE hv_mig SET grp = 'b' WHERE val = 25") .await; db.refresh_st("hv_mig_st").await; db.assert_st_matches_query("hv_mig_st", q).await; // Move it back db.execute("UPDATE hv_mig SET grp = 'a' WHERE val = 25") .await; db.refresh_st("hv_mig_st").await; db.assert_st_matches_query("hv_mig_st", q).await; } // ═══════════════════════════════════════════════════════════════════════ // HAVING with COUNT condition // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_having_count_threshold() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE hv_cnt (id SERIAL PRIMARY KEY, grp TEXT, val TEXT)") .await; db.execute("INSERT INTO hv_cnt (grp, val) VALUES ('a', 'x'), ('a', 'y'), ('b', 'z')") .await; let q = "SELECT grp, COUNT(*) AS cnt FROM hv_cnt GROUP BY grp HAVING COUNT(*) >= 2"; db.create_st("hv_cnt_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("hv_cnt_st", q).await; // 'b' reaches threshold db.execute("INSERT INTO hv_cnt (grp, val) VALUES ('b', 'w')") .await; db.refresh_st("hv_cnt_st").await; db.assert_st_matches_query("hv_cnt_st", q).await; // 'a' drops below db.execute("DELETE FROM hv_cnt WHERE grp = 'a' AND val = 'x'") .await; db.refresh_st("hv_cnt_st").await; db.assert_st_matches_query("hv_cnt_st", q).await; } // ═══════════════════════════════════════════════════════════════════════ // Multiple HAVING conditions (SUM AND COUNT) // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_having_multiple_conditions() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE hv_multi (id SERIAL PRIMARY KEY, grp TEXT, val INT)") .await; db.execute( "INSERT INTO hv_multi (grp, val) VALUES \ ('a', 10), ('a', 20), ('a', 30), ('b', 100), ('c', 5), ('c', 5)", ) .await; let q = "SELECT grp, SUM(val) AS total, COUNT(*) AS cnt \ FROM hv_multi GROUP BY grp \ HAVING SUM(val) > 15 AND COUNT(*) >= 2"; db.create_st("hv_multi_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("hv_multi_st", q).await; // 'b' gets a second row → now passes COUNT cond db.execute("INSERT INTO hv_multi (grp, val) VALUES ('b', 50)") .await; db.refresh_st("hv_multi_st").await; db.assert_st_matches_query("hv_multi_st", q).await; // Remove from 'a' until SUM drops below 15 db.execute("DELETE FROM hv_multi WHERE grp = 'a' AND val = 30") .await; db.execute("DELETE FROM hv_multi WHERE grp = 'a' AND val = 20") .await; db.refresh_st("hv_multi_st").await; db.assert_st_matches_query("hv_multi_st", q).await; } // ═══════════════════════════════════════════════════════════════════════ // Group complete elimination (delete all rows in group) // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_having_group_complete_elimination() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE hv_elim (id SERIAL PRIMARY KEY, grp TEXT, val INT)") .await; db.execute("INSERT INTO hv_elim (grp, val) VALUES ('a', 50), ('a', 60), ('b', 100)") .await; let q = "SELECT grp, SUM(val) AS total FROM hv_elim GROUP BY grp HAVING SUM(val) > 30"; db.create_st("hv_elim_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("hv_elim_st", q).await; // Delete all 'a' rows db.execute("DELETE FROM hv_elim WHERE grp = 'a'").await; db.refresh_st("hv_elim_st").await; db.assert_st_matches_query("hv_elim_st", q).await; // Re-add 'a' above threshold db.execute("INSERT INTO hv_elim (grp, val) VALUES ('a', 999)") .await; db.refresh_st("hv_elim_st").await; db.assert_st_matches_query("hv_elim_st", q).await; }