-- Truncation of the free blocks at the end of the file (core/store.h P7): only reusable blocks go, the free list and the -- high-water mark follow the file, and what is live is untouched. CREATE EXTENSION pg_sift_test CASCADE; NOTICE: installing required extension "pg_sift" SET client_min_messages = warning; CREATE FUNCTION info(idx regclass) RETURNS jsonb LANGUAGE sql AS $$ SELECT jsonb_object_agg(key, value) FROM sift._test_info(idx) $$; -- the file ends where the store says (nblocks = high_water) and every block is accounted for CREATE FUNCTION sized(idx regclass) RETURNS bool LANGUAGE sql AS $$ SELECT info(idx)->'nblocks' = info(idx)->'high_water' AND sift._test_check(idx) = 0 $$; CREATE TABLE t (v int); CREATE INDEX t_idx ON t USING sift_store_test (v); -- nothing free: nothing to give back SELECT sift._test_truncate('t_idx') AS freed, sized('t_idx'); freed | sized -------+------- 0 | t (1 row) -- four segments of 4 MB in a row SELECT sift._test_seg_write('t_idx', 4 << 20, 1) AS a \gset SELECT sift._test_seg_write('t_idx', 4 << 20, 2) AS b \gset SELECT sift._test_seg_write('t_idx', 4 << 20, 3) AS c \gset SELECT sift._test_seg_write('t_idx', 4 << 20, 4) AS d \gset -- the writes extended the file in runs and gave back the unused end of the last one (a 16th of a stream at most): what lies at the end is cut first SELECT sift._test_truncate('t_idx') >= 0 AS ran, sized('t_idx'); ran | sized -----+------- t | t (1 row) SELECT (info('t_idx')->>'high_water')::int AS full \gset -- the last two dropped: their blocks wait for the end of the dropping transaction (P4), so none goes yet BEGIN; SELECT sift._test_seg_drop('t_idx', :c) > 0 AS dropped_c, sift._test_seg_drop('t_idx', :d) > 0 AS dropped_d; dropped_c | dropped_d -----------+----------- t | t (1 row) SELECT sift._test_truncate('t_idx') AS freed, (info('t_idx')->>'nblocks')::int = :full AS file_unchanged; freed | file_unchanged -------+---------------- 0 | t (1 row) COMMIT; SELECT sift._test_truncate('t_idx') > 2 * (4 << 20) / 8192 AS freed_both, sized('t_idx'); freed_both | sized ------------+------- t | t (1 row) SELECT (info('t_idx')->>'nblocks')::int < :full * 0.6 AS half_the_file; half_the_file --------------- t (1 row) SELECT sift._test_seg_read('t_idx', :a) = (SELECT payload FROM sift._test_segments('t_idx') WHERE gen = :a) AS a_reads, sift._test_seg_read('t_idx', :b) = (SELECT payload FROM sift._test_segments('t_idx') WHERE gen = :b) AS b_reads; a_reads | b_reads ---------+--------- t | t (1 row) -- again: nothing more to give SELECT sift._test_truncate('t_idx') AS freed, sized('t_idx'); freed | sized -------+------- 0 | t (1 row) -- free space in front of a live segment is not the end of the file: it is reused, not truncated SELECT sift._test_seg_drop('t_idx', :a) > 0 AS dropped_a; dropped_a ----------- t (1 row) SELECT sift._test_truncate('t_idx') AS freed, (info('t_idx')->>'nblocks')::int = (info('t_idx')->>'high_water')::int AS sized; freed | sized -------+------- 0 | t (1 row) SELECT (info('t_idx')->>'high_water')::int AS hwm \gset SELECT sift._test_seg_write('t_idx', 2 << 20, 5) AS e \gset SELECT (info('t_idx')->>'high_water')::int = :hwm AS no_growth, sift._test_seg_read('t_idx', :e) > 0 AS e_reads; no_growth | e_reads -----------+--------- t | t (1 row) SELECT sized('t_idx'); sized ------- t (1 row) -- the blocks that grew the file again are given back too, and the file can grow after that SELECT sift._test_seg_drop('t_idx', :e) > 0 AS dropped_e; dropped_e ----------- t (1 row) SELECT sift._test_truncate('t_idx') > 0 AS freed, sized('t_idx'); freed | sized -------+------- f | t (1 row) SELECT sift._test_seg_write('t_idx', 8 << 20, 6) AS g \gset SELECT sift._test_seg_read('t_idx', :g) = (SELECT payload FROM sift._test_segments('t_idx') WHERE gen = :g) AS g_reads, sized('t_idx'); g_reads | sized ---------+------- t | t (1 row) -- unreferenced blocks (a crash between allocation and publication) stop the truncation until swept SELECT sift._test_alloc('t_idx', 8, 300, 0) > 0 AS leaked; leaked -------- t (1 row) SELECT sift._test_truncate('t_idx') AS freed, sift._test_check('t_idx') > 0 AS orphans; freed | orphans -------+--------- 0 | t (1 row) SELECT sift._test_sweep('t_idx') > 0 AS swept; swept ------- t (1 row) SELECT sift._test_truncate('t_idx') > 2000 AS freed, sized('t_idx'); freed | sized -------+------- t | t (1 row) -- a free list of many pages over a file that is all free but one segment SELECT sift._test_alloc('t_idx', 1, 1200, 2) > 0 AS fragmented; fragmented ------------ t (1 row) SELECT (info('t_idx')->>'freelist_pages')::int > 1 AS list_pages; list_pages ------------ t (1 row) SELECT sift._test_sweep('t_idx') >= 0 AS swept; swept ------- t (1 row) SELECT sift._test_truncate('t_idx') > 1000 AS freed, sized('t_idx'); freed | sized -------+------- t | t (1 row) SELECT sift._test_seg_drop('t_idx', :g) > 0 AS dropped_g; dropped_g ----------- t (1 row) SELECT sift._test_truncate('t_idx') > 0 AS freed, sized('t_idx'); freed | sized -------+------- t | t (1 row) SELECT (info('t_idx')->>'nblocks')::int < :full * 0.45 AS only_b_left; only_b_left ------------- f (1 row) SELECT sift._test_alloc('t_idx', 4, 10, 1) > 0 AS reused; reused -------- t (1 row) SELECT sized('t_idx'); sized ------- t (1 row) -- an unlogged index is cut the same way CREATE UNLOGGED TABLE ul (v int); CREATE INDEX ul_idx ON ul USING sift_store_test (v); SELECT sift._test_alloc('ul_idx', 8, 300, 1) > 0 AS allocated; allocated ----------- t (1 row) SELECT sift._test_truncate('ul_idx') > 2000 AS freed, sized('ul_idx'); freed | sized -------+------- t | t (1 row) DROP TABLE t, ul; DROP FUNCTION sized(regclass); DROP FUNCTION info(regclass); DROP EXTENSION pg_sift_test; DROP EXTENSION pg_sift;