\set ECHO none
\pset format unaligned
SET search_path TO provsql_test, provsql;

-- Inversion-free UCQ(OBDD) detector.
--
-- The detector is a sibling pass to the read-once rewriter: it recognises the
-- inversion-free, tuple-independent self-join class (the consistent-unification
-- self-joins the read-once path bails on), builds the SafeCert order recipe,
-- emits a diagnostic NOTICE, and attaches a transparent certificate plus
-- per-input order markers to the lineage (read back at probability evaluation;
-- the lineage itself is left intact).
--
-- The lineage must be built by the real planner (real tables + the SQL query),
-- never a hand-factored circuit: a hand-factored circuit is low-treewidth and
-- would silently skip the hard case.
--
-- Detection fires while the CREATE TEMP TABLE AS planner-rewrites its SELECT;
-- the temp table's provsql column carries non-deterministic input-token UUIDs,
-- so we never display it -- we drop provenance and assert the row count instead
-- (cardinality is preserved because the lineage is left intact).

SET provsql.verbose_level = 5;     -- acceptance NOTICE (>=1) + rejection reasons (>=5)
SET provsql.boolean_provenance = on;

-- TID base relations.  Column c2 is the "second attribute": it plays y in the
-- S(x,y),A(x,y) atoms and z in the S(x,z),B(x,z) atoms.
CREATE TABLE ifr_s(x int, c2 int);
INSERT INTO ifr_s VALUES (1,10),(1,20),(2,10);
SELECT add_provenance('ifr_s');
CREATE TABLE ifr_a(x int, c2 int);
INSERT INTO ifr_a VALUES (1,10),(2,99);
SELECT add_provenance('ifr_a');
CREATE TABLE ifr_b(x int, c2 int);
INSERT INTO ifr_b VALUES (1,20),(2,10);
SELECT add_provenance('ifr_b');

DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifr_s;
  PERFORM set_prob(provsql, 0.5) FROM ifr_a;
  PERFORM set_prob(provsql, 0.5) FROM ifr_b;
END $$;

-- (1) Witness: S(x,y),A(x,y),S(x,z),B(x,z) -- self-join on S through root x.
--     Expect a NOTICE that the certificate was attached (planner side).  The
--     certificate then round-trips to the evaluator: probability_evaluate reads
--     it back from the annotated root (a second NOTICE) and -- because the
--     annotation is transparent -- still returns the correct probability
--     (one derivation of four independent p=0.5 inputs => 0.5^4 = 0.0625).
--     Cardinality is preserved (only x=1 qualifies -> 1 row).
-- Capture the per-row provenance token (the annotated root) into an explicit
-- column p, then drop provenance tracking so the probability read below is not
-- itself rewritten (no non-deterministic provsql column on its output).
CREATE TEMP TABLE ifr_witness AS
  SELECT s1.x AS x, provenance() AS p
    FROM ifr_s s1, ifr_a a, ifr_s s2, ifr_b b
   WHERE s1.x = a.x AND s1.c2 = a.c2     -- S(x,y), A(x,y)
     AND s1.x = s2.x                     -- both S occurrences share the root x
     AND s2.x = b.x AND s2.c2 = b.c2;    -- S(x,z), B(x,z)
SELECT remove_provenance('ifr_witness');
SELECT round(probability_evaluate(p)::numeric, 6) AS witness_prob
  FROM ifr_witness ORDER BY 1;
SELECT count(*) AS witness_rows FROM ifr_witness;

-- (2) Rejection -- symmetric closure R(x,y),R(y,x): the root class sits at two
--     different column positions across the two R occurrences.  Expect NO
--     certificate; a rejection NOTICE at verbose>=5.  Lineage intact (3 rows).
CREATE TABLE ifr_r(a int, b int);
INSERT INTO ifr_r VALUES (1,2),(2,1),(3,3);
SELECT add_provenance('ifr_r');
DO $$ BEGIN PERFORM set_prob(provsql, 0.5) FROM ifr_r; END $$;

CREATE TEMP TABLE ifr_sym AS
  SELECT r1.a AS a
    FROM ifr_r r1, ifr_r r2
   WHERE r1.a = r2.b AND r1.b = r2.a;
