From b8be3b7ce38030ea5b7729102e19a9d87d78d015 Mon Sep 17 00:00:00 2001
From: Alexandre Felipe <o.alexandre.felipe@gmail.com>
Date: Thu, 6 Aug 2026 21:54:19 +0100
Subject: [PATCH-v1 2/2] SKIP-MERGE tests

Tests commited separately for reason
other than aesthetic.
---
 src/test/regress/expected/skip_merge.out | 633 +++++++++++++++++++++++
 src/test/regress/expected/sysviews.out   |   3 +-
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/sql/skip_merge.sql      | 286 ++++++++++
 4 files changed, 922 insertions(+), 2 deletions(-)
 create mode 100644 src/test/regress/expected/skip_merge.out
 create mode 100644 src/test/regress/sql/skip_merge.sql

diff --git a/src/test/regress/expected/skip_merge.out b/src/test/regress/expected/skip_merge.out
new file mode 100644
index 00000000000..988d4bd66b6
--- /dev/null
+++ b/src/test/regress/expected/skip_merge.out
@@ -0,0 +1,633 @@
+--
+-- Index Suffix Scan (MergeAppend)
+--
+-- Equality/IN on index prefix columns is expanded into per-prefix IndexPaths
+-- merged with MergeAppend when ORDER BY starts with a later index column.
+-- A qual on the suffix column is optional.  ASC/DESC and NULLS ordering must
+-- match the index (or the opposite scan direction).
+--
+CREATE SCHEMA skip_merge;
+SET search_path TO skip_merge;
+CREATE TABLE test AS
+SELECT x, y
+FROM generate_series(1, 50) AS x,
+     generate_series(1, 50) AS y;
+CREATE INDEX test_idx ON test USING btree (x, y);
+ANALYZE test;
+CREATE TABLE t3 AS
+SELECT a, b, c
+FROM generate_series(1, 10) AS a,
+     generate_series(1, 10) AS b,
+     generate_series(1, 20) AS c;
+CREATE INDEX t3_idx ON t3 USING btree (a, b, c);
+ANALYZE t3;
+CREATE TABLE tdesc AS
+SELECT x, y
+FROM generate_series(1, 20) AS x,
+     generate_series(1, 20) AS y;
+CREATE INDEX tdesc_idx ON tdesc USING btree (x, y DESC);
+ANALYZE tdesc;
+-- Nullable suffix for NULLS FIRST / LAST tests.
+CREATE TABLE tnulls AS
+SELECT x, y
+FROM generate_series(1, 10) AS x,
+     generate_series(1, 10) AS y
+UNION ALL
+SELECT x, NULL::int
+FROM generate_series(1, 4) AS x;
+CREATE INDEX tnulls_idx ON tnulls USING btree (x, y);			-- ASC, NULLS LAST
+CREATE INDEX tnulls_nf_idx ON tnulls USING btree (x, y NULLS FIRST);
+ANALYZE tnulls;
+-- Same data as tnulls, but only a NULLS LAST index (mismatch cases).
+CREATE TABLE tnulls_nl AS SELECT * FROM tnulls;
+CREATE INDEX tnulls_nl_idx ON tnulls_nl USING btree (x, y);		-- ASC, NULLS LAST
+ANALYZE tnulls_nl;
+-- Same data as tnulls, but only a NULLS FIRST index (mismatch cases).
+CREATE TABLE tnulls_nf AS SELECT * FROM tnulls;
+CREATE INDEX tnulls_nf_only_idx ON tnulls_nf USING btree (x, y NULLS FIRST);
+ANALYZE tnulls_nf;
+-- Single-key btree index (unsupported: no suffix column).
+CREATE TABLE tsingle AS SELECT * FROM test;
+CREATE INDEX tsingle_idx ON tsingle USING btree (x);
+ANALYZE tsingle;
+-- Hash index (unsupported: no sortopfamily).
+CREATE TABLE thash AS SELECT * FROM test;
+CREATE INDEX thash_idx ON thash USING hash (x);
+ANALYZE thash;
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+SET enable_indexskipmerge = on;
+--
+-- Supported: expect Merge Append
+--
+-- Basic: Const IN + ORDER BY suffix.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = 1)
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = 3)
+(7 rows)
+
+-- = ANY(array) form.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x = ANY (ARRAY[1, 2])
+ORDER BY y LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = 1)
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = 2)
+(7 rows)
+
+--
+-- Ordered suffix with range filters
+--
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y >= 10
+ORDER BY y LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 1) AND (y >= 10))
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 3) AND (y >= 10))
+(7 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y > 10
+ORDER BY y LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 1) AND (y > 10))
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 3) AND (y > 10))
+(7 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y < 10
+ORDER BY y LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 1) AND (y < 10))
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 3) AND (y < 10))
+(7 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y <= 10
+ORDER BY y LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 1) AND (y <= 10))
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 3) AND (y <= 10))
+(7 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y BETWEEN 10 AND 30
+ORDER BY y LIMIT 3;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 1) AND (y >= 10) AND (y <= 30))
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 3) AND (y >= 10) AND (y <= 30))
+(7 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y >= 1 AND y <= 3
+ORDER BY y LIMIT 3;
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 1) AND (y >= 1) AND (y <= 3))
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x = 3) AND (y >= 1) AND (y <= 3))
+(7 rows)
+
+-- Secondary ORDER BY on prefix (MergeAppend compares full sort keys).
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y, x LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y, x
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = 1)
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = 3)
+(7 rows)
+
+-- At max_index_merge_scans limit.
+SET max_index_merge_scans = 2;
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = 1)
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = 3)
+(7 rows)
+
+RESET max_index_merge_scans;
+-- Multi-column prefix cartesian product.
+EXPLAIN (COSTS OFF)
+SELECT a, b, c FROM t3 WHERE a IN (1, 3) AND b IN (3, 4)
+ORDER BY c LIMIT 3;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: c
+         ->  Index Only Scan using t3_idx on t3
+               Index Cond: ((a = 1) AND (b = 3))
+         ->  Index Only Scan using t3_idx on t3
+               Index Cond: ((a = 1) AND (b = 4))
+         ->  Index Only Scan using t3_idx on t3
+               Index Cond: ((a = 3) AND (b = 3))
+         ->  Index Only Scan using t3_idx on t3
+               Index Cond: ((a = 3) AND (b = 4))
+(11 rows)
+
+-- Equality OpExpr mixed with IN on another prefix column.
+EXPLAIN (COSTS OFF)
+SELECT a, b, c FROM t3 WHERE a = 1 AND b IN (3, 4)
+ORDER BY c LIMIT 3;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: c
+         ->  Index Only Scan using t3_idx on t3
+               Index Cond: ((a = 1) AND (b = 3))
+         ->  Index Only Scan using t3_idx on t3
+               Index Cond: ((a = 1) AND (b = 4))
+(7 rows)
+
+-- Suffix is a middle index column.
+EXPLAIN (COSTS OFF)
+SELECT a, b, c
+FROM t3 WHERE a IN (1, 3)
+ORDER BY b LIMIT 3;
+                   QUERY PLAN                   
+------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: b
+         ->  Index Only Scan using t3_idx on t3
+               Index Cond: (a = 1)
+         ->  Index Only Scan using t3_idx on t3
+               Index Cond: (a = 3)
+(7 rows)
+
+-- ORDER BY DESC on ASC index => Backward scan children.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) ORDER BY y DESC
+LIMIT 3;
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y DESC
+         ->  Index Only Scan Backward using test_idx on test
+               Index Cond: (x = 1)
+         ->  Index Only Scan Backward using test_idx on test
+               Index Cond: (x = 3)
+(7 rows)
+
+-- DESC index + matching DESC order => Forward.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tdesc WHERE x IN (1, 3) ORDER BY y DESC
+LIMIT 3;
+                      QUERY PLAN                      
+------------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y DESC
+         ->  Index Only Scan using tdesc_idx on tdesc
+               Index Cond: (x = 1)
+         ->  Index Only Scan using tdesc_idx on tdesc
+               Index Cond: (x = 3)
+(7 rows)
+
+-- DESC index + ASC order => Backward.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tdesc WHERE x IN (1, 3) ORDER BY y ASC
+LIMIT 3;
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan Backward using tdesc_idx on tdesc
+               Index Cond: (x = 1)
+         ->  Index Only Scan Backward using tdesc_idx on tdesc
+               Index Cond: (x = 3)
+(7 rows)
+
+-- Default ASC index NULLS LAST matches ORDER BY ... NULLS LAST.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tnulls WHERE x IN (1, 3) ORDER BY y NULLS LAST
+LIMIT 3;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using tnulls_idx on tnulls
+               Index Cond: (x = 1)
+         ->  Index Only Scan using tnulls_idx on tnulls
+               Index Cond: (x = 3)
+(7 rows)
+
+-- Explicit NULLS FIRST index matches ORDER BY ... NULLS FIRST.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tnulls WHERE x IN (1, 3) ORDER BY y NULLS FIRST
+LIMIT 3;
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y NULLS FIRST
+         ->  Index Only Scan using tnulls_nf_idx on tnulls
+               Index Cond: (x = 1)
+         ->  Index Only Scan using tnulls_nf_idx on tnulls
+               Index Cond: (x = 3)
+(7 rows)
+
+-- No LIMIT (force Merge Append over SAOP+Sort).
+SET enable_sort = off;
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y;
+                  QUERY PLAN                  
+----------------------------------------------
+ Merge Append
+   Sort Key: y
+   ->  Index Only Scan using test_idx on test
+         Index Cond: (x = 1)
+   ->  Index Only Scan using test_idx on test
+         Index Cond: (x = 3)
+(6 rows)
+
+RESET enable_sort;
+-- Outer Params (LATERAL) still accepted.
+EXPLAIN (COSTS OFF)
+SELECT s.*
+FROM (VALUES (19), (20)) AS v(ymin),
+LATERAL (
+	SELECT x, y FROM test
+	WHERE x IN (1, 3) AND y >= v.ymin
+	ORDER BY y LIMIT 1
+) s;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Nested Loop
+   ->  Values Scan on "*VALUES*"
+   ->  Limit
+         ->  Merge Append
+               Sort Key: test.y
+               ->  Index Only Scan using test_idx on test
+                     Index Cond: ((x = 1) AND (y >= "*VALUES*".column1))
+               ->  Index Only Scan using test_idx on test
+                     Index Cond: ((x = 3) AND (y >= "*VALUES*".column1))
+(9 rows)
+
+-- IN-list containing NULL expands (null-equality child included).
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x = ANY (ARRAY[1, NULL])
+ORDER BY y
+LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Merge Append
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = 1)
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = NULL::integer)
+(7 rows)
+
+--
+-- Cases that do not choose Merge Append
+--
+-- Feature GUC off.
+SET enable_indexskipmerge = off;
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = ANY ('{1,3}'::integer[]))
+(5 rows)
+
+RESET enable_indexskipmerge;
+-- Indexscan GUC off.
+SET enable_indexscan = off;
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+                           QUERY PLAN                           
+----------------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: y
+         ->  Bitmap Heap Scan on test
+               Disabled: true
+               Recheck Cond: (x = ANY ('{1,3}'::integer[]))
+               ->  Bitmap Index Scan on test_idx
+                     Index Cond: (x = ANY ('{1,3}'::integer[]))
+(8 rows)
+
+RESET enable_indexscan;
+-- Fewer than two prefixes.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x = 1
+ORDER BY y LIMIT 3;
+                  QUERY PLAN                  
+----------------------------------------------
+ Limit
+   ->  Index Only Scan using test_idx on test
+         Index Cond: (x = 1)
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1)
+ORDER BY y LIMIT 3;
+                  QUERY PLAN                  
+----------------------------------------------
+ Limit
+   ->  Index Only Scan using test_idx on test
+         Index Cond: (x = 1)
+(3 rows)
+
+-- No ORDER BY / ORDER BY leading column only.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Index Only Scan using test_idx on test
+         Index Cond: (x = ANY ('{1,3}'::integer[]))
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY x LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Index Only Scan using test_idx on test
+         Index Cond: (x = ANY ('{1,3}'::integer[]))
+(3 rows)
+
+-- Prefix range only (no equality/IN).
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x >= 1 AND x <= 2
+ORDER BY y LIMIT 3;
+                     QUERY PLAN                     
+----------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: ((x >= 1) AND (x <= 2))
+(5 rows)
+
+-- Over max_index_merge_scans.
+SET max_index_merge_scans = 2;
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 2, 5)
+ORDER BY y LIMIT 3;
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = ANY ('{1,2,5}'::integer[]))
+(5 rows)
+
+-- Multi-prefix product over the GUC limit.
+SET max_index_merge_scans = 3;
+EXPLAIN (COSTS OFF)
+SELECT a, b, c FROM t3 WHERE a IN (1, 3) AND b IN (3, 4)
+ORDER BY c LIMIT 3;
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: c
+         ->  Index Only Scan using t3_idx on t3
+               Index Cond: ((a = ANY ('{1,3}'::integer[])) AND (b = ANY ('{3,4}'::integer[])))
+(5 rows)
+
+-- Gap (b) between prefix constraints and sorting suffix
+SET max_index_merge_scans = 3;
+EXPLAIN (COSTS OFF)
+SELECT a, b, c FROM t3 WHERE a IN (1, 3)
+ORDER BY c LIMIT 3;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: c
+         ->  Index Only Scan using t3_idx on t3
+               Index Cond: (a = ANY ('{1,3}'::integer[]))
+(5 rows)
+
+-- Non-Const / null / empty array SAOP.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (SELECT u FROM generate_series(1, 2) u)
+ORDER BY y LIMIT 3;
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: test.y
+         ->  Nested Loop
+               ->  HashAggregate
+                     Group Key: u.u
+                     ->  Function Scan on generate_series u
+               ->  Index Only Scan using test_idx on test
+                     Index Cond: (x = u.u)
+(9 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x = ANY (NULL::int[])
+ORDER BY y LIMIT 3;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = ANY (NULL::integer[]))
+(5 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x = ANY ('{}'::int[])
+ORDER BY y LIMIT 3;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: y
+         ->  Index Only Scan using test_idx on test
+               Index Cond: (x = ANY ('{}'::integer[]))
+(5 rows)
+
+-- Nulls order mismatch: only NULLS LAST index, query wants NULLS FIRST.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tnulls_nl WHERE x IN (1, 3) ORDER BY y NULLS FIRST
+LIMIT 3;
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: y NULLS FIRST
+         ->  Index Only Scan using tnulls_nl_idx on tnulls_nl
+               Index Cond: (x = ANY ('{1,3}'::integer[]))
+(5 rows)
+
+-- Nulls order mismatch: only NULLS FIRST index, query wants NULLS LAST.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tnulls_nf
+WHERE x IN (1, 3) ORDER BY y NULLS LAST
+LIMIT 3;
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: y
+         ->  Index Only Scan using tnulls_nf_only_idx on tnulls_nf
+               Index Cond: (x = ANY ('{1,3}'::integer[]))
+(5 rows)
+
+-- Single-key index.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tsingle
+WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: y
+         ->  Index Scan using tsingle_idx on tsingle
+               Index Cond: (x = ANY ('{1,3}'::integer[]))
+(5 rows)
+
+-- Hash index (no sortopfamily).
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM thash
+WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+                           QUERY PLAN                           
+----------------------------------------------------------------
+ Limit
+   ->  Sort
+         Sort Key: y
+         ->  Bitmap Heap Scan on thash
+               Disabled: true
+               Recheck Cond: (x = ANY ('{1,3}'::integer[]))
+               ->  Bitmap Index Scan on thash_idx
+                     Index Cond: (x = ANY ('{1,3}'::integer[]))
+(8 rows)
+
+RESET ALL;
+DROP SCHEMA skip_merge CASCADE;
+NOTICE:  drop cascades to 8 other objects
+DETAIL:  drop cascades to table skip_merge.test
+drop cascades to table skip_merge.t3
+drop cascades to table skip_merge.tdesc
+drop cascades to table skip_merge.tnulls
+drop cascades to table skip_merge.tnulls_nl
+drop cascades to table skip_merge.tnulls_nf
+drop cascades to table skip_merge.tsingle
+drop cascades to table skip_merge.thash
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 1e327c2afa4..812f386aca3 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -167,6 +167,7 @@ select name, setting from pg_settings where name like 'enable%';
  enable_incremental_sort        | on
  enable_indexonlyscan           | on
  enable_indexscan               | on
+ enable_indexskipmerge          | on
  enable_material                | on
  enable_memoize                 | on
  enable_mergejoin               | on
@@ -181,7 +182,7 @@ select name, setting from pg_settings where name like 'enable%';
  enable_seqscan                 | on
  enable_sort                    | on
  enable_tidscan                 | on
-(26 rows)
+(27 rows)
 
 -- There are always wait event descriptions for various types.  InjectionPoint
 -- may be present or absent, depending on history since last postmaster start.
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..d029351ada2 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
 # collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
 # psql depends on create_am
 # amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 skip_merge
 
 # ----------
 # Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/skip_merge.sql b/src/test/regress/sql/skip_merge.sql
new file mode 100644
index 00000000000..3ff0c53de7f
--- /dev/null
+++ b/src/test/regress/sql/skip_merge.sql
@@ -0,0 +1,286 @@
+--
+-- Index Suffix Scan (MergeAppend)
+--
+-- Equality/IN on index prefix columns is expanded into per-prefix IndexPaths
+-- merged with MergeAppend when ORDER BY starts with a later index column.
+-- A qual on the suffix column is optional.  ASC/DESC and NULLS ordering must
+-- match the index (or the opposite scan direction).
+--
+
+CREATE SCHEMA skip_merge;
+SET search_path TO skip_merge;
+
+CREATE TABLE test AS
+SELECT x, y
+FROM generate_series(1, 50) AS x,
+     generate_series(1, 50) AS y;
+CREATE INDEX test_idx ON test USING btree (x, y);
+ANALYZE test;
+
+CREATE TABLE t3 AS
+SELECT a, b, c
+FROM generate_series(1, 10) AS a,
+     generate_series(1, 10) AS b,
+     generate_series(1, 20) AS c;
+CREATE INDEX t3_idx ON t3 USING btree (a, b, c);
+ANALYZE t3;
+
+CREATE TABLE tdesc AS
+SELECT x, y
+FROM generate_series(1, 20) AS x,
+     generate_series(1, 20) AS y;
+CREATE INDEX tdesc_idx ON tdesc USING btree (x, y DESC);
+ANALYZE tdesc;
+
+-- Nullable suffix for NULLS FIRST / LAST tests.
+CREATE TABLE tnulls AS
+SELECT x, y
+FROM generate_series(1, 10) AS x,
+     generate_series(1, 10) AS y
+UNION ALL
+SELECT x, NULL::int
+FROM generate_series(1, 4) AS x;
+CREATE INDEX tnulls_idx ON tnulls USING btree (x, y);			-- ASC, NULLS LAST
+CREATE INDEX tnulls_nf_idx ON tnulls USING btree (x, y NULLS FIRST);
+ANALYZE tnulls;
+
+-- Same data as tnulls, but only a NULLS LAST index (mismatch cases).
+CREATE TABLE tnulls_nl AS SELECT * FROM tnulls;
+CREATE INDEX tnulls_nl_idx ON tnulls_nl USING btree (x, y);		-- ASC, NULLS LAST
+ANALYZE tnulls_nl;
+
+-- Same data as tnulls, but only a NULLS FIRST index (mismatch cases).
+CREATE TABLE tnulls_nf AS SELECT * FROM tnulls;
+CREATE INDEX tnulls_nf_only_idx ON tnulls_nf USING btree (x, y NULLS FIRST);
+ANALYZE tnulls_nf;
+
+-- Single-key btree index (unsupported: no suffix column).
+CREATE TABLE tsingle AS SELECT * FROM test;
+CREATE INDEX tsingle_idx ON tsingle USING btree (x);
+ANALYZE tsingle;
+
+-- Hash index (unsupported: no sortopfamily).
+CREATE TABLE thash AS SELECT * FROM test;
+CREATE INDEX thash_idx ON thash USING hash (x);
+ANALYZE thash;
+
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+SET enable_indexskipmerge = on;
+
+--
+-- Supported: expect Merge Append
+--
+
+-- Basic: Const IN + ORDER BY suffix.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+
+-- = ANY(array) form.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x = ANY (ARRAY[1, 2])
+ORDER BY y LIMIT 3;
+
+--
+-- Ordered suffix with range filters
+--
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y >= 10
+ORDER BY y LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y > 10
+ORDER BY y LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y < 10
+ORDER BY y LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y <= 10
+ORDER BY y LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y BETWEEN 10 AND 30
+ORDER BY y LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) AND y >= 1 AND y <= 3
+ORDER BY y LIMIT 3;
+
+-- Secondary ORDER BY on prefix (MergeAppend compares full sort keys).
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y, x LIMIT 3;
+
+-- At max_index_merge_scans limit.
+SET max_index_merge_scans = 2;
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+RESET max_index_merge_scans;
+
+-- Multi-column prefix cartesian product.
+EXPLAIN (COSTS OFF)
+SELECT a, b, c FROM t3 WHERE a IN (1, 3) AND b IN (3, 4)
+ORDER BY c LIMIT 3;
+
+-- Equality OpExpr mixed with IN on another prefix column.
+EXPLAIN (COSTS OFF)
+SELECT a, b, c FROM t3 WHERE a = 1 AND b IN (3, 4)
+ORDER BY c LIMIT 3;
+
+-- Suffix is a middle index column.
+EXPLAIN (COSTS OFF)
+SELECT a, b, c
+FROM t3 WHERE a IN (1, 3)
+ORDER BY b LIMIT 3;
+
+-- ORDER BY DESC on ASC index => Backward scan children.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3) ORDER BY y DESC
+LIMIT 3;
+
+-- DESC index + matching DESC order => Forward.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tdesc WHERE x IN (1, 3) ORDER BY y DESC
+LIMIT 3;
+
+-- DESC index + ASC order => Backward.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tdesc WHERE x IN (1, 3) ORDER BY y ASC
+LIMIT 3;
+
+-- Default ASC index NULLS LAST matches ORDER BY ... NULLS LAST.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tnulls WHERE x IN (1, 3) ORDER BY y NULLS LAST
+LIMIT 3;
+
+-- Explicit NULLS FIRST index matches ORDER BY ... NULLS FIRST.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tnulls WHERE x IN (1, 3) ORDER BY y NULLS FIRST
+LIMIT 3;
+
+-- No LIMIT (force Merge Append over SAOP+Sort).
+SET enable_sort = off;
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y;
+RESET enable_sort;
+
+-- Outer Params (LATERAL) still accepted.
+EXPLAIN (COSTS OFF)
+SELECT s.*
+FROM (VALUES (19), (20)) AS v(ymin),
+LATERAL (
+	SELECT x, y FROM test
+	WHERE x IN (1, 3) AND y >= v.ymin
+	ORDER BY y LIMIT 1
+) s;
+
+-- IN-list containing NULL expands (null-equality child included).
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x = ANY (ARRAY[1, NULL])
+ORDER BY y
+LIMIT 3;
+
+--
+-- Cases that do not choose Merge Append
+--
+
+-- Feature GUC off.
+SET enable_indexskipmerge = off;
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+RESET enable_indexskipmerge;
+
+-- Indexscan GUC off.
+SET enable_indexscan = off;
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+RESET enable_indexscan;
+
+-- Fewer than two prefixes.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x = 1
+ORDER BY y LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1)
+ORDER BY y LIMIT 3;
+
+-- No ORDER BY / ORDER BY leading column only.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 3)
+ORDER BY x LIMIT 3;
+
+-- Prefix range only (no equality/IN).
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x >= 1 AND x <= 2
+ORDER BY y LIMIT 3;
+
+-- Over max_index_merge_scans.
+SET max_index_merge_scans = 2;
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (1, 2, 5)
+ORDER BY y LIMIT 3;
+
+-- Multi-prefix product over the GUC limit.
+SET max_index_merge_scans = 3;
+EXPLAIN (COSTS OFF)
+SELECT a, b, c FROM t3 WHERE a IN (1, 3) AND b IN (3, 4)
+ORDER BY c LIMIT 3;
+
+-- Gap (b) between prefix constraints and sorting suffix
+SET max_index_merge_scans = 3;
+EXPLAIN (COSTS OFF)
+SELECT a, b, c FROM t3 WHERE a IN (1, 3)
+ORDER BY c LIMIT 3;
+
+
+-- Non-Const / null / empty array SAOP.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x IN (SELECT u FROM generate_series(1, 2) u)
+ORDER BY y LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x = ANY (NULL::int[])
+ORDER BY y LIMIT 3;
+
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM test WHERE x = ANY ('{}'::int[])
+ORDER BY y LIMIT 3;
+
+-- Nulls order mismatch: only NULLS LAST index, query wants NULLS FIRST.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tnulls_nl WHERE x IN (1, 3) ORDER BY y NULLS FIRST
+LIMIT 3;
+
+-- Nulls order mismatch: only NULLS FIRST index, query wants NULLS LAST.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tnulls_nf
+WHERE x IN (1, 3) ORDER BY y NULLS LAST
+LIMIT 3;
+
+-- Single-key index.
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM tsingle
+WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+
+-- Hash index (no sortopfamily).
+EXPLAIN (COSTS OFF)
+SELECT x, y FROM thash
+WHERE x IN (1, 3)
+ORDER BY y LIMIT 3;
+
+RESET ALL;
+DROP SCHEMA skip_merge CASCADE;
-- 
2.53.0

