-- Reproducible benchmarks for pg_table_range.
--
--   cargo pgrx run pg18
--   \i bench/benchmark.sql
--
-- Reports planning and execution time separately (warm) via EXPLAIN (ANALYZE), for:
--   1. Plan-vs-execution tradeoff: a selective non-key predicate, pruning on vs. off.
--   2. Honest comparison to native partition pruning: two identical columns, one the
--      partition key (pruned natively) and one not (pruned by table_range).
--
-- Note: cargo pgrx run reuses its database. If you have previously loaded older versions
-- of this extension, stale event triggers can interfere; this script drops the extension
-- and recreates it for a clean baseline.

\set ECHO none
\pset pager off

DROP EXTENSION IF EXISTS pg_table_range CASCADE;
-- Clear any event triggers left behind by older builds (no-ops on a clean DB).
DROP EVENT TRIGGER IF EXISTS table_range_hide_indexes_trg;
DROP EVENT TRIGGER IF EXISTS table_range_drop_trg;
DROP EVENT TRIGGER IF EXISTS table_range_opclass_sync_trg;
DROP FUNCTION IF EXISTS table_range_hide_indexes() CASCADE;
CREATE EXTENSION pg_table_range;

-- ====================================================================================
-- 1. Plan-vs-execution tradeoff: 300 partitions x 8,000 rows, predicate hits 1 partition.
--    `region` is the partition key; `val` is a disjoint band per partition (non-key).
-- ====================================================================================
DROP TABLE IF EXISTS bench_a CASCADE;
CREATE TABLE bench_a (region int, val bigint, pad text) PARTITION BY LIST (region);
SELECT format('CREATE TABLE bench_a_%s PARTITION OF bench_a FOR VALUES IN (%s);', g, g)
FROM generate_series(1, 300) g \gexec
INSERT INTO bench_a
SELECT g, g * 1000000 + s, repeat('x', 40)
FROM generate_series(1, 300) g, generate_series(0, 7999) s;
VACUUM ANALYZE bench_a;
CREATE INDEX bench_a_tr ON bench_a USING table_range (val);

SET table_range.enable_pruning = on;
SELECT count(*) FROM bench_a WHERE val = 150004000;  -- warm
\echo '==== A: pruning ON (scans 1 of 300 partitions) ===='
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_a WHERE val = 150004000;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_a WHERE val = 150004000;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_a WHERE val = 150004000;

SET table_range.enable_pruning = off;
SELECT count(*) FROM bench_a WHERE val = 150004000;  -- warm
\echo '==== A: pruning OFF (scans all 300 partitions) ===='
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_a WHERE val = 150004000;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_a WHERE val = 150004000;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_a WHERE val = 150004000;
DROP TABLE bench_a CASCADE;

-- ====================================================================================
-- 2. table_range vs. native pruning: 2,000 partitions, two identical columns.
--    `pk` is the RANGE partition key (native pruning); `nk` holds the same values but is
--    NOT the key (table_range pruning). Same `=` predicate, so the only difference is
--    which mechanism prunes.
-- ====================================================================================
DROP TABLE IF EXISTS bench_b CASCADE;
CREATE TABLE bench_b (pk bigint, nk bigint) PARTITION BY RANGE (pk);
SELECT format('CREATE TABLE bench_b_%s PARTITION OF bench_b FOR VALUES FROM (%s) TO (%s);',
              g, g * 1000, (g + 1) * 1000)
FROM generate_series(1, 2000) g \gexec
INSERT INTO bench_b
SELECT v, v FROM (SELECT g * 1000 + s AS v
                  FROM generate_series(1, 2000) g, generate_series(0, 49) s) q;
VACUUM ANALYZE bench_b;
CREATE INDEX bench_b_tr ON bench_b USING table_range (nk);

SET table_range.enable_pruning = off;
SELECT count(*) FROM bench_b WHERE pk = 1000500;  -- warm
\echo '==== B: NATIVE pruning (predicate on the partition key) ===='
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_b WHERE pk = 1000500;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_b WHERE pk = 1000500;

SET table_range.enable_pruning = on;
SELECT count(*) FROM bench_b WHERE nk = 1000500;  -- warm
\echo '==== B: table_range pruning (same predicate, non-key column) ===='
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_b WHERE nk = 1000500;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_b WHERE nk = 1000500;

SET table_range.enable_pruning = off;
SELECT count(*) FROM bench_b WHERE nk = 1000500;  -- warm
\echo '==== B: NO pruning (non-key column, scans all 2,000 partitions) ===='
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_b WHERE nk = 1000500;
DROP TABLE bench_b CASCADE;

-- ====================================================================================
-- 3. table_range vs. CHECK constraint exclusion: the built-in way to prune on a non-key
--    column is a data-range CHECK on each partition. Same table, same predicate; we toggle
--    constraint_exclusion vs. table_range.enable_pruning to compare the two mechanisms.
-- ====================================================================================
DROP TABLE IF EXISTS bench_c CASCADE;
CREATE TABLE bench_c (region int, nk bigint) PARTITION BY LIST (region);
-- Each partition gets a data-driven CHECK on the non-key column nk.
SELECT format(
  'CREATE TABLE bench_c_%s PARTITION OF bench_c FOR VALUES IN (%s); '
  'ALTER TABLE bench_c_%s ADD CONSTRAINT bench_c_%s_nk CHECK (nk >= %s AND nk <= %s);',
  g, g, g, g, g * 1000, g * 1000 + 999)
FROM generate_series(1, 2000) g \gexec
INSERT INTO bench_c
SELECT g, g * 1000 + s FROM generate_series(1, 2000) g, generate_series(0, 49) s;
VACUUM ANALYZE bench_c;
CREATE INDEX bench_c_tr ON bench_c USING table_range (nk);

SET constraint_exclusion = on;
SET table_range.enable_pruning = off;
SELECT count(*) FROM bench_c WHERE nk = 1000025;  -- warm
\echo '==== C: CHECK constraint exclusion (table_range off) ===='
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_c WHERE nk = 1000025;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_c WHERE nk = 1000025;

SET constraint_exclusion = off;
SET table_range.enable_pruning = on;
SELECT count(*) FROM bench_c WHERE nk = 1000025;  -- warm
\echo '==== C: table_range pruning (CHECK exclusion off) ===='
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_c WHERE nk = 1000025;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_c WHERE nk = 1000025;

SET constraint_exclusion = off;
SET table_range.enable_pruning = off;
SELECT count(*) FROM bench_c WHERE nk = 1000025;  -- warm
\echo '==== C: no pruning (both off, scans all 2,000 partitions) ===='
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_c WHERE nk = 1000025;
DROP TABLE bench_c CASCADE;

-- ====================================================================================
-- 4. (Optional) The lock-table wall. At ~10,000 partitions a non-key predicate exhausts
--    the lock table on default settings:
--      ERROR: out of shared memory
--      HINT:  You might need to increase "max_locks_per_transaction".
--    Raise max_locks_per_transaction (e.g. to 4000) and restart to push the wall out.
--    Native key pruning is unaffected because it never locks pruned partitions.
-- ====================================================================================