SELECT remove_provenance('ifr_sym');
SELECT count(*) AS sym_rows FROM ifr_sym;

-- (3) Rejection -- path R(x,y),R(y,z): the shared class y is at column 2 in one
--     occurrence and column 1 in the other (positional inconsistency).
CREATE TEMP TABLE ifr_path AS
  SELECT r1.a AS a
    FROM ifr_r r1, ifr_r r2
   WHERE r1.b = r2.a;
SELECT remove_provenance('ifr_path');
SELECT count(*) AS path_rows FROM ifr_path;

-- (4) Live path correctness on a NON-read-once witness.  Richer data gives the
--     root x=1 several (y,z) derivations; GROUP BY x OR-aggregates them into one
--     provenance token, the inversion-free block
--       (S(1,10)A(1,10) v S(1,20)A(1,20)) ^ (S(1,20)B(1,20) v S(1,30)B(1,30))
--     which shares S(1,20), so it is NOT read-once: independentEvaluation
--     rejects and the default chain takes the inversion-free structured-d-DNNF
--     rung (markers attached on the certified path, read back at eval).  Its
--     value must equal exact possible_worlds.  Distinct per-tuple probabilities
--     make the agreement a real check (hand value 0.2203).
CREATE TABLE ifr_s2(x int, c2 int);
INSERT INTO ifr_s2 VALUES (1,10),(1,20),(1,30);
SELECT add_provenance('ifr_s2');
CREATE TABLE ifr_a2(x int, c2 int);
INSERT INTO ifr_a2 VALUES (1,10),(1,20);
SELECT add_provenance('ifr_a2');
CREATE TABLE ifr_b2(x int, c2 int);
INSERT INTO ifr_b2 VALUES (1,20),(1,30);
SELECT add_provenance('ifr_b2');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifr_s2;
  PERFORM set_prob(provsql, 0.4) FROM ifr_a2 WHERE c2 = 10;
  PERFORM set_prob(provsql, 0.6) FROM ifr_a2 WHERE c2 = 20;
  PERFORM set_prob(provsql, 0.3) FROM ifr_b2 WHERE c2 = 20;
  PERFORM set_prob(provsql, 0.7) FROM ifr_b2 WHERE c2 = 30;
END $$;

CREATE TEMP TABLE ifr_block AS
  SELECT s1.x AS x, provenance() AS p
    FROM ifr_s2 s1, ifr_a2 a, ifr_s2 s2, ifr_b2 b
   WHERE s1.x = a.x AND s1.c2 = a.c2
     AND s1.x = s2.x
     AND s2.x = b.x AND s2.c2 = b.c2
   GROUP BY s1.x;
SELECT remove_provenance('ifr_block');
SELECT count(*) AS block_rows FROM ifr_block;
-- default chain (inversion-free rung) vs explicit method vs exact oracle: equal.
SELECT round(probability_evaluate(p)::numeric, 8)                   AS block_default,
       round(probability_evaluate(p, 'inversion-free')::numeric, 8) AS block_if,
       round(probability_evaluate(p, 'possible-worlds')::numeric, 8) AS block_pw
  FROM ifr_block;

-- (5) Self-join-free hierarchical query q(x) :- A(x), B(x).  With no self-join
--     the inversion-free class coincides with the read-once class, but the
--     analysis is not gated on self-joins: it still certifies the query and
--     attaches per-input order markers.  With boolean_provenance OFF (so the
--     read-once rewriter does not pre-empt it) the explicit 'inversion-free'
--     method compiles the structured d-DNNF over those markers; its value
--     matches the default chain (here independentEvaluation already suffices)
--     and exact possible worlds (0.5*0.4 = 0.2 at each x).
SET provsql.boolean_provenance = off;
CREATE TABLE ifr_sjf_a(x int);
INSERT INTO ifr_sjf_a VALUES (1),(2),(3);
SELECT add_provenance('ifr_sjf_a');
CREATE TABLE ifr_sjf_b(x int);
INSERT INTO ifr_sjf_b VALUES (1),(2),(3);
SELECT add_provenance('ifr_sjf_b');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifr_sjf_a;
  PERFORM set_prob(provsql, 0.4) FROM ifr_sjf_b;
