-- ----------------------------------------------------------------------
-- test/bench/reachability_bench.sql
--
-- Route benchmark for the bounded-treewidth reachability compiler: which
-- method the cost chooser resolves to, per requested guarantee, on an
-- ordinary WITH RECURSIVE reachability query over treelike data.
--
-- The data is a directed ladder: two rails 1..n and n+1..2n, rung edges
-- between them, so the Gaifman graph of the edge relation has treewidth 2
-- for every n -- bounded, but not a tree (the ladder is 2-connected, and
-- the reverse rungs make it cyclic; the compiler is native on cycles, no
-- fixpoint involved, whereas the generic recursive evaluation reaches
-- them only under an absorptive-or-lower provenance class).  Under
-- provsql.provenance = 'boolean' the recursive-CTE lowering recognises
-- the linear reachability shape and compiles, along a tree decomposition
-- of the *data* graph, one certified d-D per reachable vertex; the
-- chooser then reads the persisted certificate and the linear
-- certified-island pass serves every guarantee ("exact when cheap": the
-- exact value trivially meets any tolerance).  That pass is what
-- 'independent' performs, but the root carries the compiler's route tag,
-- so it is reported under the route's own name, 'reachability'.
--
-- Deliberately small data -- the point is the route taken and its
-- stability across guarantees, not scale; ucq_joint_bench.sql is the
-- scaling counterpart on the data side.
--
-- Run:
--   createdb reachbench && psql reachbench -X -f test/bench/reachability_bench.sql
--
-- Reference picks (this machine, PostgreSQL 18, provsql 1.12.0-dev;
-- n = 24, edge probability 0.9, target vertex 48):
--
--   route         exact         rel eps=.1    rel eps=.3    additive      det d=0       route_off
--   reachability  reachability  reachability  reachability  reachability  reachability  tree-decomposition
--
-- One method for every guarantee, ~11 ms each, P = 0.748201.  No GUC disables
-- the route -- it fires for every absorptive-or-lower class, 'boolean'
-- and 'absorptive' alike -- so the route_off leg raises the class to
-- 'semiring' instead, where the generic eval_recursive fixpoint builds
-- the literal circuit and the chooser has to pay for it
-- (tree-decomposition, P = 0.288396).  That leg necessarily runs the
-- ACYCLIC twin of the ladder, since cyclic data has no finite semiring
-- provenance: the comparison isolates the compiler, not the cycle.
-- ----------------------------------------------------------------------
\set ECHO none
\timing off
CREATE EXTENSION IF NOT EXISTS provsql CASCADE;
CREATE SCHEMA IF NOT EXISTS provsql_test;
-- After CREATE EXTENSION: installing the required uuid-ossp through
-- CASCADE resets search_path to public, so setting it earlier is lost.
SET search_path TO provsql_test, provsql, public;

DROP TABLE IF EXISTS reach_edge CASCADE;
CREATE TABLE reach_edge(src int, dst int);

-- Ladder on 2n vertices: rails 1..n and n+1..2n (forward), rungs both
-- ways (so the reachability relation is genuinely cyclic).
DO $$
DECLARE n int := 24;
BEGIN
  INSERT INTO reach_edge SELECT i, i+1 FROM generate_series(1, n-1) i;
  INSERT INTO reach_edge SELECT n+i, n+i+1 FROM generate_series(1, n-1) i;
  INSERT INTO reach_edge SELECT i, n+i FROM generate_series(1, n) i;
  INSERT INTO reach_edge SELECT n+i, i FROM generate_series(1, n) i;
END $$;
SELECT add_provenance('reach_edge');
DO $$ BEGIN PERFORM set_prob(provenance(), 0.9) FROM reach_edge; END $$;

-- The same ladder with one-way rungs only: still treewidth 2, but now
-- acyclic, so the generic fixpoint terminates and the route_off leg
-- lands on a method rather than on an error.
DROP TABLE IF EXISTS reach_dag CASCADE;
CREATE TABLE reach_dag(src int, dst int);
DO $$
DECLARE n int := 24;
BEGIN
  INSERT INTO reach_dag SELECT i, i+1 FROM generate_series(1, n-1) i;
  INSERT INTO reach_dag SELECT n+i, n+i+1 FROM generate_series(1, n-1) i;
  INSERT INTO reach_dag SELECT i, n+i FROM generate_series(1, n) i;
END $$;
SELECT add_provenance('reach_dag');
DO $$ BEGIN PERFORM set_prob(provenance(), 0.9) FROM reach_dag; END $$;

