From 29854ee8d1ca4a46adb7e84deb17e6fb18e531cc Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Tue, 15 Jun 2021 15:59:20 +0300 Subject: [PATCH] Support for unnest(multirange) and cast multirange as an array of ranges It has been spotted that multiranges lack of ability to decompose them into individual ranges. Subscription and proper expanded object representation require substantial work, and it's too late for v14. This commit provides the implementation of unnest(multirange) and cast multirange as an array of ranges, which is quite trivial. unnest(multirange) is defined as a polymorphic procedure. The catalog description of the cast underlying procedure is duplicated for each multirange type because we don't have anyrangearray polymorphic type to use here. Catversion is bumped. Reported-by: Jonathan S. Katz Discussion: https://postgr.es/m/flat/60258efe-bd7e-4886-82e1-196e0cac5433%40postgresql.org Author: Alexander Korotkov Reviewed-by: Justin Pryzby, Jonathan S. Katz, Zhihong Yu --- doc/src/sgml/func.sgml | 23 ++++ doc/src/sgml/rangetypes.sgml | 12 ++ src/backend/commands/typecmds.c | 92 +++++++++++++-- src/backend/utils/adt/multirangetypes.c | 106 ++++++++++++++++++ src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_cast.dat | 20 ++++ src/include/catalog/pg_proc.dat | 23 ++++ src/test/regress/expected/multirangetypes.out | 60 ++++++++++ src/test/regress/expected/opr_sanity.out | 6 + src/test/regress/sql/multirangetypes.sql | 13 +++ src/test/regress/sql/opr_sanity.sql | 6 + 11 files changed, 354 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 764c3ad307..a2d810ef40 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -19181,6 +19181,29 @@ SELECT NULLIF(value, '(none)') ... {[1,2)} + + + + + unnest + for multirange + + unnest ( anymultirange ) + setof anyrange + + + Expands a multirange into a set of ranges. + The ranges are read out in storage order (ascending). + + + unnest('{[1,2), [3,4)}'::int4multirange) + + + [1,2) + [3,4) + + + diff --git a/doc/src/sgml/rangetypes.sgml b/doc/src/sgml/rangetypes.sgml index 92ea0e83da..9ec5510e23 100644 --- a/doc/src/sgml/rangetypes.sgml +++ b/doc/src/sgml/rangetypes.sgml @@ -266,6 +266,18 @@ SELECT '[4,4)'::int4range; SELECT '{}'::int4multirange; SELECT '{[3,7)}'::int4multirange; SELECT '{[3,7), [8,9)}'::int4multirange; + + + + + A multirange can be cast to an array of ranges of the same type. + + + + Examples: + +SELECT '{[3,7), [8,9)}'::int4multirange::int4range[]; +SELECT '{[1.0,14.0), [20.0,25.0)}'::nummultirange::numrange[]; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 58ec65c6af..9ea0ce17e1 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -114,7 +114,11 @@ static void makeRangeConstructors(const char *name, Oid namespace, Oid rangeOid, Oid subtype); static void makeMultirangeConstructors(const char *name, Oid namespace, Oid multirangeOid, Oid rangeOid, - Oid rangeArrayOid, Oid *castFuncOid); + Oid rangeArrayOid, + Oid *oneArgContructorOid); +static void makeMultirangeCasts(const char *name, Oid namespace, + Oid multirangeOid, Oid rangeOid, + Oid rangeArrayOid, Oid singleArgContructorOid); static Oid findTypeInputFunction(List *procname, Oid typeOid); static Oid findTypeOutputFunction(List *procname, Oid typeOid); static Oid findTypeReceiveFunction(List *procname, Oid typeOid); @@ -1365,7 +1369,7 @@ DefineRange(CreateRangeStmt *stmt) ListCell *lc; ObjectAddress address; ObjectAddress mltrngaddress PG_USED_FOR_ASSERTS_ONLY; - Oid castFuncOid; + Oid singleArgContructorOid; /* Convert list of names to a name and namespace */ typeNamespace = QualifiedNameGetCreationNamespace(stmt->typeName, @@ -1717,10 +1721,12 @@ DefineRange(CreateRangeStmt *stmt) makeRangeConstructors(typeName, typeNamespace, typoid, rangeSubtype); makeMultirangeConstructors(multirangeTypeName, typeNamespace, multirangeOid, typoid, rangeArrayOid, - &castFuncOid); + &singleArgContructorOid); - /* Create cast from the range type to its multirange type */ - CastCreate(typoid, multirangeOid, castFuncOid, 'e', 'f', DEPENDENCY_INTERNAL); + /* Create casts for this multirange type */ + makeMultirangeCasts(multirangeTypeName, typeNamespace, + multirangeOid, typoid, rangeArrayOid, + singleArgContructorOid); pfree(multirangeTypeName); pfree(multirangeArrayName); @@ -1808,13 +1814,13 @@ makeRangeConstructors(const char *name, Oid namespace, * If we had an anyrangearray polymorphic type we could use it here, * but since each type has its own constructor name there's no need. * - * Sets castFuncOid to the oid of the new constructor that can be used + * Sets oneArgContructorOid to the oid of the new constructor that can be used * to cast from a range to a multirange. */ static void makeMultirangeConstructors(const char *name, Oid namespace, Oid multirangeOid, Oid rangeOid, Oid rangeArrayOid, - Oid *castFuncOid) + Oid *oneArgContructorOid) { ObjectAddress myself, referenced; @@ -1904,7 +1910,7 @@ makeMultirangeConstructors(const char *name, Oid namespace, /* ditto */ recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); pfree(argtypes); - *castFuncOid = myself.objectId; + *oneArgContructorOid = myself.objectId; /* n-arg constructor - vararg */ argtypes = buildoidvector(&rangeArrayOid, 1); @@ -1949,6 +1955,76 @@ makeMultirangeConstructors(const char *name, Oid namespace, pfree(parameterModes); } +/* + * Create casts for the multirange type. The first cast makes multirange from + * range, and it's based on the single-argument constructor. The second cast + * makes an array of ranges from multirange. + */ +static void +makeMultirangeCasts(const char *name, Oid namespace, + Oid multirangeOid, Oid rangeOid, Oid rangeArrayOid, + Oid singleArgContructorOid) +{ + ObjectAddress myself, + referenced; + oidvector *argtypes; + + /* + * Create cast from range to multirange using the existing single-argument + * constructor procedure. + */ + CastCreate(rangeOid, multirangeOid, singleArgContructorOid, 'e', 'f', + DEPENDENCY_INTERNAL); + + referenced.classId = TypeRelationId; + referenced.objectId = multirangeOid; + referenced.objectSubId = 0; + + /* multirange_to_array() function */ + argtypes = buildoidvector(&multirangeOid, 1); + myself = ProcedureCreate("multirange_to_array", /* name */ + namespace, + false, /* replace */ + false, /* returns set */ + rangeArrayOid, /* return type */ + BOOTSTRAP_SUPERUSERID, /* proowner */ + INTERNALlanguageId, /* language */ + F_FMGR_INTERNAL_VALIDATOR, + "multirange_to_array", /* prosrc */ + NULL, /* probin */ + NULL, /* prosqlbody */ + PROKIND_FUNCTION, + false, /* security_definer */ + false, /* leakproof */ + true, /* isStrict */ + PROVOLATILE_IMMUTABLE, /* volatility */ + PROPARALLEL_SAFE, /* parallel safety */ + argtypes, /* parameterTypes */ + PointerGetDatum(NULL), /* allParameterTypes */ + PointerGetDatum(NULL), /* parameterModes */ + PointerGetDatum(NULL), /* parameterNames */ + NIL, /* parameterDefaults */ + PointerGetDatum(NULL), /* trftypes */ + PointerGetDatum(NULL), /* proconfig */ + InvalidOid, /* prosupport */ + 1.0, /* procost */ + 0.0); /* prorows */ + + /* + * Make the multirange_to_array() function internally-dependent on the + * multirange type so that they go away silently when the type is dropped. + */ + recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); + pfree(argtypes); + + /* + * Create cast from multirange to the array of ranges using + * multirange_to_array() function. + */ + CastCreate(multirangeOid, rangeArrayOid, myself.objectId, 'e', 'f', + DEPENDENCY_INTERNAL); +} + /* * Find suitable I/O and other support functions for a type. * diff --git a/src/backend/utils/adt/multirangetypes.c b/src/backend/utils/adt/multirangetypes.c index fbcc27d072..fbbda4af36 100644 --- a/src/backend/utils/adt/multirangetypes.c +++ b/src/backend/utils/adt/multirangetypes.c @@ -34,6 +34,7 @@ #include "access/tupmacs.h" #include "common/hashfn.h" +#include "funcapi.h" #include "lib/stringinfo.h" #include "libpq/pqformat.h" #include "miscadmin.h" @@ -1068,6 +1069,39 @@ multirange_constructor0(PG_FUNCTION_ARGS) PG_RETURN_MULTIRANGE_P(make_multirange(mltrngtypid, rangetyp, 0, NULL)); } +/* + * Cast multirange to an array of ranges. + */ +Datum +multirange_to_array(PG_FUNCTION_ARGS) +{ + ArrayBuildState *astate = NULL; + MultirangeType *mr = PG_GETARG_MULTIRANGE_P(0); + TypeCacheEntry *typcache; + int i; + + typcache = multirange_get_typcache(fcinfo, MultirangeTypeGetOid(mr)); + + astate = initArrayResult(typcache->rngtype->type_id, + CurrentMemoryContext, + false); + + for (i = 0; i < mr->rangeCount; i++) + { + RangeType *r; + + r = multirange_get_range(typcache->rngtype, mr, i); + astate = accumArrayResult(astate, + RangeTypePGetDatum(r), + false, + typcache->rngtype->type_id, + CurrentMemoryContext); + } + + PG_RETURN_ARRAYTYPE_P(makeArrayResult(astate, CurrentMemoryContext)); +} + + /* multirange, multirange -> multirange type functions */ @@ -2645,6 +2679,78 @@ range_merge_from_multirange(PG_FUNCTION_ARGS) PG_RETURN_RANGE_P(result); } +/* Turn multirange into a set of ranges */ +Datum +multirange_unnest(PG_FUNCTION_ARGS) +{ + typedef struct + { + MultirangeType *mr; + TypeCacheEntry *typcache; + int index; + } multirange_unnest_fctx; + + FuncCallContext *funcctx; + multirange_unnest_fctx *fctx; + MemoryContext oldcontext; + + /* stuff done only on the first call of the function */ + if (SRF_IS_FIRSTCALL()) + { + MultirangeType *mr; + + /* create a function context for cross-call persistence */ + funcctx = SRF_FIRSTCALL_INIT(); + + /* + * switch to memory context appropriate for multiple function calls + */ + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + /* + * Get the multirange value and detoast if needed. We can't do this + * earlier because if we have to detoast, we want the detoasted copy + * to be in multi_call_memory_ctx, so it will go away when we're done + * and not before. (If no detoast happens, we assume the originally + * passed multirange will stick around till then.) + */ + mr = PG_GETARG_MULTIRANGE_P(0); + + /* allocate memory for user context */ + fctx = (multirange_unnest_fctx *) palloc(sizeof(multirange_unnest_fctx)); + + /* initialize state */ + fctx->mr = mr; + fctx->index = 0; + fctx->typcache = lookup_type_cache(MultirangeTypeGetOid(mr), + TYPECACHE_MULTIRANGE_INFO); + + funcctx->user_fctx = fctx; + MemoryContextSwitchTo(oldcontext); + } + + /* stuff done on every call of the function */ + funcctx = SRF_PERCALL_SETUP(); + fctx = funcctx->user_fctx; + + if (fctx->index < fctx->mr->rangeCount) + { + RangeType *range; + + range = multirange_get_range(fctx->typcache->rngtype, + fctx->mr, + fctx->index); + fctx->index++; + + SRF_RETURN_NEXT(funcctx, RangeTypePGetDatum(range)); + } + else + { + /* do when there is no more left */ + SRF_RETURN_DONE(funcctx); + } +} + /* Hash support */ /* hash a multirange value */ diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 1b23c7c253..7f483936db 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202106151 +#define CATALOG_VERSION_NO 202106152 #endif diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat index 67f73cb6fb..d0b37edb60 100644 --- a/src/include/catalog/pg_cast.dat +++ b/src/include/catalog/pg_cast.dat @@ -548,4 +548,24 @@ { castsource => 'tstzrange', casttarget => 'tstzmultirange', castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e', castmethod => 'f' }, + +# multirange to array +{ castsource => 'int4multirange', casttarget => '_int4range', + castfunc => 'multirange_to_array(int4multirange)', castcontext => 'e', + castmethod => 'f' } +{ castsource => 'int8multirange', casttarget => '_int8range', + castfunc => 'multirange_to_array(int8multirange)', castcontext => 'e', + castmethod => 'f' } +{ castsource => 'nummultirange', casttarget => '_numrange', + castfunc => 'multirange_to_array(nummultirange)', castcontext => 'e', + castmethod => 'f' } +{ castsource => 'datemultirange', casttarget => '_daterange', + castfunc => 'multirange_to_array(datemultirange)', castcontext => 'e', + castmethod => 'f' } +{ castsource => 'tsmultirange', casttarget => '_tsrange', + castfunc => 'multirange_to_array(tsmultirange)', castcontext => 'e', + castmethod => 'f' } +{ castsource => 'tstzmultirange', casttarget => '_tstzrange', + castfunc => 'multirange_to_array(tstzmultirange)', castcontext => 'e', + castmethod => 'f' } ] diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fde251fa4f..5af721ee3d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -10537,6 +10537,29 @@ proname => 'range_intersect_agg', prokind => 'a', proisstrict => 'f', prorettype => 'anymultirange', proargtypes => 'anymultirange', prosrc => 'aggregate_dummy' }, +{ oid => '1293', descr => 'expand multirange to set of ranges', + proname => 'unnest', prorows => '100', + proretset => 't', prorettype => 'anyrange', proargtypes => 'anymultirange', + prosrc => 'multirange_unnest' }, + +{ oid => '4544', descr => 'convert multirange to array of ranges', + proname => 'multirange_to_array', prorettype => '_int4range', + proargtypes => 'int4multirange', prosrc => 'multirange_to_array' }, +{ oid => '4545', descr => 'convert multirange to array of ranges', + proname => 'multirange_to_array', prorettype => '_int8range', + proargtypes => 'int8multirange', prosrc => 'multirange_to_array' }, +{ oid => '4546', descr => 'convert multirange to array of ranges', + proname => 'multirange_to_array', prorettype => '_numrange', + proargtypes => 'nummultirange', prosrc => 'multirange_to_array' }, +{ oid => '4547', descr => 'convert multirange to array of ranges', + proname => 'multirange_to_array', prorettype => '_daterange', + proargtypes => 'datemultirange', prosrc => 'multirange_to_array' }, +{ oid => '4548', descr => 'convert multirange to array of ranges', + proname => 'multirange_to_array', prorettype => '_tsrange', + proargtypes => 'tsmultirange', prosrc => 'multirange_to_array' }, +{ oid => '4549', descr => 'convert multirange to array of ranges', + proname => 'multirange_to_array', prorettype => '_tstzrange', + proargtypes => 'tstzmultirange', prosrc => 'multirange_to_array' }, # date, time, timestamp constructors { oid => '3846', descr => 'construct date', diff --git a/src/test/regress/expected/multirangetypes.out b/src/test/regress/expected/multirangetypes.out index 98ac592127..7ca2d3f30a 100644 --- a/src/test/regress/expected/multirangetypes.out +++ b/src/test/regress/expected/multirangetypes.out @@ -322,6 +322,18 @@ select int4range(null, null)::int4multirange; {(,)} (1 row) +select 'empty'::int4range::int4multirange::int4range[]; + int4range +----------- + {} +(1 row) + +select int4multirange(int4range('5', '6'), int4range('1', '2'))::int4range[]; + int4multirange +------------------- + {"[1,2)","[5,6)"} +(1 row) + select 'empty'::textrange::textmultirange; textmultirange ---------------- @@ -346,6 +358,35 @@ select textrange(null, null)::textmultirange; {(,)} (1 row) +select textmultirange(textrange('a', 'b'), textrange('d', 'e'))::textrange[]; + textmultirange +------------------- + {"[a,b)","[d,e)"} +(1 row) + +select 'empty'::textrange::textmultirange::textrange[]; + textrange +----------- + {} +(1 row) + +-- +-- test unnest(multirange) function +-- +select unnest(int4multirange(int4range('5', '6'), int4range('1', '2'))); + unnest +-------- + [1,2) + [5,6) +(2 rows) + +select unnest(textmultirange(textrange('a', 'b'), textrange('d', 'e'))); + unnest +-------- + [a,b) + [d,e) +(2 rows) + -- -- create some test data and test the operators -- @@ -2728,6 +2769,25 @@ LINE 1: select multirange_of_text(textrange2('a','Z')); HINT: No function matches the given name and argument types. You might need to add explicit type casts. select multirange_of_text(textrange1('a','Z')) @> 'b'::text; ERROR: range lower bound must be less than or equal to range upper bound +select multirange_of_text(textrange1('a','b'), textrange1('d','e'))::textrange1[]; + multirange_of_text +-------------------- + {"[a,b)","[d,e)"} +(1 row) + +select multirange_of_text()::textrange1[]; + multirange_of_text +-------------------- + {} +(1 row) + +select unnest(multirange_of_text(textrange1('a','b'), textrange1('d','e'))); + unnest +-------- + [a,b) + [d,e) +(2 rows) + select _textrange1(textrange2('a','z')) @> 'b'::text; ?column? ---------- diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out index 562b586d8e..fbcd193409 100644 --- a/src/test/regress/expected/opr_sanity.out +++ b/src/test/regress/expected/opr_sanity.out @@ -151,6 +151,8 @@ WHERE p1.oid != p2.oid AND p2.prosrc NOT LIKE E'range\\_constructor_' AND p1.prosrc NOT LIKE E'multirange\\_constructor_' AND p2.prosrc NOT LIKE E'multirange\\_constructor_' AND + p1.prosrc != 'multirange_to_array' AND + p2.prosrc != 'multirange_to_array' AND (p1.prorettype < p2.prorettype) ORDER BY 1, 2; prorettype | prorettype @@ -171,6 +173,8 @@ WHERE p1.oid != p2.oid AND p2.prosrc NOT LIKE E'range\\_constructor_' AND p1.prosrc NOT LIKE E'multirange\\_constructor_' AND p2.prosrc NOT LIKE E'multirange\\_constructor_' AND + p1.prosrc != 'multirange_to_array' AND + p2.prosrc != 'multirange_to_array' AND (p1.proargtypes[0] < p2.proargtypes[0]) ORDER BY 1, 2; proargtypes | proargtypes @@ -193,6 +197,8 @@ WHERE p1.oid != p2.oid AND p2.prosrc NOT LIKE E'range\\_constructor_' AND p1.prosrc NOT LIKE E'multirange\\_constructor_' AND p2.prosrc NOT LIKE E'multirange\\_constructor_' AND + p1.prosrc != 'multirange_to_array' AND + p2.prosrc != 'multirange_to_array' AND (p1.proargtypes[1] < p2.proargtypes[1]) ORDER BY 1, 2; proargtypes | proargtypes diff --git a/src/test/regress/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql index 3cbebedcd4..d636e8ca34 100644 --- a/src/test/regress/sql/multirangetypes.sql +++ b/src/test/regress/sql/multirangetypes.sql @@ -72,10 +72,20 @@ select 'empty'::int4range::int4multirange; select int4range(1, 3)::int4multirange; select int4range(1, null)::int4multirange; select int4range(null, null)::int4multirange; +select 'empty'::int4range::int4multirange::int4range[]; +select int4multirange(int4range('5', '6'), int4range('1', '2'))::int4range[]; select 'empty'::textrange::textmultirange; select textrange('a', 'c')::textmultirange; select textrange('a', null)::textmultirange; select textrange(null, null)::textmultirange; +select textmultirange(textrange('a', 'b'), textrange('d', 'e'))::textrange[]; +select 'empty'::textrange::textmultirange::textrange[]; + +-- +-- test unnest(multirange) function +-- +select unnest(int4multirange(int4range('5', '6'), int4range('1', '2'))); +select unnest(textmultirange(textrange('a', 'b'), textrange('d', 'e'))); -- -- create some test data and test the operators @@ -621,6 +631,9 @@ create type textrange2 as range(subtype=text, multirange_type_name=_textrange1, select multirange_of_text(textrange2('a','Z')); -- should fail select multirange_of_text(textrange1('a','Z')) @> 'b'::text; +select multirange_of_text(textrange1('a','b'), textrange1('d','e'))::textrange1[]; +select multirange_of_text()::textrange1[]; +select unnest(multirange_of_text(textrange1('a','b'), textrange1('d','e'))); select _textrange1(textrange2('a','z')) @> 'b'::text; drop type textrange1; diff --git a/src/test/regress/sql/opr_sanity.sql b/src/test/regress/sql/opr_sanity.sql index 5a9c479692..19497625f9 100644 --- a/src/test/regress/sql/opr_sanity.sql +++ b/src/test/regress/sql/opr_sanity.sql @@ -127,6 +127,8 @@ WHERE p1.oid != p2.oid AND p2.prosrc NOT LIKE E'range\\_constructor_' AND p1.prosrc NOT LIKE E'multirange\\_constructor_' AND p2.prosrc NOT LIKE E'multirange\\_constructor_' AND + p1.prosrc != 'multirange_to_array' AND + p2.prosrc != 'multirange_to_array' AND (p1.prorettype < p2.prorettype) ORDER BY 1, 2; @@ -140,6 +142,8 @@ WHERE p1.oid != p2.oid AND p2.prosrc NOT LIKE E'range\\_constructor_' AND p1.prosrc NOT LIKE E'multirange\\_constructor_' AND p2.prosrc NOT LIKE E'multirange\\_constructor_' AND + p1.prosrc != 'multirange_to_array' AND + p2.prosrc != 'multirange_to_array' AND (p1.proargtypes[0] < p2.proargtypes[0]) ORDER BY 1, 2; @@ -153,6 +157,8 @@ WHERE p1.oid != p2.oid AND p2.prosrc NOT LIKE E'range\\_constructor_' AND p1.prosrc NOT LIKE E'multirange\\_constructor_' AND p2.prosrc NOT LIKE E'multirange\\_constructor_' AND + p1.prosrc != 'multirange_to_array' AND + p2.prosrc != 'multirange_to_array' AND (p1.proargtypes[1] < p2.proargtypes[1]) ORDER BY 1, 2;