END $$;
CREATE TEMP TABLE ifr_sjf AS
  SELECT a.x AS x, provenance() AS p
    FROM ifr_sjf_a a, ifr_sjf_b b
   WHERE a.x = b.x
   GROUP BY a.x;
SELECT remove_provenance('ifr_sjf');
SELECT count(*) AS sjf_rows FROM ifr_sjf;
SELECT x,
       round(probability_evaluate(p)::numeric, 6)                    AS sjf_default,
       round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS sjf_if,
       round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS sjf_pw
  FROM ifr_sjf ORDER BY x;

-- (6) Decline / robustness cases: the analysis must keep clear of atoms it does
--     not model, and the evaluator must treat a malformed certificate as inert.
--     In each the lineage still evaluates correctly through the normal chain.

-- (6a) Aggregating view: a GROUP BY / aggregate inside the view turns its
--      provenance into a semiring sum over groups, outside the monotone-DNF
--      model.  The flattener refuses it (hasAggs / groupClause), so the detector
--      still sees an RTE_SUBQUERY and declines; the lineage evaluates correctly
--      through the normal chain.  (Plain SPJ views ARE flattened and certify --
--      see section 9.)
CREATE TABLE ifr_va(x int);
INSERT INTO ifr_va VALUES (1),(2);
SELECT add_provenance('ifr_va');
CREATE TABLE ifr_vb(x int);
INSERT INTO ifr_vb VALUES (1),(2);
SELECT add_provenance('ifr_vb');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifr_va;
  PERFORM set_prob(provsql, 0.4) FROM ifr_vb;
END $$;
CREATE VIEW ifr_vbv AS SELECT x, count(*) AS c FROM ifr_vb GROUP BY x;
CREATE TEMP TABLE ifr_vt AS
  SELECT a.x AS x, provenance() AS p
    FROM ifr_va a, ifr_vbv b WHERE a.x = b.x GROUP BY a.x;
SELECT remove_provenance('ifr_vt');
SELECT x, (get_gate_type(p) <> 'annotation') AS declined,
       round(probability_evaluate(p)::numeric, 6) AS prob
  FROM ifr_vt ORDER BY x;

-- (6b) BID atom: a repair_key relation contributes gate_mulinput leaves.  The
--      detector declines (no certificate), so the default chain evaluates the
--      block soundly (x=1: disjoint 0.3+0.4 = 0.7; x=2: 0.5) and the explicit
--      'inversion-free' method declines rather than mis-evaluating it.
CREATE TABLE ifr_bid(x int, v int, conf float);
INSERT INTO ifr_bid VALUES (1,100,0.3),(1,200,0.4),(2,300,0.5);
SELECT add_provenance('ifr_bid');
SELECT remove_provenance('ifr_bid');
SELECT repair_key('ifr_bid', 'x');
DO $$ BEGIN PERFORM set_prob(provsql, conf) FROM ifr_bid; END $$;
CREATE TEMP TABLE ifr_bt AS SELECT x, provenance() AS p FROM ifr_bid GROUP BY x;
SELECT remove_provenance('ifr_bt');
SELECT x, get_gate_type(p) AS root_type,
       round(probability_evaluate(p)::numeric, 6)                    AS prob_default,
       round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS prob_pw
  FROM ifr_bt ORDER BY x;
DO $$
DECLARE tok uuid; declined boolean := false;
BEGIN
  SELECT p INTO tok FROM ifr_bt WHERE x = 1;
  BEGIN PERFORM probability_evaluate(tok, 'inversion-free');
  EXCEPTION WHEN OTHERS THEN
    declined := (SQLERRM ~ 'requires an inversion-free certificate');
  END;
  IF NOT declined THEN
    RAISE EXCEPTION 'explicit inversion-free should have declined on a BID block';
  END IF;
END $$;
SELECT 'BID: explicit inversion-free declines; default sound' AS bid_check;