DROP TABLE IF EXISTS reach_res CASCADE;
CREATE TABLE reach_res(request text, method text, prob numeric, ms double precision);

DO $$
DECLARE
  v double precision; i int; t0 timestamptz;
  -- the four guarantees of the paper's route table, plus the
  -- no-failure (delta=0) request, as in dtree_bench.sql.
  reqs text[][] := ARRAY[['exact',NULL,'exact'],
                         ['relative','epsilon=0.1,delta=0.05','rel_tight'],
                         ['relative','epsilon=0.3,delta=0.2','rel_loose'],
                         ['additive','epsilon=0.1,delta=0.05','additive'],
                         ['additive','epsilon=0.1,delta=0','det_d0']];
BEGIN
  -- ===== route ON: 'boolean' licenses the decomposition-aligned driver ====
  PERFORM set_config('provsql.provenance','boolean',false);
  CREATE TEMP TABLE _reach1 AS
    WITH RECURSIVE reach(node) AS (
        SELECT 1
      UNION
        SELECT e.dst FROM reach_edge e JOIN reach r ON e.src = r.node
    )
    SELECT node, provenance() AS p FROM reach;
  PERFORM remove_provenance('_reach1');

  FOR i IN 1..array_length(reqs,1) LOOP
    PERFORM set_config('provsql.last_eval_method','',false);
    t0 := clock_timestamp();
    SELECT round(probability_evaluate(p, reqs[i][1], reqs[i][2])::numeric,6)
      INTO v FROM _reach1 WHERE node = 48;
    INSERT INTO reach_res VALUES (reqs[i][3],
      current_setting('provsql.last_eval_method'), v,
      extract(epoch FROM clock_timestamp()-t0)*1000);
  END LOOP;

  -- ===== route OFF: no GUC disables the route (it fires for every
  -- absorptive-or-lower class, 'boolean' and 'absorptive' alike), so the
  -- comparison runs the class ABOVE it, 'semiring', where the generic
  -- eval_recursive fixpoint builds the literal circuit and the chooser
  -- faces that instead.  On the acyclic ladder: cyclic data has no finite
  -- semiring provenance, so it is the class, not the cycle, that decides
  -- what this leg can measure. =========================================
  BEGIN
    PERFORM set_config('provsql.provenance','semiring',false);
    CREATE TEMP TABLE _reach0 AS
      WITH RECURSIVE reach(node) AS (
          SELECT 1
        UNION
          SELECT e.dst FROM reach_dag e JOIN reach r ON e.src = r.node
      )
      SELECT node, provenance() AS p FROM reach;
    PERFORM remove_provenance('_reach0');
    PERFORM set_config('provsql.last_eval_method','',false);
    t0 := clock_timestamp();
    SELECT round(probability_evaluate(p)::numeric,6) INTO v FROM _reach0 WHERE node = 48;
    INSERT INTO reach_res VALUES ('route_off',
      current_setting('provsql.last_eval_method'), v,
      extract(epoch FROM clock_timestamp()-t0)*1000);
  EXCEPTION WHEN OTHERS THEN
    INSERT INTO reach_res VALUES ('route_off', 'ERROR: '||SQLERRM, NULL, NULL);
  END;
END $$;
RESET provsql.provenance;

\pset format aligned
\echo '--- resolved method per guarantee (reachability route) ---'
\echo '--- (route_off runs the acyclic twin: no finite semiring provenance on cycles) ---'
SELECT request, method, prob, round(ms::numeric,1) AS ms
FROM reach_res
ORDER BY CASE request WHEN 'exact' THEN 1 WHEN 'rel_tight' THEN 2
                      WHEN 'rel_loose' THEN 3 WHEN 'additive' THEN 4
                      WHEN 'det_d0' THEN 5 ELSE 6 END;

\echo '--- one line, uniform with the paper route table ---'
SELECT 'reachability' AS route,
       max(method) FILTER (WHERE request='exact')     AS exact,
       max(method) FILTER (WHERE request='rel_tight') AS rel_tight,
       max(method) FILTER (WHERE request='rel_loose') AS rel_loose,
       max(method) FILTER (WHERE request='additive')  AS additive,
       max(method) FILTER (WHERE request='det_d0')    AS det_d0,
       max(method) FILTER (WHERE request='route_off') AS route_off
FROM reach_res;

DROP TABLE reach_res;
SELECT remove_provenance('reach_edge'); SELECT remove_provenance('reach_dag');
DROP TABLE reach_edge, reach_dag;
