-- SiftAgg over number leaves (int2, int4, int8, float4, float8 paths, a numeric(p, s) column) when a value is out of the range the leaf declares: a -- JSON number too big for its path type is the shredder's overflow like a string, and the index cannot read it. A query that reads such a leaf keeps -- the normal plan, so that it gives the normal plan's rows (the rows its WHERE leaves out) or its error (a row that reaches the value); a query that -- reads the other leaves, and a numeric column whose sums outgrow 64 bits, stay on the scan. Every query runs with and without the pushdown. CREATE EXTENSION pg_sift; CREATE EXTENSION pg_sift_helpers; SET max_parallel_workers_per_gather = 0; SET seq_page_cost = 1000; SET random_page_cost = 1000; CREATE TABLE r_t (id int PRIMARY KEY, amt numeric(18, 2), doc jsonb) WITH (autovacuum_enabled = false); -- from the row 1000 on: 70000 in the int2 path, 5000000000 in the int4 path, 1e30 in the int8 path, 1e300 in the float4 path, 1e400 in the float8 path INSERT INTO r_t SELECT i, 9999999999999999.99 - i % 7, jsonb_build_object('lines', (SELECT jsonb_agg(jsonb_build_object('k', (array['a', 'b', 'c'])[1 + (i + j) % 3], 'h', CASE WHEN i >= 1000 AND i % 50 = 0 THEN 70000 ELSE j % 100 END, 'n', CASE WHEN i >= 1000 AND i % 60 = 0 THEN 5000000000 ELSE j % 100 END, 'b', CASE WHEN i >= 1000 AND i % 70 = 0 THEN 1e30::numeric ELSE j % 100 END, 'f', CASE WHEN i >= 1000 AND i % 80 = 0 THEN 1e300::numeric ELSE (j % 100) / 4.0 END, 'd', CASE WHEN i >= 1000 AND i % 90 = 0 THEN ('1' || repeat('0', 400))::numeric ELSE (j % 100) / 4.0 END)) FROM generate_series(1, 1 + i % 4) j)) FROM generate_series(1, 5000) i; CREATE INDEX r_t_col ON r_t USING sift_col (id, amt, doc) WITH (paths = 'doc.lines[]{k text, h int2, n int4, b int8, f float4, d float8}'); VACUUM (FREEZE, ANALYZE) r_t; \ir test/agg_helpers.sql -- the rows with and without the pushdown are the same (and not empty) CREATE FUNCTION same(q text) RETURNS bool LANGUAGE sql AS $$ SELECT sift_helpers.agg_equals_normal_nonempty(q) $$; -- the error of the query with and without the pushdown is the same (and there is one) CREATE FUNCTION same_error(q text) RETURNS bool LANGUAGE plpgsql AS $$ DECLARE e_on text; e_off text; BEGIN SET sift.enable_agg = on; BEGIN EXECUTE q; EXCEPTION WHEN OTHERS THEN e_on := SQLSTATE || ' ' || SQLERRM; END; SET sift.enable_agg = off; BEGIN EXECUTE q; EXCEPTION WHEN OTHERS THEN e_off := SQLSTATE || ' ' || SQLERRM; END; RESET sift.enable_agg; RETURN e_on IS NOT NULL AND e_on = e_off; END $$; CREATE FUNCTION lines_q(leaf text, typ text, sel text, filter text DEFAULT 'true', grp text DEFAULT 'l.k') RETURNS text LANGUAGE sql AS $$ SELECT format('SELECT %s FROM r_t t, jsonb_to_recordset(t.doc -> ''lines'') AS l(k text, %s %s) WHERE %s GROUP BY %s', sel, leaf, typ, filter, grp) $$; -- every path with a value out of its range: the rows of the clean ones read by the normal plan, and its error for the rest SELECT pushed(lines_q('h', 'int2', 'l.k, sum(l.h)', 't.id < 1000')) AS int2_pushed, same(lines_q('h', 'int2', 'l.k, sum(l.h)', 't.id < 1000')) AS int2_same, same_error(lines_q('h', 'int2', 'l.k, sum(l.h)')) AS int2_error; SELECT pushed(lines_q('n', 'int4', 'l.k, sum(l.n), min(l.n)', 't.id < 1000')) AS int4_pushed, same(lines_q('n', 'int4', 'l.k, sum(l.n), min(l.n)', 't.id < 1000')) AS int4_same, same_error(lines_q('n', 'int4', 'l.k, sum(l.n), min(l.n)')) AS int4_error; SELECT pushed(lines_q('b', 'int8', 'l.k, sum(l.b)', 't.id < 1000')) AS int8_pushed, same(lines_q('b', 'int8', 'l.k, sum(l.b)', 't.id < 1000')) AS int8_same, same_error(lines_q('b', 'int8', 'l.k, sum(l.b)')) AS int8_error; SELECT pushed(lines_q('f', 'float4', 'l.k, avg(l.f)', 't.id < 1000')) AS float4_pushed, same(lines_q('f', 'float4', 'l.k, avg(l.f)', 't.id < 1000')) AS float4_same, same_error(lines_q('f', 'float4', 'l.k, avg(l.f)')) AS float4_error; SELECT pushed(lines_q('d', 'float8', 'l.k, avg(l.d)', 't.id < 1000')) AS float8_pushed, same(lines_q('d', 'float8', 'l.k, avg(l.d)', 't.id < 1000')) AS float8_same, same_error(lines_q('d', 'float8', 'l.k, avg(l.d)')) AS float8_error; -- as a group key and in a filter SELECT pushed(lines_q('n', 'int4', 'l.n, count(*)', 't.id < 1000', 'l.n')) AS key_pushed, same(lines_q('n', 'int4', 'l.n, count(*)', 't.id < 1000', 'l.n')) AS key_same; SELECT pushed(lines_q('n', 'int4', 'l.k, count(*) FILTER (WHERE l.n > 50)', 't.id < 1000')) AS filter_pushed, same(lines_q('n', 'int4', 'l.k, count(*) FILTER (WHERE l.n > 50)', 't.id < 1000')) AS filter_same; -- the leaf that holds no such value stays on the scan, whatever the other leaves hold SELECT pushed('SELECT l.k, count(*) FROM r_t t, jsonb_to_recordset(t.doc -> ''lines'') AS l(k text) GROUP BY l.k') AS key_leaf_pushed, same('SELECT l.k, count(*) FROM r_t t, jsonb_to_recordset(t.doc -> ''lines'') AS l(k text) GROUP BY l.k') AS key_leaf_same; -- a numeric(18, 2) column: the sum outgrows 64 bits of the scaled values and is still the exact numeric of the normal plan SELECT pushed('SELECT count(*), sum(amt), min(amt), max(amt) FROM r_t') AS amt_pushed, same('SELECT count(*), sum(amt), min(amt), max(amt) FROM r_t') AS amt_same; SELECT pushed('SELECT id % 3 AS g, sum(amt), avg(amt) FROM r_t GROUP BY 1') AS amt_group_pushed, same('SELECT id % 3 AS g, sum(amt), avg(amt) FROM r_t GROUP BY 1') AS amt_group_same; -- a number out of range written after the index was built: in the write buffer, then in a segment, the same INSERT INTO r_t VALUES (100000, 1, '{"lines": [{"k": "a", "h": 1, "n": 5000000000, "b": 1, "f": 1.5, "d": 1.5}]}'); SELECT pushed(lines_q('n', 'int4', 'l.k, sum(l.n)', 't.id < 1000')) AS buffer_int4_pushed, same(lines_q('n', 'int4', 'l.k, sum(l.n)', 't.id < 1000')) AS buffer_int4_same; SELECT pushed(lines_q('h', 'int2', 'l.k, sum(l.h)', 't.id < 1000')) AS buffer_int2_pushed, same(lines_q('h', 'int2', 'l.k, sum(l.h)', 't.id < 1000')) AS buffer_int2_same; SELECT sift.col_promote('r_t_col') >= 0 AS promoted; SELECT pushed(lines_q('n', 'int4', 'l.k, sum(l.n)', 't.id < 1000')) AS promoted_int4_pushed, same(lines_q('n', 'int4', 'l.k, sum(l.n)', 't.id < 1000')) AS promoted_int4_same; -- the shared database of a plain installcheck goes on with the next test SET client_min_messages = warning; DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT USAGE ON SCHEMA public TO PUBLIC; RESET client_min_messages; DROP EXTENSION pg_sift_helpers; DROP EXTENSION pg_sift;