summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/backend/access/nbtree/nbtinsert.c9
-rw-r--r--src/test/modules/meson.build1
-rw-r--r--src/test/modules/nbtree/Makefile28
-rw-r--r--src/test/modules/nbtree/expected/nbtree_incomplete_splits.out179
-rw-r--r--src/test/modules/nbtree/meson.build16
-rw-r--r--src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql134
6 files changed, 367 insertions, 0 deletions
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index 7c113c007e5..3a4b791f2ab 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -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));
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index f5114469b92..cc57461e59a 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -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')
diff --git a/src/test/modules/nbtree/Makefile b/src/test/modules/nbtree/Makefile
new file mode 100644
index 00000000000..34946a84445
--- /dev/null
+++ b/src/test/modules/nbtree/Makefile
@@ -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
diff --git a/src/test/modules/nbtree/expected/nbtree_incomplete_splits.out b/src/test/modules/nbtree/expected/nbtree_incomplete_splits.out
new file mode 100644
index 00000000000..88e87e875c8
--- /dev/null
+++ b/src/test/modules/nbtree/expected/nbtree_incomplete_splits.out
@@ -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)
+
diff --git a/src/test/modules/nbtree/meson.build b/src/test/modules/nbtree/meson.build
new file mode 100644
index 00000000000..efebf30a16f
--- /dev/null
+++ b/src/test/modules/nbtree/meson.build
@@ -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',
+ ],
+ },
+}
diff --git a/src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql b/src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql
new file mode 100644
index 00000000000..0609ed7464e
--- /dev/null
+++ b/src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql
@@ -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');