Hi Hackers, Here is a fix for finding D5 from [0].
The issue is that we look up helper functions based on the application-time column's opclass's opcintype but only accept anyrange and anymultirange. The fix is to call type_is_range and type_is_multirange instead. [0] https://www.postgresql.org/message-id/CA%2BrenyV6QLOJYmLo3gbsg1Y%2BCrho8NqME1jJXgPbO_NgxfBaKQ%40mail.gmail.com Yours, -- Paul ~{:-) [email protected]
From 9748016bc1948a31d0c85621f6b9ac74380ee5f7 Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" <[email protected]> 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 <[email protected]> Author: Paul A. Jungwirth <[email protected]> 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
