From 9748016bc1948a31d0c85621f6b9ac74380ee5f7 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Thu, 3 Sep 2026 12:26:01 -0700
Subject: [PATCH v1] Support concrete-typed range opclasses in FOR PORTION OF

FOR PORTION OF fails if a column's default opclass uses a concrete range type
instead of anyrange (or anymultirange). This commit replaces two switch
statements on opcintype with if/else statements checking type_is_range and
type_is_multirange.

Reported-by: Noah Misch <noah@leadboat.com>
Author: Paul A. Jungwirth <pj@illuminatedcomputing.com>
Backpatch-through: 19
---
 src/backend/parser/analyze.c                 |  42 +++----
 src/test/regress/expected/for_portion_of.out | 122 +++++++++++++++++++
 src/test/regress/sql/for_portion_of.sql      | 101 +++++++++++++++
 3 files changed, 238 insertions(+), 27 deletions(-)

diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 581457c69c9..88915d45881 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1329,8 +1329,6 @@ transformForPortionOfClause(ParseState *pstate,
 	Form_pg_attribute attr;
 	Oid			attbasetype;
 	Oid			opclass;
-	Oid			opfamily;
-	Oid			opcintype;
 	Oid			funcid = InvalidOid;
 	StrategyNumber strat;
 	Oid			opid;
@@ -1523,20 +1521,13 @@ transformForPortionOfClause(ParseState *pstate,
 	 * user-defined types. An opclass support function doesn't make sense,
 	 * since there is no index involved. Perhaps a type support function.
 	 */
-	if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype))
-		switch (opcintype)
-		{
-			case ANYRANGEOID:
-				result->withoutPortionProc = F_RANGE_MINUS_MULTI;
-				break;
-			case ANYMULTIRANGEOID:
-				result->withoutPortionProc = F_MULTIRANGE_MINUS_MULTI;
-				break;
-			default:
-				elog(ERROR, "unexpected opcintype: %u", opcintype);
-		}
+	if (type_is_range(attbasetype))
+		result->withoutPortionProc = F_RANGE_MINUS_MULTI;
+	else if (type_is_multirange(attbasetype))
+		result->withoutPortionProc = F_MULTIRANGE_MINUS_MULTI;
 	else
-		elog(ERROR, "unexpected opclass: %u", opclass);
+		elog(ERROR, "unexpected type for FOR PORTION OF column: %u",
+			 attbasetype);
 
 	if (isUpdate)
 	{
@@ -1556,23 +1547,20 @@ transformForPortionOfClause(ParseState *pstate,
 		 * we can use its backing procedure for intersects in FOR PORTION OF.
 		 * XXX: Share code with FindFKPeriodOpers?
 		 */
-		switch (opcintype)
-		{
-			case ANYRANGEOID:
-				intersectoperoid = OID_RANGE_INTERSECT_RANGE_OP;
-				break;
-			case ANYMULTIRANGEOID:
-				intersectoperoid = OID_MULTIRANGE_INTERSECT_MULTIRANGE_OP;
-				break;
-			default:
-				elog(ERROR, "unexpected opcintype: %u", opcintype);
-		}
+		if (type_is_range(attbasetype))
+			intersectoperoid = OID_RANGE_INTERSECT_RANGE_OP;
+		else if (type_is_multirange(attbasetype))
+			intersectoperoid = OID_MULTIRANGE_INTERSECT_MULTIRANGE_OP;
+		else
+			elog(ERROR, "unexpected type for FOR PORTION OF column: %u",
+				 attbasetype);
+
 		funcid = get_opcode(intersectoperoid);
 		if (!OidIsValid(funcid))
 			ereport(ERROR,
 					errcode(ERRCODE_UNDEFINED_OBJECT),
 					errmsg("could not identify an intersect function for type %s",
-						   format_type_be(opcintype)));
+						   format_type_be(attbasetype)));
 
 		funcArgs = list_make2(copyObject(rangeVar),
 							  copyObject(result->targetRange));
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 64789d1777b..7131961478a 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -2793,4 +2793,126 @@ SELECT * FROM fpo_rls ORDER BY valid_at;
 
 DROP TABLE fpo_rls;
 DROP ROLE regress_fpo_rls;