-- (6c) Malformed certificate: a stray 'C'-prefixed annotation that is not a
--      valid SafeCert recipe must be read as inert (transparent passthrough),
--      so evaluation falls back to the wrapped circuit and stays correct.  Build
--      the token with the analysis off so the only certificate present is the
--      garbage one we attach by hand.
SET provsql.inversion_free = off;
CREATE TABLE ifr_ma(x int);
INSERT INTO ifr_ma VALUES (1);
SELECT add_provenance('ifr_ma');
CREATE TABLE ifr_mb(x int);
INSERT INTO ifr_mb VALUES (1);
SELECT add_provenance('ifr_mb');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifr_ma;
  PERFORM set_prob(provsql, 0.4) FROM ifr_mb;
END $$;
CREATE TEMP TABLE ifr_mt AS
  SELECT a.x AS x, provenance() AS p
    FROM ifr_ma a, ifr_mb b WHERE a.x = b.x GROUP BY a.x;
SELECT remove_provenance('ifr_mt');
SET provsql.inversion_free = on;
SELECT x, round(probability_evaluate(p)::numeric, 6) AS prob_plain,
          round(probability_evaluate(
                  annotate(p, 'Cgarbage-not-a-recipe'))::numeric, 6) AS prob_badcert
  FROM ifr_mt ORDER BY x;

-- (7) Non-integer key columns.  The per-input order markers carry the root and
--     secondary class values as length-prefixed value text, so any scalar type
--     whose output function is injective works.  These shapes are structurally
--     identical to the integer-keyed ones above; only the key column types
--     change.  The certified path must fire (acceptance NOTICE) and the
--     structured-d-DNNF value must equal exact possible_worlds -- which proves
--     the text codec round-tripped the keys (a mis-parse would mis-group and
--     change the probability).

-- (7a) text root AND text secondary, on the self-join block of case (4).  One
--      root value contains a space, exercising the byte-length prefix that keeps
--      arbitrary text unambiguous.  Probabilities mirror case (4) (0.5 for each
--      S; A: 0.4/0.6; B: 0.3/0.7), so the block value is the same 0.2203.
SET provsql.boolean_provenance = on;
CREATE TABLE ifr_ts(x text, c2 text);
INSERT INTO ifr_ts VALUES ('grp one','a'),('grp one','b'),('grp one','c');
SELECT add_provenance('ifr_ts');
CREATE TABLE ifr_ta(x text, c2 text);
INSERT INTO ifr_ta VALUES ('grp one','a'),('grp one','b');
SELECT add_provenance('ifr_ta');
CREATE TABLE ifr_tb(x text, c2 text);
INSERT INTO ifr_tb VALUES ('grp one','b'),('grp one','c');
SELECT add_provenance('ifr_tb');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifr_ts;
  PERFORM set_prob(provsql, 0.4) FROM ifr_ta WHERE c2 = 'a';
  PERFORM set_prob(provsql, 0.6) FROM ifr_ta WHERE c2 = 'b';
  PERFORM set_prob(provsql, 0.3) FROM ifr_tb WHERE c2 = 'b';
  PERFORM set_prob(provsql, 0.7) FROM ifr_tb WHERE c2 = 'c';
END $$;
CREATE TEMP TABLE ifr_tblock AS
  SELECT s1.x AS x, provenance() AS p
    FROM ifr_ts s1, ifr_ta a, ifr_ts s2, ifr_tb b
   WHERE s1.x = a.x AND s1.c2 = a.c2
     AND s1.x = s2.x
     AND s2.x = b.x AND s2.c2 = b.c2
   GROUP BY s1.x;
SELECT remove_provenance('ifr_tblock');
SELECT count(*) AS tblock_rows FROM ifr_tblock;
SELECT round(probability_evaluate(p)::numeric, 8)                   AS tblock_default,
       round(probability_evaluate(p, 'inversion-free')::numeric, 8) AS tblock_if,
       round(probability_evaluate(p, 'possible-worlds')::numeric, 8) AS tblock_pw
  FROM ifr_tblock;

-- (7b) uuid root, self-join-free hierarchical q(x) :- A(x), B(x).  The join /
--      group key is a uuid; its canonical text rendering is the order key.
SET provsql.boolean_provenance = off;
CREATE TABLE ifr_ua(x uuid);
INSERT INTO ifr_ua VALUES
  ('11111111-1111-1111-1111-111111111111'),
  ('22222222-2222-2222-2222-222222222222');
