-- postgresql-0002-0004.sql — Functional tests for CWE-407 fixes
--
-- Tests correctness of the three postgresql defect fixes:
--   0002  preptlist.c:180,206,316  tlist_member in MERGE/RETURNING var collection
--   0003  equivclass.c:1041        list_member in equivalence-class EC matching
--   0004  analyzejoins.c:1914      list_member in join-elimination reltarget merge
--
-- Run with: psql -f postgresql-0002-0004.sql
--           or: psql -v ON_ERROR_STOP=1 -f postgresql-0002-0004.sql
--
-- All tests are idempotent.  Schema is dropped and recreated at the top.
-- Requires PostgreSQL 15+ (MERGE support for 0002 tests).

\set ON_ERROR_STOP 1

BEGIN;

-- ── Setup ────────────────────────────────────────────────────────────────────

DROP SCHEMA IF EXISTS cwe407_test CASCADE;
CREATE SCHEMA cwe407_test;
SET search_path TO cwe407_test;

-- ═══════════════════════════════════════════════════════════════════════════
-- 0002 — preptlist.c: MERGE/UPDATE/RETURNING Var dedup
--
-- Tests that MERGE, UPDATE...RETURNING, and multi-source queries produce
-- correct results with many columns (exercising the tlist dedup code path).
-- ═══════════════════════════════════════════════════════════════════════════

-- Wide target table (many columns to stress the tlist_member dedup loop)
CREATE TABLE target_wide (
    id      int PRIMARY KEY,
    c01 int, c02 int, c03 int, c04 int, c05 int,
    c06 int, c07 int, c08 int, c09 int, c10 int,
    c11 int, c12 int, c13 int, c14 int, c15 int,
    c16 int, c17 int, c18 int, c19 int, c20 int
);

CREATE TABLE source_wide (
    id      int PRIMARY KEY,
    c01 int, c02 int, c03 int, c04 int, c05 int,
    c06 int, c07 int, c08 int, c09 int, c10 int,
    c11 int, c12 int, c13 int, c14 int, c15 int,
    c16 int, c17 int, c18 int, c19 int, c20 int
);

INSERT INTO target_wide
SELECT
    g,
    g*1, g*2, g*3, g*4, g*5,
    g*6, g*7, g*8, g*9, g*10,
    g*11, g*12, g*13, g*14, g*15,
    g*16, g*17, g*18, g*19, g*20
FROM generate_series(1, 100) g;

INSERT INTO source_wide
SELECT
    g,
    g*10, g*20, g*30, g*40, g*50,
    g*60, g*70, g*80, g*90, g*100,
    g*110, g*120, g*130, g*140, g*150,
    g*160, g*170, g*180, g*190, g*200
FROM generate_series(51, 150) g;

-- Test 0002-A: MERGE with UPDATE action referencing all columns
-- Exercises preptlist.c:180 (action targetList Var dedup)
MERGE INTO target_wide t
USING source_wide s ON t.id = s.id
WHEN MATCHED THEN
    UPDATE SET
        c01 = s.c01, c02 = s.c02, c03 = s.c03, c04 = s.c04, c05 = s.c05,
        c06 = s.c06, c07 = s.c07, c08 = s.c08, c09 = s.c09, c10 = s.c10,
        c11 = s.c11, c12 = s.c12, c13 = s.c13, c14 = s.c14, c15 = s.c15,
        c16 = s.c16, c17 = s.c17, c18 = s.c18, c19 = s.c19, c20 = s.c20
WHEN NOT MATCHED THEN
    INSERT (id, c01,c02,c03,c04,c05,c06,c07,c08,c09,c10,
               c11,c12,c13,c14,c15,c16,c17,c18,c19,c20)
    VALUES (s.id, s.c01,s.c02,s.c03,s.c04,s.c05,
                  s.c06,s.c07,s.c08,s.c09,s.c10,
                  s.c11,s.c12,s.c13,s.c14,s.c15,
                  s.c16,s.c17,s.c18,s.c19,s.c20);

-- Verify: matched rows (51..100) updated to source values; new rows (101..150) inserted
DO $$
DECLARE
    matched_ok  bool;
    inserted_ok bool;
BEGIN
    SELECT bool_and(c01 = id * 10) INTO matched_ok
    FROM target_wide WHERE id BETWEEN 51 AND 100;

    SELECT bool_and(c01 = id * 10) INTO inserted_ok
    FROM target_wide WHERE id BETWEEN 101 AND 150;

    IF NOT matched_ok THEN
        RAISE EXCEPTION '0002-A: MERGE UPDATE produced wrong column values';
    END IF;
    IF NOT inserted_ok THEN
        RAISE EXCEPTION '0002-A: MERGE INSERT produced wrong column values';
    END IF;