+--
+-- A user-defined default GiST operator class whose opcintype is a concrete
+-- range (or multirange) type, rather than the polymorphic anyrange the
+-- built-in range_ops uses.
+--
+CREATE TYPE fpo_range AS RANGE (
+  subtype = date,
+  multirange_type_name = fpo_multirange
+);
+CREATE FUNCTION fpo_range_overlaps(fpo_range, fpo_range) RETURNS boolean
+  LANGUAGE sql IMMUTABLE STRICT
+  AS $$ SELECT $1 OPERATOR(pg_catalog.&&) $2 $$;
+CREATE OPERATOR && (
+  leftarg = fpo_range, rightarg = fpo_range,
+  procedure = fpo_range_overlaps, commutator = &&,
+  restrict = rangesel, join = areajoinsel
+);
+CREATE OPERATOR CLASS fpo_range_ops
+  DEFAULT FOR TYPE fpo_range USING gist AS
+  OPERATOR 3 &&(fpo_range, fpo_range),
+  FUNCTION 1 range_gist_consistent(internal, anyrange, smallint, oid, internal),
+  FUNCTION 2 range_gist_union(internal, internal),
+  FUNCTION 5 range_gist_penalty(internal, internal, internal),
+  FUNCTION 6 range_gist_picksplit(internal, internal),
+  FUNCTION 7 range_gist_same(anyrange, anyrange, internal);
+-- gistutil.c looks this one up with get_opfamily_proc(opfamily, ANYOID,
+-- ANYOID, GIST_TRANSLATE_CMPTYPE_PROC), so it must be registered that way.
+ALTER OPERATOR FAMILY fpo_range_ops USING gist
+  ADD FUNCTION 12 ("any", "any") gist_translate_cmptype_common(integer);
+CREATE FUNCTION fpo_multirange_overlaps(fpo_multirange, fpo_multirange)
+  RETURNS boolean LANGUAGE sql IMMUTABLE STRICT
+  AS $$ SELECT $1 OPERATOR(pg_catalog.&&) $2 $$;
+CREATE OPERATOR && (
+  leftarg = fpo_multirange, rightarg = fpo_multirange,
+  procedure = fpo_multirange_overlaps, commutator = &&,
+  restrict = multirangesel, join = areajoinsel
+);
+CREATE OPERATOR CLASS fpo_multirange_ops
+  DEFAULT FOR TYPE fpo_multirange USING gist AS
+  OPERATOR 3 &&(fpo_multirange, fpo_multirange),
+  FUNCTION 1 multirange_gist_consistent(internal, anymultirange, smallint, oid, internal),
+  FUNCTION 2 range_gist_union(internal, internal),
+  FUNCTION 3 multirange_gist_compress(internal),
+  FUNCTION 5 range_gist_penalty(internal, internal, internal),
+  FUNCTION 6 range_gist_picksplit(internal, internal),
+  FUNCTION 7 range_gist_same(anyrange, anyrange, internal);
+ALTER OPERATOR FAMILY fpo_multirange_ops USING gist
+  ADD FUNCTION 12 ("any", "any") gist_translate_cmptype_common(integer);
+CREATE TABLE fpo_opc (
+  id int,
+  valid_at fpo_range,
+  valid_ats fpo_multirange,
+  name text
+);
+-- The opclasses really are usable for indexing.
+CREATE INDEX fpo_opc_r_idx ON fpo_opc USING gist (valid_at);
+CREATE INDEX fpo_opc_mr_idx ON fpo_opc USING gist (valid_ats);
+INSERT INTO fpo_opc VALUES
+  (1, fpo_range('2000-01-01', '2010-01-01'),
+      fpo_multirange(fpo_range('2000-01-01', '2010-01-01')), 'one');
+-- control: an ordinary overlaps qual against the same opclass
+UPDATE fpo_opc SET name = 'ctl'
+  WHERE valid_at && fpo_range('2002-01-01', '2003-01-01');
+SELECT * FROM fpo_opc ORDER BY valid_at;
+ id |        valid_at         |         valid_ats         | name 
+----+-------------------------+---------------------------+------
+  1 | [2000-01-01,2010-01-01) | {[2000-01-01,2010-01-01)} | ctl
+(1 row)
+
+UPDATE fpo_opc FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET name = 'upd';
+SELECT id, valid_at, name FROM fpo_opc ORDER BY valid_at;
+ id |        valid_at         | name 
+----+-------------------------+------
+  1 | [2000-01-01,2002-01-01) | ctl
+  1 | [2002-01-01,2003-01-01) | upd
+  1 | [2003-01-01,2010-01-01) | ctl
+(3 rows)
+
+DELETE FROM fpo_opc FOR PORTION OF valid_at FROM '2005-01-01' TO '2006-01-01';
+SELECT id, valid_at, name FROM fpo_opc ORDER BY valid_at;
+ id |        valid_at         | name 
+----+-------------------------+------
+  1 | [2000-01-01,2002-01-01) | ctl
+  1 | [2002-01-01,2003-01-01) | upd
+  1 | [2003-01-01,2005-01-01) | ctl
+  1 | [2006-01-01,2010-01-01) | ctl
+(4 rows)
+
+DELETE FROM fpo_opc;
+INSERT INTO fpo_opc VALUES
+  (1, fpo_range('2000-01-01', '2010-01-01'),
+      fpo_multirange(fpo_range('2000-01-01', '2010-01-01')), 'one');
+UPDATE fpo_opc
+  FOR PORTION OF valid_ats
+    (fpo_multirange(fpo_range('2002-01-01', '2003-01-01')))
+  SET name = 'upd';
+SELECT id, valid_ats, name FROM fpo_opc ORDER BY valid_ats;
+ id |                     valid_ats                     | name 
+----+---------------------------------------------------+------
+  1 | {[2000-01-01,2002-01-01),[2003-01-01,2010-01-01)} | one
+  1 | {[2002-01-01,2003-01-01)}                         | upd
+(2 rows)
+
+DELETE FROM fpo_opc
+  FOR PORTION OF valid_ats
+    (fpo_multirange(fpo_range('2005-01-01', '2006-01-01')));
+SELECT id, valid_ats, name FROM fpo_opc ORDER BY valid_ats;
+ id |                                 valid_ats                                 | name 
+----+---------------------------------------------------------------------------+------
+  1 | {[2000-01-01,2002-01-01),[2003-01-01,2005-01-01),[2006-01-01,2010-01-01)} | one
+  1 | {[2002-01-01,2003-01-01)}                                                 | upd
+(2 rows)
+
+DROP TABLE fpo_opc;
+DROP OPERATOR CLASS fpo_multirange_ops USING gist;
+DROP OPERATOR && (fpo_multirange, fpo_multirange);
+DROP FUNCTION fpo_multirange_overlaps(fpo_multirange, fpo_multirange);
+DROP OPERATOR CLASS fpo_range_ops USING gist;
+DROP OPERATOR && (fpo_range, fpo_range);
+DROP FUNCTION fpo_range_overlaps(fpo_range, fpo_range);
+DROP TYPE fpo_range;
 RESET datestyle;
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index b61fe10478e..0969cc7ed58 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -1849,4 +1849,105 @@ SELECT * FROM fpo_rls ORDER BY valid_at;
 DROP TABLE fpo_rls;
 DROP ROLE regress_fpo_rls;
 