SELECT add_provenance('ifr_ua');
CREATE TABLE ifr_ub(x uuid);
INSERT INTO ifr_ub VALUES
  ('11111111-1111-1111-1111-111111111111'),
  ('22222222-2222-2222-2222-222222222222');
SELECT add_provenance('ifr_ub');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifr_ua;
  PERFORM set_prob(provsql, 0.4) FROM ifr_ub;
END $$;
CREATE TEMP TABLE ifr_ut AS
  SELECT a.x AS x, provenance() AS p
    FROM ifr_ua a, ifr_ub b WHERE a.x = b.x GROUP BY a.x;
SELECT remove_provenance('ifr_ut');
SELECT count(*) AS ut_rows FROM ifr_ut;
SELECT round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS ut_if,
       round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS ut_pw
  FROM ifr_ut;

-- (7c) date root, same self-join-free shape; key text is 'YYYY-MM-DD'.
CREATE TABLE ifr_da(x date);
INSERT INTO ifr_da VALUES ('2026-01-01'),('2026-02-02');
SELECT add_provenance('ifr_da');
CREATE TABLE ifr_db(x date);
INSERT INTO ifr_db VALUES ('2026-01-01'),('2026-02-02');
SELECT add_provenance('ifr_db');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifr_da;
  PERFORM set_prob(provsql, 0.4) FROM ifr_db;
END $$;
CREATE TEMP TABLE ifr_dt AS
  SELECT a.x AS x, provenance() AS p
    FROM ifr_da a, ifr_db b WHERE a.x = b.x GROUP BY a.x;
SELECT remove_provenance('ifr_dt');
SELECT count(*) AS dt_rows FROM ifr_dt;
SELECT round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS dt_if,
       round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS dt_pw
  FROM ifr_dt;

-- (8) Deterministic (non-tracked) relations.  A relation with no provsql column
--     and no metadata contributes only probability-1 tuples and anchors no
--     provenance variable: the detector *erases* it before the root /
--     positional / precedence / marker passes (it still filters the cross
--     product through its join equalities).  Erasing only removes precedence
--     edges, so it enlarges the certified class soundly.  boolean_provenance is
--     off here (from 7b), so the explicit 'inversion-free' method compiles the
--     structured d-DNNF and the read-once rewriter does not pre-empt the NOTICE.

-- (8a) self-join-free q(x) :- A(x), B(x) with a deterministic filter D(x).  D
--      excludes x=3 but adds no variable; the query still certifies (root x
--      touches both *tracked* atoms) and matches possible worlds (0.5*0.4).
CREATE TABLE ifd_a(x int); INSERT INTO ifd_a VALUES (1),(2),(3);
SELECT add_provenance('ifd_a');
CREATE TABLE ifd_b(x int); INSERT INTO ifd_b VALUES (1),(2),(3);
SELECT add_provenance('ifd_b');
CREATE TABLE ifd_d(x int); INSERT INTO ifd_d VALUES (1),(2);   -- deterministic
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifd_a;
  PERFORM set_prob(provsql, 0.4) FROM ifd_b;
END $$;
CREATE TEMP TABLE ifd_t AS
  SELECT a.x AS x, provenance() AS p
    FROM ifd_a a, ifd_b b, ifd_d d
   WHERE a.x = b.x AND a.x = d.x GROUP BY a.x;
SELECT remove_provenance('ifd_t');
SELECT count(*) AS ifd_rows FROM ifd_t;
SELECT x, round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS ifd_if,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS ifd_pw
  FROM ifd_t ORDER BY x;

-- (8b) the deterministic atom anchors the *secondary* class: q(x) :- A(x),
--      B(x,y), D(y).  Were D tracked, neither class {x} (in A,B) nor {y} (in
--      B,D) would touch all three atoms -> no root -> reject (this is h0, the
--      canonical non-hierarchical query).  Erasing deterministic D leaves {x}
--      touching both tracked atoms, so it certifies; value matches possible
--      worlds (x=1 has two y-derivations sharing nothing: 0.5*(1-0.6^2)=0.32).
CREATE TABLE ifd2_a(x int);        INSERT INTO ifd2_a VALUES (1),(2);
SELECT add_provenance('ifd2_a');
CREATE TABLE ifd2_b(x int, y int); INSERT INTO ifd2_b VALUES (1,10),(1,20),(2,30);
SELECT add_provenance('ifd2_b');
CREATE TABLE ifd2_d(y int);        INSERT INTO ifd2_d VALUES (10),(20),(30); -- deterministic
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifd2_a;
  PERFORM set_prob(provsql, 0.4) FROM ifd2_b;