END;
$$;
\echo '0002-A PASS: MERGE with wide table — correct values after MERGE'

-- Test 0002-B: MERGE with join condition Var dedup
-- Exercises preptlist.c:206 (mergeJoinCondition Var dedup)
CREATE TABLE join_source (
    id      int,
    extra1  int,
    extra2  int,
    extra3  int
);
INSERT INTO join_source VALUES (1,10,20,30),(2,11,21,31),(999,99,99,99);

CREATE TABLE join_target (
    id int PRIMARY KEY,
    val int
);
INSERT INTO join_target VALUES (1,0),(2,0);

MERGE INTO join_target t
USING join_source s ON t.id = s.id AND s.extra1 > 5 AND s.extra2 > 10
WHEN MATCHED THEN
    UPDATE SET val = s.extra3;

DO $$
BEGIN
    IF (SELECT val FROM join_target WHERE id=1) <> 30 THEN
        RAISE EXCEPTION '0002-B: MERGE join condition Var not resolved correctly';
    END IF;
    IF (SELECT val FROM join_target WHERE id=2) <> 31 THEN
        RAISE EXCEPTION '0002-B: MERGE join condition Var not resolved correctly for id=2';
    END IF;
END;
$$;
\echo '0002-B PASS: MERGE with join condition Vars — correct update'

-- Test 0002-C: UPDATE...RETURNING with many columns
-- Exercises preptlist.c:316 (RETURNING Var dedup)
CREATE TABLE ret_tgt (id int PRIMARY KEY, a int, b int, c int, d int, e int);
CREATE TABLE ret_src (id int PRIMARY KEY, a int, b int, c int, d int, e int);

INSERT INTO ret_tgt VALUES (1,1,1,1,1,1),(2,2,2,2,2,2),(3,3,3,3,3,3);
INSERT INTO ret_src VALUES (1,10,20,30,40,50),(2,11,21,31,41,51);

UPDATE ret_tgt t SET a=s.a, b=s.b, c=s.c, d=s.d, e=s.e
FROM ret_src s
WHERE t.id = s.id
RETURNING t.id, t.a, t.b, t.c, t.d, t.e, s.a AS src_a;

DO $$
DECLARE rows int;
BEGIN
    SELECT count(*) INTO rows FROM (
        UPDATE cwe407_test.ret_tgt t
        SET a=s.a, b=s.b, c=s.c, d=s.d, e=s.e
        FROM cwe407_test.ret_src s
        WHERE t.id = s.id
        RETURNING t.id, t.a, s.a AS src_a
    ) sub WHERE sub.a = sub.src_a;

    IF rows <> 2 THEN
        RAISE EXCEPTION '0002-C: RETURNING produced unexpected row count: %', rows;
    END IF;
END;
$$;
\echo '0002-C PASS: UPDATE...RETURNING with multi-table Vars — correct output'

-- ═══════════════════════════════════════════════════════════════════════════
-- 0003 — equivclass.c: EC Var membership check
--
-- Tests that query plans using equivalence classes for ORDER BY / sort
-- produce correct results.  Many equivalence-class members stress the
-- list_member loop at equivclass.c:1041.
-- ═══════════════════════════════════════════════════════════════════════════

CREATE TABLE ec_t1 (a int, b int, c int);
CREATE TABLE ec_t2 (a int, b int, d int);
CREATE TABLE ec_t3 (a int, b int, e int);

INSERT INTO ec_t1 SELECT g, g*2, g*3 FROM generate_series(1,50) g;
INSERT INTO ec_t2 SELECT g, g*2, g*4 FROM generate_series(1,50) g;
INSERT INTO ec_t3 SELECT g, g*2, g*5 FROM generate_series(1,50) g;

CREATE INDEX ON ec_t1(a,b);
CREATE INDEX ON ec_t2(a,b);
CREATE INDEX ON ec_t3(a,b);

-- Test 0003-A: Three-way join with EC sort — exercises find_em_expr_for_rel
-- The join creates equivalence classes {t1.a, t2.a, t3.a} and {t1.b, t2.b, t3.b}.
-- The ORDER BY on t1.a + c forces EC member lookup to check Var availability.
DO $$
DECLARE
    cnt     int;
    prev_ab int := 0;
    ok      bool := true;
