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
This commit is contained in:
Alexander Korotkov 2021-06-15 15:59:20 +03:00
parent 4daa140a2f
commit 29854ee8d1
11 changed files with 354 additions and 9 deletions

View File

@ -19181,6 +19181,29 @@ SELECT NULLIF(value, '(none)') ...
<returnvalue>{[1,2)}</returnvalue>
</para></entry>
</row>
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
<primary>unnest</primary>
<secondary>for multirange</secondary>
</indexterm>
<function>unnest</function> ( <type>anymultirange</type> )
<returnvalue>setof anyrange</returnvalue>
</para>
<para>
Expands a multirange into a set of ranges.
The ranges are read out in storage order (ascending).
</para>
<para>
<literal>unnest('{[1,2), [3,4)}'::int4multirange)</literal>
<returnvalue></returnvalue>
<programlisting>
[1,2)
[3,4)
</programlisting>
</para></entry>
</row>
</tbody>
</tgroup>
</table>

View File

@ -266,6 +266,18 @@ SELECT '[4,4)'::int4range;
SELECT '{}'::int4multirange;
SELECT '{[3,7)}'::int4multirange;
SELECT '{[3,7), [8,9)}'::int4multirange;
</programlisting>
</para>
<para>
A multirange can be cast to an array of ranges of the same type.
</para>
<para>
Examples:
<programlisting>
SELECT '{[3,7), [8,9)}'::int4multirange::int4range[];
SELECT '{[1.0,14.0), [20.0,25.0)}'::nummultirange::numrange[];
</programlisting>
</para>

View File

@ -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.
*

View File

@ -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 */

View File

@ -53,6 +53,6 @@
*/
/* yyyymmddN */
#define CATALOG_VERSION_NO 202106151
#define CATALOG_VERSION_NO 202106152
#endif

View File

@ -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' }
]

View File

@ -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',

View File

@ -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?
----------

View File

@ -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

View File

@ -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;

View File

@ -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;