END $$;
CREATE TEMP TABLE ifd2_t AS
  SELECT a.x AS x, provenance() AS p
    FROM ifd2_a a, ifd2_b b, ifd2_d d
   WHERE a.x = b.x AND b.y = d.y GROUP BY a.x;
SELECT remove_provenance('ifd2_t');
SELECT x, round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS ifd2_if,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS ifd2_pw
  FROM ifd2_t ORDER BY x;

-- (8c) near-miss: the same shape but the third relation is provenance-*tracked*.
--      Erasure must not over-fire -- a tracked atom is never erased, so the
--      genuine non-hierarchy (h0) is still caught: the detector rejects and the
--      explicit 'inversion-free' method declines, while the default chain
--      evaluates the block soundly through the fallback (matches possible
--      worlds: x=1: 0.5*(1-(1-0.4*0.3)^2)=0.5*0.2256=0.1128; x=2: 0.5*0.4*0.3).
CREATE TABLE ifd3_a(x int);        INSERT INTO ifd3_a VALUES (1),(2);
SELECT add_provenance('ifd3_a');
CREATE TABLE ifd3_b(x int, y int); INSERT INTO ifd3_b VALUES (1,10),(1,20),(2,30);
SELECT add_provenance('ifd3_b');
CREATE TABLE ifd3_d(y int);        INSERT INTO ifd3_d VALUES (10),(20),(30);
SELECT add_provenance('ifd3_d');   -- tracked: creates the inversion
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifd3_a;
  PERFORM set_prob(provsql, 0.4) FROM ifd3_b;
  PERFORM set_prob(provsql, 0.3) FROM ifd3_d;
END $$;
CREATE TEMP TABLE ifd3_t AS
  SELECT a.x AS x, provenance() AS p
    FROM ifd3_a a, ifd3_b b, ifd3_d d
   WHERE a.x = b.x AND b.y = d.y GROUP BY a.x;
SELECT remove_provenance('ifd3_t');
DO $$
DECLARE tok uuid; declined boolean := false;
BEGIN
  SELECT p INTO tok FROM ifd3_t WHERE x = 1;
  BEGIN PERFORM probability_evaluate(tok, 'inversion-free');
  EXCEPTION WHEN OTHERS THEN
    declined := (SQLERRM ~ 'requires an inversion-free certificate');
  END;
  IF NOT declined THEN
    RAISE EXCEPTION 'tracked third relation (h0) should have declined inversion-free';
  END IF;
END $$;
SELECT x, round(probability_evaluate(p)::numeric, 6)                    AS ifd3_default,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS ifd3_pw
  FROM ifd3_t ORDER BY x;

-- (9) Single-base SPJ views / subqueries.  Inversion-freeness is a property of
--     the *flattened* query: the detector runs on a copy where each single-base
--     SPJ subquery/view is inlined to its base relation in place, so the markers
--     map straight back and are threaded into each subquery's recursive rewrite
--     (transparent annotations on the base inputs, so query results and the
--     circuit are unchanged).  boolean_provenance is off here.

-- (9a) view + base, hierarchical self-join-free: q(x) :- Av(x), B(x) where Av is
--      the projection view SELECT x FROM A.  Certifies and matches pw (0.5*0.4).
CREATE TABLE ifv_a(x int); INSERT INTO ifv_a VALUES (1),(2),(3);
SELECT add_provenance('ifv_a');
CREATE TABLE ifv_b(x int); INSERT INTO ifv_b VALUES (1),(2),(3);
SELECT add_provenance('ifv_b');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifv_a;
  PERFORM set_prob(provsql, 0.4) FROM ifv_b;
END $$;
CREATE VIEW ifv_av AS SELECT x FROM ifv_a;
CREATE TEMP TABLE ifv_t1 AS
  SELECT a.x AS x, provenance() AS p
    FROM ifv_av a, ifv_b b WHERE a.x = b.x GROUP BY a.x;