BEGIN
    FOR cnt IN
        SELECT t1.a + t1.b
        FROM ec_t1 t1
        JOIN ec_t2 t2 ON t1.a = t2.a AND t1.b = t2.b
        JOIN ec_t3 t3 ON t1.a = t3.a AND t1.b = t3.b
        WHERE t1.c > 0
        ORDER BY t1.a, t1.b
    LOOP
        IF cnt < prev_ab THEN
            ok := false;
            EXIT;
        END IF;
        prev_ab := cnt;
    END LOOP;

    IF NOT ok THEN
        RAISE EXCEPTION '0003-A: EC member sort produced out-of-order results';
    END IF;
END;
$$;
\echo '0003-A PASS: Three-way EC join with ORDER BY — correct sorted results'

-- Test 0003-B: EC with expressions — forces non-Var EC member path
DO $$
DECLARE cnt int;
BEGIN
    SELECT count(*) INTO cnt
    FROM (
        SELECT t1.a, t1.b, t1.a + t1.b AS sum_ab
        FROM ec_t1 t1
        JOIN ec_t2 t2 ON t1.a = t2.a
        WHERE t1.a + t1.b > 10
        ORDER BY t1.a + t1.b
    ) sub
    WHERE sum_ab <= 10;

    IF cnt > 0 THEN
        RAISE EXCEPTION '0003-B: EC expression filter produced rows outside range';
    END IF;
END;
$$;
\echo '0003-B PASS: EC with expression members — correct filter results'

-- ═══════════════════════════════════════════════════════════════════════════
-- 0004 — analyzejoins.c: join-elimination reltarget merge
--
-- Tests that self-join elimination produces correct query results.
-- remove_useless_joins() calls remove_self_join_rel() when a join can be
-- eliminated; the patched code merges reltarget->exprs.
-- ═══════════════════════════════════════════════════════════════════════════

CREATE TABLE je_t (
    id   int PRIMARY KEY,
    k    int NOT NULL,
    v    int,
    w    int,
    x    int
);
CREATE UNIQUE INDEX ON je_t(k);

INSERT INTO je_t SELECT g, g, g*2, g*3, g*4 FROM generate_series(1,100) g;

-- Test 0004-A: Self-join on unique key — should be eliminated
-- After elimination, toRemove's exprs must be correctly merged into toKeep.
DO $$
DECLARE
    direct_cnt   int;
    selfjoin_cnt int;
BEGIN
    SELECT count(*) INTO direct_cnt FROM je_t WHERE k > 50;

    SELECT count(*) INTO selfjoin_cnt
    FROM je_t t1
    JOIN je_t t2 ON t1.k = t2.k   -- eliminatable: unique key join
    WHERE t1.k > 50;

    IF direct_cnt <> selfjoin_cnt THEN
        RAISE EXCEPTION '0004-A: self-join elimination changed row count: direct=% selfjoin=%',
            direct_cnt, selfjoin_cnt;
    END IF;
END;
$$;
\echo '0004-A PASS: Self-join elimination — row count correct'

-- Test 0004-B: Self-join with projection of columns from both sides
-- After elimination, columns from the eliminated side must still be available.
DO $$
DECLARE
    mismatched int;
BEGIN
    SELECT count(*) INTO mismatched
    FROM (
        SELECT t1.v AS v1, t2.w AS w2, t1.x AS x1, t2.v AS v2
        FROM je_t t1
        JOIN je_t t2 ON t1.k = t2.k
        WHERE t1.id BETWEEN 1 AND 20
    ) sub
    WHERE v1 <> v2 OR x1 <> id * 4;

    IF mismatched > 0 THEN
        RAISE EXCEPTION '0004-B: self-join elimination produced wrong column values: % rows mismatched',
            mismatched;
    END IF;
END;
$$;
\echo '0004-B PASS: Self-join column projection — correct values after elimination'

-- Test 0004-C: Self-join with WHERE clause referencing both sides
DO $$
DECLARE cnt int;
BEGIN
    SELECT count(*) INTO cnt
    FROM je_t t1
    JOIN je_t t2 ON t1.k = t2.k
    WHERE t1.v > 100 AND t2.w > 150
    ORDER BY t1.id;

    DECLARE direct_cnt int;
    SELECT count(*) INTO direct_cnt
    FROM je_t
    WHERE v > 100 AND w > 150;

    IF cnt <> direct_cnt THEN
        RAISE EXCEPTION '0004-C: self-join WHERE filter mismatch: eliminated=% direct=%',
            cnt, direct_cnt;
    END IF;
END;
$$;
\echo '0004-C PASS: Self-join with dual-side WHERE — correct filter after elimination'

-- ── Teardown ─────────────────────────────────────────────────────────────────

ROLLBACK;

\echo ''
\echo 'All postgresql-0002/0003/0004 functional tests PASSED.'