+--
+-- A user-defined default GiST operator class whose opcintype is a concrete
+-- range (or multirange) type, rather than the polymorphic anyrange the
+-- built-in range_ops uses.
+--
+
+CREATE TYPE fpo_range AS RANGE (
+  subtype = date,
+  multirange_type_name = fpo_multirange
+);
+
+CREATE FUNCTION fpo_range_overlaps(fpo_range, fpo_range) RETURNS boolean
+  LANGUAGE sql IMMUTABLE STRICT
+  AS $$ SELECT $1 OPERATOR(pg_catalog.&&) $2 $$;
+CREATE OPERATOR && (
+  leftarg = fpo_range, rightarg = fpo_range,
+  procedure = fpo_range_overlaps, commutator = &&,
+  restrict = rangesel, join = areajoinsel
+);
+CREATE OPERATOR CLASS fpo_range_ops
+  DEFAULT FOR TYPE fpo_range USING gist AS
+  OPERATOR 3 &&(fpo_range, fpo_range),
+  FUNCTION 1 range_gist_consistent(internal, anyrange, smallint, oid, internal),
+  FUNCTION 2 range_gist_union(internal, internal),
+  FUNCTION 5 range_gist_penalty(internal, internal, internal),
+  FUNCTION 6 range_gist_picksplit(internal, internal),
+  FUNCTION 7 range_gist_same(anyrange, anyrange, internal);
+-- gistutil.c looks this one up with get_opfamily_proc(opfamily, ANYOID,
+-- ANYOID, GIST_TRANSLATE_CMPTYPE_PROC), so it must be registered that way.
+ALTER OPERATOR FAMILY fpo_range_ops USING gist
+  ADD FUNCTION 12 ("any", "any") gist_translate_cmptype_common(integer);
+
+CREATE FUNCTION fpo_multirange_overlaps(fpo_multirange, fpo_multirange)
+  RETURNS boolean LANGUAGE sql IMMUTABLE STRICT
+  AS $$ SELECT $1 OPERATOR(pg_catalog.&&) $2 $$;
+CREATE OPERATOR && (
+  leftarg = fpo_multirange, rightarg = fpo_multirange,
+  procedure = fpo_multirange_overlaps, commutator = &&,
+  restrict = multirangesel, join = areajoinsel
+);
+CREATE OPERATOR CLASS fpo_multirange_ops
+  DEFAULT FOR TYPE fpo_multirange USING gist AS
+  OPERATOR 3 &&(fpo_multirange, fpo_multirange),
+  FUNCTION 1 multirange_gist_consistent(internal, anymultirange, smallint, oid, internal),
+  FUNCTION 2 range_gist_union(internal, internal),
+  FUNCTION 3 multirange_gist_compress(internal),
+  FUNCTION 5 range_gist_penalty(internal, internal, internal),
+  FUNCTION 6 range_gist_picksplit(internal, internal),
+  FUNCTION 7 range_gist_same(anyrange, anyrange, internal);
+ALTER OPERATOR FAMILY fpo_multirange_ops USING gist
+  ADD FUNCTION 12 ("any", "any") gist_translate_cmptype_common(integer);
+
+CREATE TABLE fpo_opc (
+  id int,
+  valid_at fpo_range,
+  valid_ats fpo_multirange,
+  name text
+);
+-- The opclasses really are usable for indexing.
+CREATE INDEX fpo_opc_r_idx ON fpo_opc USING gist (valid_at);
+CREATE INDEX fpo_opc_mr_idx ON fpo_opc USING gist (valid_ats);
+
+INSERT INTO fpo_opc VALUES
+  (1, fpo_range('2000-01-01', '2010-01-01'),
+      fpo_multirange(fpo_range('2000-01-01', '2010-01-01')), 'one');
+
+-- control: an ordinary overlaps qual against the same opclass
+UPDATE fpo_opc SET name = 'ctl'
+  WHERE valid_at && fpo_range('2002-01-01', '2003-01-01');
+SELECT * FROM fpo_opc ORDER BY valid_at;
+
+UPDATE fpo_opc FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET name = 'upd';
+SELECT id, valid_at, name FROM fpo_opc ORDER BY valid_at;
+DELETE FROM fpo_opc FOR PORTION OF valid_at FROM '2005-01-01' TO '2006-01-01';
+SELECT id, valid_at, name FROM fpo_opc ORDER BY valid_at;
+
+DELETE FROM fpo_opc;
+INSERT INTO fpo_opc VALUES
+  (1, fpo_range('2000-01-01', '2010-01-01'),
+      fpo_multirange(fpo_range('2000-01-01', '2010-01-01')), 'one');
+
+UPDATE fpo_opc
+  FOR PORTION OF valid_ats
+    (fpo_multirange(fpo_range('2002-01-01', '2003-01-01')))
+  SET name = 'upd';
+SELECT id, valid_ats, name FROM fpo_opc ORDER BY valid_ats;
+DELETE FROM fpo_opc
+  FOR PORTION OF valid_ats
+    (fpo_multirange(fpo_range('2005-01-01', '2006-01-01')));
+SELECT id, valid_ats, name FROM fpo_opc ORDER BY valid_ats;
+
+DROP TABLE fpo_opc;
+DROP OPERATOR CLASS fpo_multirange_ops USING gist;
+DROP OPERATOR && (fpo_multirange, fpo_multirange);
+DROP FUNCTION fpo_multirange_overlaps(fpo_multirange, fpo_multirange);
+DROP OPERATOR CLASS fpo_range_ops USING gist;
+DROP OPERATOR && (fpo_range, fpo_range);
+DROP FUNCTION fpo_range_overlaps(fpo_range, fpo_range);
+DROP TYPE fpo_range;
+
 RESET datestyle;
-- 
2.47.3