SELECT remove_provenance('ifv_t1');
SELECT x, round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS t1_if,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS t1_pw
  FROM ifv_t1 ORDER BY x;

-- (9b) the view referenced *twice* -> a structured self-join through the view,
--      which inlines to a self-join on the base A.  The read-once path rejects
--      self-joins; inversion-free certifies it.  A(x) AND A(x) collapses
--      idempotently, so the probability is P(A(x)) = 0.5.
CREATE TEMP TABLE ifv_t2 AS
  SELECT v1.x AS x, provenance() AS p
    FROM ifv_av v1, ifv_av v2 WHERE v1.x = v2.x GROUP BY v1.x;
SELECT remove_provenance('ifv_t2');
SELECT x, round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS t2_if,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS t2_pw
  FROM ifv_t2 ORDER BY x;

-- (9c) selection view (a WHERE inside the view): SELECT x FROM B WHERE x <= 2.
--      The view's selection is pulled up into the flattened conjunction, so x=3
--      is filtered; the certified shape matches pw on the surviving groups.
CREATE VIEW ifv_bv AS SELECT x FROM ifv_b WHERE x <= 2;
CREATE TEMP TABLE ifv_t3 AS
  SELECT a.x AS x, provenance() AS p
    FROM ifv_a a, ifv_bv b WHERE a.x = b.x GROUP BY a.x;
SELECT remove_provenance('ifv_t3');
SELECT x, round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS t3_if,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS t3_pw
  FROM ifv_t3 ORDER BY x;

-- (9d) same structured self-join through the view, but with boolean_provenance
--      ON: the parent is not read-once (self-join), so it still reaches the
--      inversion-free analysis; each inlined subquery is rewritten with the
--      parent-supplied markers (the read-once rewrite is skipped for them so the
--      transparent markers are not bypassed).  Default chain matches pw.
SET provsql.boolean_provenance = on;
CREATE TEMP TABLE ifv_t4 AS
  SELECT v1.x AS x, provenance() AS p
    FROM ifv_av v1, ifv_av v2 WHERE v1.x = v2.x GROUP BY v1.x;
SELECT remove_provenance('ifv_t4');
SELECT x, round(probability_evaluate(p)::numeric, 6)                    AS t4_default,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS t4_pw
  FROM ifv_t4 ORDER BY x;
SET provsql.boolean_provenance = off;

-- (10) Multi-relation SPJ views (a join *inside* the view).  The flattener
--      expands one subquery slot into all its base atoms, pulling the view's
--      join condition up into the flat conjunction; markers are threaded to each
--      base input.  boolean_provenance is off.

-- (10a) view joins A(x,k), B(k,z) on the internal key k and projects x: the
--       flattened conjunction A(x,k), B(k,z) has root k (touches both atoms).
--       Certifies and matches pw (0.5 * 0.4).
CREATE TABLE ifm_a(x int, k int); INSERT INTO ifm_a VALUES (1,10),(2,20);
SELECT add_provenance('ifm_a');
CREATE TABLE ifm_b(k int, z int); INSERT INTO ifm_b VALUES (10,100),(20,200);
SELECT add_provenance('ifm_b');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifm_a;
  PERFORM set_prob(provsql, 0.4) FROM ifm_b;
END $$;
CREATE VIEW ifm_v AS SELECT a.x AS x, b.z AS z FROM ifm_a a, ifm_b b WHERE a.k = b.k;
CREATE TEMP TABLE ifm_t1 AS
  SELECT v.x AS x, provenance() AS p FROM ifm_v v GROUP BY v.x;
SELECT remove_provenance('ifm_t1');
SELECT x, round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS t1_if,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS t1_pw
  FROM ifm_t1 ORDER BY x;

-- (10b) a join-on-x view V(x) := A(x), B(x) referenced *twice* -> a structured
--       self-join through a multi-relation view, flattening to A,B,A,B all
--       sharing the root x.  Certifies; A(x)&B(x) repeated collapses
--       idempotently, so the value is P(A(x) & B(x)) = 0.5*0.4 = 0.2.
CREATE TABLE ifm2_a(x int); INSERT INTO ifm2_a VALUES (1),(2),(3);
SELECT add_provenance('ifm2_a');
CREATE TABLE ifm2_b(x int); INSERT INTO ifm2_b VALUES (1),(2),(3);
SELECT add_provenance('ifm2_b');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifm2_a;
  PERFORM set_prob(provsql, 0.4) FROM ifm2_b;
