mirror of
https://github.com/postgres/postgres.git
synced 2025-12-10 00:06:45 -05:00
Add a test for incomplete splits in B-tree indexes
To increase our test coverage in general, and because I will add onto this in the next commit to also test amcheck with incomplete splits. This is copied from the similar test we had for GIN indexes. B-tree's incomplete splits work similarly to GIN's, so with small changes, the same test works for B-tree too. Reviewed-by: Peter Geoghegan <pg@bowt.ie> Discussion: https://www.postgresql.org/message-id/abd65090-5336-42cc-b768-2bdd66738404@iki.fi
This commit is contained in:
parent
f894acb24a
commit
1e4e5783e7
@ -26,6 +26,7 @@
|
||||
#include "miscadmin.h"
|
||||
#include "storage/lmgr.h"
|
||||
#include "storage/predicate.h"
|
||||
#include "utils/injection_point.h"
|
||||
|
||||
/* Minimum tree height for application of fastpath optimization */
|
||||
#define BTREE_FASTPATH_MIN_LEVEL 2
|
||||
@ -1239,6 +1240,13 @@ _bt_insertonpg(Relation rel,
|
||||
* page.
|
||||
*----------
|
||||
*/
|
||||
#ifdef USE_INJECTION_POINTS
|
||||
if (P_ISLEAF(opaque))
|
||||
INJECTION_POINT("nbtree-leave-leaf-split-incomplete", NULL);
|
||||
else
|
||||
INJECTION_POINT("nbtree-leave-internal-split-incomplete", NULL);
|
||||
#endif
|
||||
|
||||
_bt_insert_parent(rel, heaprel, buf, rbuf, stack, isroot, isonly);
|
||||
}
|
||||
else
|
||||
@ -2285,6 +2293,7 @@ _bt_finish_split(Relation rel, Relation heaprel, Buffer lbuf, BTStack stack)
|
||||
/* Was this the only page on the level before split? */
|
||||
wasonly = (P_LEFTMOST(lpageop) && P_RIGHTMOST(rpageop));
|
||||
|
||||
INJECTION_POINT("nbtree-finish-incomplete-split", NULL);
|
||||
elog(DEBUG1, "finishing incomplete split of %u/%u",
|
||||
BufferGetBlockNumber(lbuf), BufferGetBlockNumber(rbuf));
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ subdir('index')
|
||||
subdir('injection_points')
|
||||
subdir('ldap_password_func')
|
||||
subdir('libpq_pipeline')
|
||||
subdir('nbtree')
|
||||
subdir('oauth_validator')
|
||||
subdir('plsample')
|
||||
subdir('spgist_name_ops')
|
||||
|
||||
28
src/test/modules/nbtree/Makefile
Normal file
28
src/test/modules/nbtree/Makefile
Normal file
@ -0,0 +1,28 @@
|
||||
# src/test/modules/nbtree/Makefile
|
||||
|
||||
EXTRA_INSTALL = src/test/modules/injection_points
|
||||
|
||||
REGRESS = nbtree_incomplete_splits
|
||||
|
||||
ifdef USE_PGXS
|
||||
PG_CONFIG = pg_config
|
||||
PGXS := $(shell $(PG_CONFIG) --pgxs)
|
||||
include $(PGXS)
|
||||
else
|
||||
subdir = src/test/modules/nbtree
|
||||
top_builddir = ../../../..
|
||||
include $(top_builddir)/src/Makefile.global
|
||||
|
||||
# XXX: This test is conditional on enable_injection_points in the
|
||||
# parent Makefile, so we should never get here in the first place if
|
||||
# injection points are not enabled. But the buildfarm 'misc-check'
|
||||
# step doesn't pay attention to the if-condition in the parent
|
||||
# Makefile. To work around that, disable running the test here too.
|
||||
ifeq ($(enable_injection_points),yes)
|
||||
include $(top_srcdir)/contrib/contrib-global.mk
|
||||
else
|
||||
check:
|
||||
@echo "injection points are disabled in this build"
|
||||
endif
|
||||
|
||||
endif
|
||||
179
src/test/modules/nbtree/expected/nbtree_incomplete_splits.out
Normal file
179
src/test/modules/nbtree/expected/nbtree_incomplete_splits.out
Normal file
@ -0,0 +1,179 @@
|
||||
--
|
||||
-- Test incomplete splits in B-tree indexes.
|
||||
--
|
||||
-- We use a test table with integers from 1 to :next_i. Each integer
|
||||
-- occurs exactly once, no gaps or duplicates, although the index does
|
||||
-- contain some duplicates because some of the inserting transactions
|
||||
-- are rolled back during the test. The exact contents of the table
|
||||
-- depend on the physical layout of the index, which in turn depends
|
||||
-- at least on the block size, so instead of checking the exact
|
||||
-- contents, we check those invariants. :next_i psql variable is
|
||||
-- maintained at all times to hold the last inserted integer + 1.
|
||||
--
|
||||
-- This uses injection points to cause errors that leave some page
|
||||
-- splits in "incomplete" state
|
||||
set client_min_messages TO 'warning';
|
||||
create extension if not exists injection_points;
|
||||
reset client_min_messages;
|
||||
-- Make all injection points local to this process, for concurrency.
|
||||
SELECT injection_points_set_local();
|
||||
injection_points_set_local
|
||||
----------------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
-- Use the index for all the queries
|
||||
set enable_seqscan=off;
|
||||
-- Print a NOTICE whenever an incomplete split gets fixed
|
||||
SELECT injection_points_attach('nbtree-finish-incomplete-split', 'notice');
|
||||
injection_points_attach
|
||||
-------------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
--
|
||||
-- First create the test table and some helper functions
|
||||
--
|
||||
create table nbtree_incomplete_splits(i int4) with (autovacuum_enabled = off);
|
||||
create index nbtree_incomplete_splits_i_idx on nbtree_incomplete_splits using btree (i);
|
||||
-- Inserts 'n' rows to the test table. Pass :next_i as the first
|
||||
-- argument, returns the new value for :next_i.
|
||||
create function insert_n(first_i int, n int) returns int language plpgsql as $$
|
||||
begin
|
||||
insert into nbtree_incomplete_splits select g from generate_series(first_i, first_i + n - 1) as g;
|
||||
return first_i + n;
|
||||
end;
|
||||
$$;
|
||||
-- Inserts to the table until an insert fails. Like insert_n(), returns the
|
||||
-- new value for :next_i.
|
||||
create function insert_until_fail(next_i int, step int default 1) returns int language plpgsql as $$
|
||||
declare
|
||||
i integer;
|
||||
begin
|
||||
-- Insert rows in batches of 'step' rows each, until an error occurs.
|
||||
i := 0;
|
||||
loop
|
||||
begin
|
||||
select insert_n(next_i, step) into next_i;
|
||||
exception when others then
|
||||
raise notice 'failed with: %', sqlerrm;
|
||||
exit;
|
||||
end;
|
||||
|
||||
-- The caller is expected to set an injection point that eventually
|
||||
-- causes an error. But bail out if still no error after 10000
|
||||
-- attempts, so that we don't get stuck in an infinite loop.
|
||||
i := i + 1;
|
||||
if i = 10000 then
|
||||
raise 'no error on inserts after % iterations', i;
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
return next_i;
|
||||
end;
|
||||
$$;
|
||||
-- Check the invariants.
|
||||
create function verify(next_i int) returns bool language plpgsql as $$
|
||||
declare
|
||||
c integer;
|
||||
begin
|
||||
-- Perform a scan over the trailing part of the index, where the
|
||||
-- possible incomplete splits are. (We don't check the whole table,
|
||||
-- because that'd be pretty slow.)
|
||||
--
|
||||
-- Find all rows that overlap with the last 200 inserted integers. Or
|
||||
-- the next 100, which shouldn't exist.
|
||||
select count(*) into c from nbtree_incomplete_splits where i between next_i - 200 and next_i + 100;
|
||||
if c <> 200 then
|
||||
raise 'unexpected count % ', c;
|
||||
end if;
|
||||
return true;
|
||||
end;
|
||||
$$;
|
||||
-- Insert one array to get started.
|
||||
select insert_n(1, 1000) as next_i
|
||||
\gset
|
||||
select verify(:next_i);
|
||||
verify
|
||||
--------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
--
|
||||
-- Test incomplete leaf split
|
||||
--
|
||||
SELECT injection_points_attach('nbtree-leave-leaf-split-incomplete', 'error');
|
||||
injection_points_attach
|
||||
-------------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
select insert_until_fail(:next_i) as next_i
|
||||
\gset
|
||||
NOTICE: failed with: error triggered for injection point nbtree-leave-leaf-split-incomplete
|
||||
SELECT injection_points_detach('nbtree-leave-leaf-split-incomplete');
|
||||
injection_points_detach
|
||||
-------------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
-- Verify that a scan works even though there's an incomplete split
|
||||
select verify(:next_i);
|
||||
verify
|
||||
--------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- Insert some more rows, finishing the split
|
||||
select insert_n(:next_i, 10) as next_i
|
||||
\gset
|
||||
NOTICE: notice triggered for injection point nbtree-finish-incomplete-split
|
||||
-- Verify that a scan still works
|
||||
select verify(:next_i);
|
||||
verify
|
||||
--------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
--
|
||||
-- Test incomplete internal page split
|
||||
--
|
||||
SELECT injection_points_attach('nbtree-leave-internal-split-incomplete', 'error');
|
||||
injection_points_attach
|
||||
-------------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
select insert_until_fail(:next_i, 100) as next_i
|
||||
\gset
|
||||
NOTICE: failed with: error triggered for injection point nbtree-leave-internal-split-incomplete
|
||||
SELECT injection_points_detach('nbtree-leave-internal-split-incomplete');
|
||||
injection_points_detach
|
||||
-------------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
-- Verify that a scan works even though there's an incomplete split
|
||||
select verify(:next_i);
|
||||
verify
|
||||
--------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
-- Insert some more rows, finishing the split
|
||||
select insert_n(:next_i, 10) as next_i
|
||||
\gset
|
||||
NOTICE: notice triggered for injection point nbtree-finish-incomplete-split
|
||||
-- Verify that a scan still works
|
||||
select verify(:next_i);
|
||||
verify
|
||||
--------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT injection_points_detach('nbtree-finish-incomplete-split');
|
||||
injection_points_detach
|
||||
-------------------------
|
||||
|
||||
(1 row)
|
||||
|
||||
16
src/test/modules/nbtree/meson.build
Normal file
16
src/test/modules/nbtree/meson.build
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2022-2025, PostgreSQL Global Development Group
|
||||
|
||||
if not get_option('injection_points')
|
||||
subdir_done()
|
||||
endif
|
||||
|
||||
tests += {
|
||||
'name': 'nbtree',
|
||||
'sd': meson.current_source_dir(),
|
||||
'bd': meson.current_build_dir(),
|
||||
'regress': {
|
||||
'sql': [
|
||||
'nbtree_incomplete_splits',
|
||||
],
|
||||
},
|
||||
}
|
||||
134
src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql
Normal file
134
src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql
Normal file
@ -0,0 +1,134 @@
|
||||
--
|
||||
-- Test incomplete splits in B-tree indexes.
|
||||
--
|
||||
-- We use a test table with integers from 1 to :next_i. Each integer
|
||||
-- occurs exactly once, no gaps or duplicates, although the index does
|
||||
-- contain some duplicates because some of the inserting transactions
|
||||
-- are rolled back during the test. The exact contents of the table
|
||||
-- depend on the physical layout of the index, which in turn depends
|
||||
-- at least on the block size, so instead of checking the exact
|
||||
-- contents, we check those invariants. :next_i psql variable is
|
||||
-- maintained at all times to hold the last inserted integer + 1.
|
||||
--
|
||||
|
||||
-- This uses injection points to cause errors that leave some page
|
||||
-- splits in "incomplete" state
|
||||
set client_min_messages TO 'warning';
|
||||
create extension if not exists injection_points;
|
||||
reset client_min_messages;
|
||||
|
||||
-- Make all injection points local to this process, for concurrency.
|
||||
SELECT injection_points_set_local();
|
||||
|
||||
-- Use the index for all the queries
|
||||
set enable_seqscan=off;
|
||||
|
||||
-- Print a NOTICE whenever an incomplete split gets fixed
|
||||
SELECT injection_points_attach('nbtree-finish-incomplete-split', 'notice');
|
||||
|
||||
--
|
||||
-- First create the test table and some helper functions
|
||||
--
|
||||
create table nbtree_incomplete_splits(i int4) with (autovacuum_enabled = off);
|
||||
|
||||
create index nbtree_incomplete_splits_i_idx on nbtree_incomplete_splits using btree (i);
|
||||
|
||||
-- Inserts 'n' rows to the test table. Pass :next_i as the first
|
||||
-- argument, returns the new value for :next_i.
|
||||
create function insert_n(first_i int, n int) returns int language plpgsql as $$
|
||||
begin
|
||||
insert into nbtree_incomplete_splits select g from generate_series(first_i, first_i + n - 1) as g;
|
||||
return first_i + n;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Inserts to the table until an insert fails. Like insert_n(), returns the
|
||||
-- new value for :next_i.
|
||||
create function insert_until_fail(next_i int, step int default 1) returns int language plpgsql as $$
|
||||
declare
|
||||
i integer;
|
||||
begin
|
||||
-- Insert rows in batches of 'step' rows each, until an error occurs.
|
||||
i := 0;
|
||||
loop
|
||||
begin
|
||||
select insert_n(next_i, step) into next_i;
|
||||
exception when others then
|
||||
raise notice 'failed with: %', sqlerrm;
|
||||
exit;
|
||||
end;
|
||||
|
||||
-- The caller is expected to set an injection point that eventually
|
||||
-- causes an error. But bail out if still no error after 10000
|
||||
-- attempts, so that we don't get stuck in an infinite loop.
|
||||
i := i + 1;
|
||||
if i = 10000 then
|
||||
raise 'no error on inserts after % iterations', i;
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
return next_i;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Check the invariants.
|
||||
create function verify(next_i int) returns bool language plpgsql as $$
|
||||
declare
|
||||
c integer;
|
||||
begin
|
||||
-- Perform a scan over the trailing part of the index, where the
|
||||
-- possible incomplete splits are. (We don't check the whole table,
|
||||
-- because that'd be pretty slow.)
|
||||
--
|
||||
-- Find all rows that overlap with the last 200 inserted integers. Or
|
||||
-- the next 100, which shouldn't exist.
|
||||
select count(*) into c from nbtree_incomplete_splits where i between next_i - 200 and next_i + 100;
|
||||
if c <> 200 then
|
||||
raise 'unexpected count % ', c;
|
||||
end if;
|
||||
return true;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Insert one array to get started.
|
||||
select insert_n(1, 1000) as next_i
|
||||
\gset
|
||||
select verify(:next_i);
|
||||
|
||||
|
||||
--
|
||||
-- Test incomplete leaf split
|
||||
--
|
||||
SELECT injection_points_attach('nbtree-leave-leaf-split-incomplete', 'error');
|
||||
select insert_until_fail(:next_i) as next_i
|
||||
\gset
|
||||
SELECT injection_points_detach('nbtree-leave-leaf-split-incomplete');
|
||||
|
||||
-- Verify that a scan works even though there's an incomplete split
|
||||
select verify(:next_i);
|
||||
|
||||
-- Insert some more rows, finishing the split
|
||||
select insert_n(:next_i, 10) as next_i
|
||||
\gset
|
||||
-- Verify that a scan still works
|
||||
select verify(:next_i);
|
||||
|
||||
|
||||
--
|
||||
-- Test incomplete internal page split
|
||||
--
|
||||
SELECT injection_points_attach('nbtree-leave-internal-split-incomplete', 'error');
|
||||
select insert_until_fail(:next_i, 100) as next_i
|
||||
\gset
|
||||
SELECT injection_points_detach('nbtree-leave-internal-split-incomplete');
|
||||
|
||||
-- Verify that a scan works even though there's an incomplete split
|
||||
select verify(:next_i);
|
||||
|
||||
-- Insert some more rows, finishing the split
|
||||
select insert_n(:next_i, 10) as next_i
|
||||
\gset
|
||||
-- Verify that a scan still works
|
||||
select verify(:next_i);
|
||||
|
||||
SELECT injection_points_detach('nbtree-finish-incomplete-split');
|
||||
Loading…
x
Reference in New Issue
Block a user