-- Test for issue #3055: Don't spin up extra parallel workers for tiny segments
--
-- This test verifies that the paradedb.min_rows_per_worker GUC correctly
-- limits parallel workers based on row count, so each worker processes
-- enough rows for parallelism to be worthwhile (~10ms startup overhead).
--
-- Since #4664 the cost model decides cost-able scans and ignores this GUC; the
-- row heuristic (this GUC) governs only uncostable scans, so Tests 1-3 use one.
--
-- However, when the scan declares sorted output (TopK with ORDER BY, or
-- sorted columnar), it must visit ALL segments to produce globally correct
-- results. In that case, cost is segment-scan dominated and the
-- min_rows_per_worker threshold is bypassed.
--
-- Based on benchmarks, the crossover point where parallel becomes beneficial
-- is around 300K rows total. Default min_rows_per_worker is 300000.
--
-- See: https://github.com/paradedb/paradedb/issues/3055

CREATE EXTENSION IF NOT EXISTS pg_search;

-- Enable parallel workers globally
SET max_parallel_workers_per_gather = 2;
SET max_parallel_workers = 8;

-- Force postgres to consider parallel plans
SET parallel_tuple_cost = 0;
SET parallel_setup_cost = 0;
SET min_parallel_table_scan_size = 0;

-- Create test table
DROP TABLE IF EXISTS items CASCADE;
CREATE TABLE items (
    id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL
);

-- Create BM25 index BEFORE inserting data to create multiple segments
CREATE INDEX items_bm25_idx ON items
USING bm25 (id, name)
WITH (key_field = 'id');

-- Insert first batch of data (creates segment 1)
INSERT INTO items (name) SELECT 'item ' || g FROM generate_series(1, 5000) g;

-- Insert second batch (creates segment 2)
INSERT INTO items (name) SELECT 'item ' || g FROM generate_series(5001, 10000) g;

-- ANALYZE to get accurate row estimates (required for threshold check to work)
ANALYZE items;

-- Verify reltuples is set correctly
SELECT relname, reltuples FROM pg_class WHERE relname = 'items';

-- Tests 1-3 use an uncostable predicate (id <> a runtime subquery) so the row
-- heuristic decides; the planner estimates ~1000 rows, capping at 1000/min_rows.

-- Test 1: min_rows_per_worker=2000 -> 1000/2000 = 0 workers -> serial
SET paradedb.min_rows_per_worker = 2000;

EXPLAIN (COSTS OFF, VERBOSE, TIMING OFF)
SELECT id, name FROM items WHERE name @@@ 'item' AND id <> (SELECT max(id) + 1 FROM items);

-- Test 2: min_rows_per_worker=500 -> 1000/500 = 2 workers -> parallel
SET paradedb.min_rows_per_worker = 500;

EXPLAIN (COSTS OFF, VERBOSE, TIMING OFF)
SELECT id, name FROM items WHERE name @@@ 'item' AND id <> (SELECT max(id) + 1 FROM items);

-- Test 3: min_rows_per_worker=0 -> no cap -> parallel by segment count
SET paradedb.min_rows_per_worker = 0;

EXPLAIN (COSTS OFF, VERBOSE, TIMING OFF)
SELECT id, name FROM items WHERE name @@@ 'item' AND id <> (SELECT max(id) + 1 FROM items);

-- Test 4: TopK sorted scan bypasses min_rows_per_worker
-- Even with a high min_rows_per_worker threshold, TopK queries that declare sorted
-- output (ORDER BY score) should still use parallel workers because they must scan
-- ALL segments to produce globally correct top-K results. The cost is segment-scan
-- dominated, not row-count dominated.
SET paradedb.min_rows_per_worker = 300000;

EXPLAIN (COSTS OFF, VERBOSE, TIMING OFF)
SELECT id, name FROM items
WHERE name @@@ pdb.match('item')
ORDER BY paradedb.score(id) DESC, id
LIMIT 10;

SELECT id, name FROM items
WHERE name @@@ pdb.match('item')
ORDER BY paradedb.score(id) DESC, id
LIMIT 10;

-- Test 5: TopK with ORDER BY column also bypasses min_rows_per_worker
-- TopK with ORDER BY id (not score) also declares sorted output and must visit
-- all segments, so parallel should be enabled despite high threshold.
EXPLAIN (COSTS OFF, VERBOSE, TIMING OFF)
SELECT id, name FROM items WHERE name @@@ 'item' ORDER BY id LIMIT 10;

-- Test 6: Verify unanalyzed table behavior
-- When reltuples is unknown (-1), parallel should still be allowed
-- (we shouldn't limit workers when we can't trust the row estimate)
DROP TABLE items;

CREATE TABLE items (
    id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL
) WITH (autovacuum_enabled = off);

CREATE INDEX items_bm25_idx ON items
USING bm25 (id, name)
WITH (key_field = 'id');

-- Insert data in two batches but DON'T analyze
INSERT INTO items (name) SELECT 'item ' || g FROM generate_series(1, 5000) g;
INSERT INTO items (name) SELECT 'item ' || g FROM generate_series(5001, 10000) g;

-- Verify reltuples is -1 (unanalyzed)
SELECT relname, reltuples FROM pg_class WHERE relname = 'items';

-- Even with high threshold that would normally disable parallel for unsorted scans,
-- parallel should still be used because we don't have reliable row estimates
SET paradedb.min_rows_per_worker = 300000;

EXPLAIN (COSTS OFF, VERBOSE, TIMING OFF)
SELECT id, name FROM items WHERE name @@@ 'item';

-- Clean up
DROP TABLE items;

-- Reset GUCs
RESET paradedb.min_rows_per_worker;
RESET max_parallel_workers_per_gather;
RESET max_parallel_workers;
RESET parallel_tuple_cost;
RESET parallel_setup_cost;
RESET min_parallel_table_scan_size;