END $$;
CREATE VIEW ifm2_v AS SELECT a.x AS x FROM ifm2_a a, ifm2_b b WHERE a.x = b.x;
CREATE TEMP TABLE ifm2_t AS
  SELECT v1.x AS x, provenance() AS p
    FROM ifm2_v v1, ifm2_v v2 WHERE v1.x = v2.x GROUP BY v1.x;
SELECT remove_provenance('ifm2_t');
SELECT x, round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS t2_if,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS t2_pw
  FROM ifm2_t ORDER BY x;

-- (11) Nested subqueries / views (a subquery whose FROM contains another).  The
--      flattener recurses, collapsing views-over-views to base atoms before
--      inlining, and composes the slot path so markers reach the base input
--      through the nested rewrite.  boolean_provenance is off.
CREATE TABLE ifn_a(x int); INSERT INTO ifn_a VALUES (1),(2),(3);
SELECT add_provenance('ifn_a');
CREATE TABLE ifn_b(x int); INSERT INTO ifn_b VALUES (1),(2),(3);
SELECT add_provenance('ifn_b');
CREATE TABLE ifn_c(x int); INSERT INTO ifn_c VALUES (1),(2),(3);
SELECT add_provenance('ifn_c');
DO $$ BEGIN
  PERFORM set_prob(provsql, 0.5) FROM ifn_a;
  PERFORM set_prob(provsql, 0.4) FROM ifn_b;
  PERFORM set_prob(provsql, 0.3) FROM ifn_c;
END $$;

-- (11a) view over a view (two levels) joined with a base: V2 := SELECT x FROM
--       V1, V1 := SELECT x FROM A.  Flattens through both levels to A; joined
--       with B on x.  Matches pw (0.5 * 0.4).
CREATE VIEW ifn_v1 AS SELECT x FROM ifn_a;
CREATE VIEW ifn_v2 AS SELECT x FROM ifn_v1;
CREATE TEMP TABLE ifn_t1 AS
  SELECT v.x AS x, provenance() AS p
    FROM ifn_v2 v, ifn_b b WHERE v.x = b.x GROUP BY v.x;
SELECT remove_provenance('ifn_t1');
SELECT x, round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS t1_if,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS t1_pw
  FROM ifn_t1 ORDER BY x;

-- (11b) a *multi-relation* inner view nested inside an outer view: the path
--       composes through a multi-base level.  Inner V := A(x) JOIN B(x) on x,
--       outer wraps it, joined with C on x -> flat A(x),B(x),C(x) sharing root
--       x.  Matches pw (0.5 * 0.4 * 0.3).
CREATE VIEW ifn_in AS SELECT a.x AS x FROM ifn_a a, ifn_b b WHERE a.x = b.x;
CREATE VIEW ifn_out AS SELECT x FROM ifn_in;
CREATE TEMP TABLE ifn_t2 AS
  SELECT o.x AS x, provenance() AS p
    FROM ifn_out o, ifn_c c WHERE o.x = c.x GROUP BY o.x;
SELECT remove_provenance('ifn_t2');
SELECT x, round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS t2_if,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS t2_pw
  FROM ifn_t2 ORDER BY x;

-- (11c) inline nested derived tables (no views): FROM (SELECT FROM (SELECT FROM A)).
CREATE TEMP TABLE ifn_t3 AS
  SELECT s2.x AS x, provenance() AS p
    FROM (SELECT x FROM (SELECT x FROM ifn_a) s1) s2, ifn_b b
   WHERE s2.x = b.x GROUP BY s2.x;
SELECT remove_provenance('ifn_t3');
SELECT x, round(probability_evaluate(p, 'inversion-free')::numeric, 6)  AS t3_if,
          round(probability_evaluate(p, 'possible-worlds')::numeric, 6) AS t3_pw
  FROM ifn_t3 ORDER BY x;

RESET provsql.boolean_provenance;
RESET provsql.verbose_level;
