commit b27295b9140bd02963e5108f296bcd8ba50f12b3
Author: Alexander Korotkov <akorotkov@postgresql.org>
Date:   Tue Jun 15 15:59:20 2021 +0300

    Support for unnest(multirange)
    
    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), which is quite trivial.
    unnest(multirange) is defined as a polymorphic procedure.
    
    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, Tom Lane

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 6388385edc5..91b7674cbb3 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -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>
diff --git a/src/backend/utils/adt/multirangetypes.c b/src/backend/utils/adt/multirangetypes.c
index fbcc27d0726..4a445610aec 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"
@@ -1069,6 +1070,7 @@ multirange_constructor0(PG_FUNCTION_ARGS)
 }
 
 
+
 /* multirange, multirange -> multirange type functions */
 
 /* multirange union */
@@ -2645,6 +2647,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/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fde251fa4f3..79669bf5a2e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10537,6 +10537,10 @@
   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' },
 
 # 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 98ac592127b..5e94d163c07 100644
--- a/src/test/regress/expected/multirangetypes.out
+++ b/src/test/regress/expected/multirangetypes.out
@@ -346,6 +346,23 @@ select textrange(null, null)::textmultirange;
  {(,)}
 (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 +2745,13 @@ 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 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/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql
index 3cbebedcd4a..ca0b7387780 100644
--- a/src/test/regress/sql/multirangetypes.sql
+++ b/src/test/regress/sql/multirangetypes.sql
@@ -77,6 +77,12 @@ select textrange('a', 'c')::textmultirange;
 select textrange('a', null)::textmultirange;
 select textrange(null, null)::textmultirange;
 
+--
+-- 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 +627,7 @@ 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 unnest(multirange_of_text(textrange1('a','b'), textrange1('d','e')));
 select _textrange1(textrange2('a','z')) @> 'b'::text;
 
 drop type textrange1;
