//! E2E tests for FULL OUTER JOIN differential correctness (F18+F26). //! //! Validates FULL JOIN behaviour under INSERT, UPDATE, DELETE, including //! NULL join keys (F26) and row migrations across matched/unmatched sides. //! //! Prerequisites: `./tests/build_e2e_image.sh` mod e2e; use e2e::E2eDb; // ═══════════════════════════════════════════════════════════════════════ // Basic FULL JOIN differential // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_full_join_basic_differential() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE fj_left (id SERIAL PRIMARY KEY, key INT, lval TEXT)") .await; db.execute("CREATE TABLE fj_right (id SERIAL PRIMARY KEY, key INT, rval TEXT)") .await; db.execute("INSERT INTO fj_left (key, lval) VALUES (1, 'a'), (2, 'b'), (3, 'c')") .await; db.execute("INSERT INTO fj_right (key, rval) VALUES (2, 'x'), (3, 'y'), (4, 'z')") .await; let q = "SELECT l.key AS lkey, l.lval, r.key AS rkey, r.rval \ FROM fj_left l FULL OUTER JOIN fj_right r ON l.key = r.key"; db.create_st("fj_basic_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("fj_basic_st", q).await; // INSERT on left only — creates new unmatched row db.execute("INSERT INTO fj_left (key, lval) VALUES (5, 'd')") .await; db.refresh_st("fj_basic_st").await; db.assert_st_matches_query("fj_basic_st", q).await; // INSERT on right matching existing left row db.execute("INSERT INTO fj_right (key, rval) VALUES (1, 'w')") .await; db.refresh_st("fj_basic_st").await; db.assert_st_matches_query("fj_basic_st", q).await; // DELETE from left — unmatched right side db.execute("DELETE FROM fj_left WHERE key = 2").await; db.refresh_st("fj_basic_st").await; db.assert_st_matches_query("fj_basic_st", q).await; // UPDATE right side value db.execute("UPDATE fj_right SET rval = 'updated' WHERE key = 3") .await; db.refresh_st("fj_basic_st").await; db.assert_st_matches_query("fj_basic_st", q).await; } // ═══════════════════════════════════════════════════════════════════════ // FULL JOIN with NULL join keys (F26) // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_full_join_null_keys_differential() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE fj_nl (id SERIAL PRIMARY KEY, key INT, val TEXT)") .await; db.execute("CREATE TABLE fj_nr (id SERIAL PRIMARY KEY, key INT, val TEXT)") .await; db.execute("INSERT INTO fj_nl (key, val) VALUES (1, 'a'), (NULL, 'null_left')") .await; db.execute("INSERT INTO fj_nr (key, val) VALUES (1, 'x'), (NULL, 'null_right')") .await; let q = "SELECT l.key AS lkey, l.val AS lval, r.key AS rkey, r.val AS rval \ FROM fj_nl l FULL OUTER JOIN fj_nr r ON l.key = r.key"; db.create_st("fj_null_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("fj_null_st", q).await; // Add another NULL key row on left db.execute("INSERT INTO fj_nl (key, val) VALUES (NULL, 'null_left_2')") .await; db.refresh_st("fj_null_st").await; db.assert_st_matches_query("fj_null_st", q).await; // Remove the right NULL key row db.execute("DELETE FROM fj_nr WHERE key IS NULL").await; db.refresh_st("fj_null_st").await; db.assert_st_matches_query("fj_null_st", q).await; // Update a non-null key to NULL db.execute("UPDATE fj_nl SET key = NULL WHERE key = 1") .await; db.refresh_st("fj_null_st").await; db.assert_st_matches_query("fj_null_st", q).await; } // ═══════════════════════════════════════════════════════════════════════ // FULL JOIN — join key updates (row migrates matched ↔ unmatched) // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_full_join_key_update_migration() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE fj_ml (id SERIAL PRIMARY KEY, key INT, val TEXT)") .await; db.execute("CREATE TABLE fj_mr (id SERIAL PRIMARY KEY, key INT, val TEXT)") .await; db.execute("INSERT INTO fj_ml (key, val) VALUES (1, 'a'), (2, 'b')") .await; db.execute("INSERT INTO fj_mr (key, val) VALUES (1, 'x'), (3, 'z')") .await; let q = "SELECT l.key AS lkey, l.val AS lval, r.key AS rkey, r.val AS rval \ FROM fj_ml l FULL OUTER JOIN fj_mr r ON l.key = r.key"; db.create_st("fj_key_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("fj_key_st", q).await; // Change left key=2 to key=3 → now matches right side db.execute("UPDATE fj_ml SET key = 3 WHERE key = 2").await; db.refresh_st("fj_key_st").await; db.assert_st_matches_query("fj_key_st", q).await; // Change left key=3 to key=99 → now unmatched again db.execute("UPDATE fj_ml SET key = 99 WHERE key = 3").await; db.refresh_st("fj_key_st").await; db.assert_st_matches_query("fj_key_st", q).await; } // ═══════════════════════════════════════════════════════════════════════ // FULL JOIN with aggregation // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_full_join_with_aggregate_differential() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE fj_al (id SERIAL PRIMARY KEY, dept TEXT, budget INT)") .await; db.execute("CREATE TABLE fj_ar (id SERIAL PRIMARY KEY, dept TEXT, revenue INT)") .await; db.execute("INSERT INTO fj_al (dept, budget) VALUES ('eng', 100), ('eng', 200), ('sales', 50)") .await; db.execute("INSERT INTO fj_ar (dept, revenue) VALUES ('eng', 500), ('mkt', 300)") .await; let q = "SELECT COALESCE(l.dept, r.dept) AS dept, \ SUM(l.budget) AS total_budget, SUM(r.revenue) AS total_revenue \ FROM fj_al l \ FULL OUTER JOIN fj_ar r ON l.dept = r.dept \ GROUP BY COALESCE(l.dept, r.dept)"; db.create_st("fj_agg_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("fj_agg_st", q).await; db.execute("INSERT INTO fj_al (dept, budget) VALUES ('mkt', 75)") .await; db.refresh_st("fj_agg_st").await; db.assert_st_matches_query("fj_agg_st", q).await; db.execute("DELETE FROM fj_ar WHERE dept = 'eng'").await; db.refresh_st("fj_agg_st").await; db.assert_st_matches_query("fj_agg_st", q).await; } // ═══════════════════════════════════════════════════════════════════════ // FULL JOIN — multi-column join key // ═══════════════════════════════════════════════════════════════════════ #[tokio::test] async fn test_full_join_multi_column_key_differential() { let db = E2eDb::new().await.with_extension().await; db.execute("CREATE TABLE fj_mcl (id SERIAL PRIMARY KEY, a INT, b TEXT, lval INT)") .await; db.execute("CREATE TABLE fj_mcr (id SERIAL PRIMARY KEY, a INT, b TEXT, rval INT)") .await; db.execute("INSERT INTO fj_mcl (a, b, lval) VALUES (1, 'x', 10), (2, 'y', 20)") .await; db.execute("INSERT INTO fj_mcr (a, b, rval) VALUES (1, 'x', 100), (3, 'z', 300)") .await; let q = "SELECT l.a AS la, l.b AS lb, l.lval, r.a AS ra, r.b AS rb, r.rval \ FROM fj_mcl l FULL OUTER JOIN fj_mcr r ON l.a = r.a AND l.b = r.b"; db.create_st("fj_mc_st", q, "1m", "DIFFERENTIAL").await; db.assert_st_matches_query("fj_mc_st", q).await; db.execute("INSERT INTO fj_mcr (a, b, rval) VALUES (2, 'y', 200)") .await; db.refresh_st("fj_mc_st").await; db.assert_st_matches_query("fj_mc_st", q).await; db.execute("DELETE FROM fj_mcl WHERE a = 1").await; db.refresh_st("fj_mc_st").await; db.assert_st_matches_query("fj_mc_st", q).await; }