diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index e918133874..768d7fb0c0 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -14514,6 +14514,11 @@ NULL baz</literallayout>(3 rows)</entry>
    <function>lower_inf</function>, and <function>upper_inf</function>
    functions all return false for an empty range.
   </para>
+
+  <para>
+   See also <xref linkend="functions-aggregate"/> about the aggregate
+   function <function>range_agg</function> for use with ranges.
+  </para>
   </sect1>
 
  <sect1 id="functions-aggregate">
@@ -14832,6 +14837,76 @@ NULL baz</literallayout>(3 rows)</entry>
      <row>
       <entry>
        <indexterm>
+        <primary>range_agg</primary>
+       </indexterm>
+       <function>
+         range_agg(<replaceable class="parameter">expression</replaceable>)
+       </function>
+      </entry>
+      <entry>
+       any range type
+      </entry>
+      <entry>
+       same as argument type
+      </entry>
+      <entry>No</entry>
+      <entry>input ranges merged into a single range, or an error if there are any gaps or overlaps</entry>
+     </row>
+
+     <row>
+      <entry>
+       <indexterm>
+        <primary>range_agg</primary>
+       </indexterm>
+       <function>
+         range_agg(<replaceable class="parameter">expression</replaceable>,
+          <replaceable>permit_gaps</replaceable>)
+       </function>
+      </entry>
+      <entry>
+       (any range type, <type>boolean</type>)
+      </entry>
+      <entry>
+       array of the same range type as argument type
+      </entry>
+      <entry>No</entry>
+      <entry>
+       input ranges merged into a list of non-touching ranges,
+       with gaps permitted, rejected, or yielding a NULL result
+       as the second argument is true, false, or NULL respectively
+      </entry>
+     </row>
+
+     <row>
+      <entry>
+       <indexterm>
+        <primary>range_agg</primary>
+       </indexterm>
+       <function>
+         range_agg(<replaceable class="parameter">expression</replaceable>,
+          <replaceable>permit_gaps</replaceable>,
+          <replaceable>permit_overlaps</replaceable>)
+       </function>
+      </entry>
+      <entry>
+       (any range type, <type>boolean</type>, <type>boolean</type>)
+      </entry>
+      <entry>
+       array of the same range type as argument type
+      </entry>
+      <entry>No</entry>
+      <entry>
+       input ranges merged into a list of non-touching ranges,
+       with gaps permitted, rejected, or yielding a NULL result
+       as the second argument is true, false, or NULL respectively,
+       and overlaps permitted, rejected, or yielding a NULL result
+       as the third argument is true, false, or NULL respectively
+      </entry>
+     </row>
+
+     <row>
+      <entry>
+       <indexterm>
         <primary>string_agg</primary>
        </indexterm>
        <function>
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index cb16d701d8..bfa46bd389 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -34,6 +34,7 @@
 #include "lib/stringinfo.h"
 #include "libpq/pqformat.h"
 #include "miscadmin.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/date.h"
 #include "utils/hashutils.h"
@@ -54,6 +55,18 @@ typedef struct RangeIOData
 	FmgrInfo	proc;			/* lookup result for typiofunc */
 } RangeIOData;
 
+/* aggregate state for range_agg */
+typedef struct RangeAggState
+{
+	ArrayBuildState *inputs;
+	uint8 flags;
+} RangeAggState;
+#define RANGE_AGG_GAPS_RAISE		0x01
+#define RANGE_AGG_GAPS_SET_NULL		0x02
+#define RANGE_AGG_OVERLAPS_RAISE	0x04
+#define RANGE_AGG_OVERLAPS_SET_NULL	0x08
+#define RANGE_AGG_GAPS_FLAGS		(RANGE_AGG_GAPS_RAISE | RANGE_AGG_GAPS_SET_NULL)
+#define RANGE_AGG_OVERLAPS_FLAGS	(RANGE_AGG_OVERLAPS_RAISE | RANGE_AGG_OVERLAPS_SET_NULL)
 
 static RangeIOData *get_range_io_data(FunctionCallInfo fcinfo, Oid rngtypid,
 									  IOFuncSelector func);
@@ -69,6 +82,7 @@ static Size datum_compute_size(Size sz, Datum datum, bool typbyval,
 							   char typalign, int16 typlen, char typstorage);
 static Pointer datum_write(Pointer ptr, Datum datum, bool typbyval,
 						   char typalign, int16 typlen, char typstorage);
+static int element_compare(const void *key1, const void *key2, void *arg);
 
 
 /*
@@ -1135,6 +1149,189 @@ range_intersect(PG_FUNCTION_ARGS)
 	PG_RETURN_RANGE_P(make_range(typcache, result_lower, result_upper, false));
 }
 
+/* Aggregate functions */
+
+/*
+ * range_agg_transfn: combine adjacent/overlapping ranges.
+ *
+ * All we do here is gather the input ranges into an array
+ * so that the finalfn can sort and combine them.
+ * We can't simply use array_append as the transfn
+ * because we have to capture the second & third parameters
+ * and put them in the aggregate's running state,
+ * so that our finalfn can use them.
+ */
+Datum
+range_agg_transfn(PG_FUNCTION_ARGS)
+{
+	Oid rangeTypeId;
+	MemoryContext aggContext;
+	RangeAggState *state;
+
+	if (!AggCheckCallContext(fcinfo, &aggContext))
+	{
+		elog(ERROR, "range_agg_transfn called in non-aggregate context");
+	}
+
+	rangeTypeId = get_fn_expr_argtype(fcinfo->flinfo, 1);
+	if (!type_is_range(rangeTypeId))
+	{
+		ereport(ERROR, (errmsg("range_agg must be called with a range")));
+	}
+	if (PG_ARGISNULL(0))
+	{
+		state = MemoryContextAlloc(aggContext, sizeof(RangeAggState));
+		state->inputs = initArrayResult(rangeTypeId, aggContext, false);
+
+		state->flags = 0x0;
+		switch (PG_NARGS())
+		{
+			case 2:
+				state->flags |= RANGE_AGG_GAPS_RAISE;
+				state->flags |= RANGE_AGG_OVERLAPS_RAISE;
+				break;
+			case 3:
+				if (PG_ARGISNULL(2))         state->flags |= RANGE_AGG_GAPS_SET_NULL;
+				else if (!PG_GETARG_BOOL(2)) state->flags |= RANGE_AGG_GAPS_RAISE;
+				state->flags |= RANGE_AGG_OVERLAPS_RAISE;
+				break;
+			case 4:
+				if (PG_ARGISNULL(2))         state->flags |= RANGE_AGG_GAPS_SET_NULL;
+				else if (!PG_GETARG_BOOL(2)) state->flags |= RANGE_AGG_GAPS_RAISE;
+				if (PG_ARGISNULL(3))         state->flags |= RANGE_AGG_OVERLAPS_SET_NULL;
+				else if (!PG_GETARG_BOOL(3)) state->flags |= RANGE_AGG_OVERLAPS_RAISE;
+				break;
+			default:
+				Assert(false);
+		}
+	}
+	else
+	{
+		state = (RangeAggState *)PG_GETARG_POINTER(0);
+	}
+
+	/* Might as well skip NULLs here so the finalfn doesn't have to: */
+	if (!PG_ARGISNULL(1))
+	{
+		accumArrayResult(state->inputs, PG_GETARG_DATUM(1), false,
+				rangeTypeId, aggContext);
+	}
+
+	PG_RETURN_POINTER(state);
+}
+
+/* range_agg_finalfn: combine adjacent/overlapping ranges */
+Datum
+range_agg_finalfn(PG_FUNCTION_ARGS)
+{
+	MemoryContext aggContext;
+	Oid rangeTypeId;
+	TypeCacheEntry *typcache;
+	RangeAggState *state;
+	ArrayBuildState *inputArray;
+	int inputLength;
+	Datum *inputVals;
+	bool *inputNulls;
+	int i;
+	RangeType *currentRange;
+	RangeType *lastRange;
+	char *r1Str, *r2Str;
+	ArrayBuildState *resultContent;
+	Datum result;
+
+	if (!AggCheckCallContext(fcinfo, &aggContext))
+	{
+		elog(ERROR, "range_agg_finalfn called in non-aggregate context");
+	}
+
+	state = PG_ARGISNULL(0) ? NULL : (RangeAggState *)PG_GETARG_POINTER(0);
+	if (state == NULL) PG_RETURN_NULL();
+	inputArray  = state->inputs;
+	inputVals   = inputArray->dvalues;
+	inputNulls  = inputArray->dnulls;
+	inputLength = inputArray->nelems;
+	rangeTypeId = inputArray->element_type;
+
+	typcache = range_get_typcache(fcinfo, rangeTypeId);
+	if (inputLength == 0) PG_RETURN_NULL();
+	qsort_arg(inputVals, inputLength, sizeof(Datum), element_compare, typcache);
+
+	resultContent = initArrayResult(rangeTypeId, aggContext, false);
+	lastRange = DatumGetRangeTypeP(inputVals[0]);
+	for (i = 1; i < inputLength; i++)
+	{
+		Assert(!inputNulls[i]);
+		currentRange = DatumGetRangeTypeP(inputVals[i]);
+		/* range_adjacent_interval gives true
+		 * if *either* A meets B or B meets A,
+		 * which is not quite what we want,
+		 * but we rely on the sorting above
+		 * to rule out B meets A ever happening.
+		 */
+		if (range_adjacent_internal(typcache, lastRange, currentRange))
+		{
+			/* The two ranges touch without overlap: */
+			lastRange = range_union_internal(typcache, lastRange, currentRange, false);
+		}
+		else if (range_before_internal(typcache, lastRange, currentRange))
+		{
+			/* There is a gap: */
+			if (state->flags & RANGE_AGG_GAPS_FLAGS)
+			{
+				if (state->flags & RANGE_AGG_GAPS_RAISE)
+				{
+					r1Str = "lastRange";
+					r2Str = "currentRange";
+					// TODO: Why is this segfaulting?:
+					// r1Str = DatumGetCString(DirectFunctionCall1(range_out, RangeTypePGetDatum(lastRange)));
+					// r2Str = DatumGetCString(DirectFunctionCall1(range_out, RangeTypePGetDatum(currentRange)));
+					ereport(ERROR, (errmsg("range_agg: gap detected between %s and %s", r1Str, r2Str)));
+				}
+				else
+				{
+					Assert(state->flags & RANGE_AGG_GAPS_SET_NULL);
+					PG_RETURN_NULL();
+				}
+			}
+			accumArrayResult(resultContent, RangeTypePGetDatum(lastRange), false, rangeTypeId, aggContext);
+			lastRange = currentRange;
+
+		}
+		else
+		{
+			/* They must overlap: */
+			if (state->flags & RANGE_AGG_OVERLAPS_FLAGS)
+			{
+				if (state->flags & RANGE_AGG_OVERLAPS_RAISE)
+				{
+					r1Str = "lastRange"; r2Str = "currentRange";
+					// TODO: Why is this segfaulting?:
+					// r1Str = DatumGetCString(DirectFunctionCall1(range_out, RangeTypePGetDatum(lastRange)));
+					// r2Str = DatumGetCString(DirectFunctionCall1(range_out, RangeTypePGetDatum(currentRange)));
+					ereport(ERROR, (errmsg("range_agg: overlap detected between %s and %s", r1Str, r2Str)));
+				}
+				else
+				{
+					Assert(state->flags & RANGE_AGG_OVERLAPS_SET_NULL);
+					PG_RETURN_NULL();
+				}
+			}
+			lastRange = range_union_internal(typcache, lastRange, currentRange, false);
+		}
+	}
+	accumArrayResult(resultContent, RangeTypePGetDatum(lastRange), false, rangeTypeId, aggContext);
+
+	if (type_is_array(get_fn_expr_rettype(fcinfo->flinfo)))
+	{
+		result = makeArrayResult(resultContent, CurrentMemoryContext);
+		PG_RETURN_DATUM(result);
+	}
+	else
+	{
+		PG_RETURN_DATUM(RangeTypePGetDatum(lastRange));
+	}
+}
+
 /* Btree support */
 
 /* btree comparator */
@@ -2488,3 +2685,33 @@ datum_write(Pointer ptr, Datum datum, bool typbyval, char typalign,
 
 	return ptr;
 }
+
+/*
+ * Compare two ranges so we can qsort them.
+ */
+static int
+element_compare(const void *key1, const void *key2, void *arg)
+{
+	Datum *d1 = (Datum *)key1;
+	Datum *d2 = (Datum *)key2;
+	RangeType *r1 = DatumGetRangeTypeP(*d1);
+	RangeType *r2 = DatumGetRangeTypeP(*d2);
+	TypeCacheEntry *typcache = (TypeCacheEntry *) arg;
+	RangeBound lower1, lower2;
+	RangeBound upper1, upper2;
+	bool empty1, empty2;
+	int cmp;
+
+	range_deserialize(typcache, r1, &lower1, &upper1, &empty1);
+	range_deserialize(typcache, r2, &lower2, &upper2, &empty2);
+
+	if (empty1 && empty2) cmp = 0;
+	else if (empty1) cmp = -1;
+	else if (empty2) cmp = 1;
+	else {
+		cmp = range_cmp_bounds(typcache, &lower1, &lower2);
+		if (cmp == 0) cmp = range_cmp_bounds(typcache, &upper1, &upper2);
+	}
+
+	return cmp;
+}
diff --git a/src/include/catalog/pg_aggregate.dat b/src/include/catalog/pg_aggregate.dat
index 044695a046..1c8aec3977 100644
--- a/src/include/catalog/pg_aggregate.dat
+++ b/src/include/catalog/pg_aggregate.dat
@@ -544,6 +544,47 @@
 { aggfnoid => 'jsonb_object_agg', aggtransfn => 'jsonb_object_agg_transfn',
   aggfinalfn => 'jsonb_object_agg_finalfn', aggtranstype => 'internal' },
 
+# range
+{ aggfnoid => 'range_agg(anyrange)', aggtransfn => 'range_agg_transfn',
+  aggfinalfn => 'range_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(int4range,bool)', aggtransfn => 'int4range_agg_transfn',
+  aggfinalfn => 'int4range_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(int8range,bool)', aggtransfn => 'int8range_agg_transfn',
+  aggfinalfn => 'int8range_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(numrange,bool)', aggtransfn => 'numrange_agg_transfn',
+  aggfinalfn => 'numrange_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(tsrange,bool)', aggtransfn => 'tsrange_agg_transfn',
+  aggfinalfn => 'tsrange_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(tstzrange,bool)', aggtransfn => 'tstzrange_agg_transfn',
+  aggfinalfn => 'tstzrange_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(daterange,bool)', aggtransfn => 'daterange_agg_transfn',
+  aggfinalfn => 'daterange_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(int4range,bool,bool)', aggtransfn => 'int4rangebb_agg_transfn',
+  aggfinalfn => 'int4rangebb_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(int8range,bool,bool)', aggtransfn => 'int8rangebb_agg_transfn',
+  aggfinalfn => 'int8rangebb_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(numrange,bool,bool)', aggtransfn => 'numrangebb_agg_transfn',
+  aggfinalfn => 'numrangebb_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(tsrange,bool,bool)', aggtransfn => 'tsrangebb_agg_transfn',
+  aggfinalfn => 'tsrangebb_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(tstzrange,bool,bool)', aggtransfn => 'tstzrangebb_agg_transfn',
+  aggfinalfn => 'tstzrangebb_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+{ aggfnoid => 'range_agg(daterange,bool,bool)', aggtransfn => 'daterangebb_agg_transfn',
+  aggfinalfn => 'daterangebb_agg_finalfn', aggfinalextra => 't',
+  aggtranstype => 'internal' },
+
 # ordered-set and hypothetical-set aggregates
 { aggfnoid => 'percentile_disc(float8,anyelement)', aggkind => 'o',
   aggnumdirectargs => '1', aggtransfn => 'ordered_set_transition',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 87335248a0..249220c8a2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9538,6 +9538,144 @@
 { oid => '3869',
   proname => 'range_minus', prorettype => 'anyrange',
   proargtypes => 'anyrange anyrange', prosrc => 'range_minus' },
+{ oid => '8000', descr => 'aggregate transition function',
+  proname => 'range_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal anyrange', prosrc => 'range_agg_transfn' },
+{ oid => '8001', descr => 'aggregate final function',
+  proname => 'range_agg_finalfn', proisstrict => 'f', prorettype => 'anyrange',
+  proargtypes => 'internal anyrange', prosrc => 'range_agg_finalfn' },
+{ oid => '8002', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => 'anyrange', proargtypes => 'anyrange',
+  prosrc => 'aggregate_dummy' },
+
+# Making 2- and 3-param range_agg polymorphic is difficult
+# because it would take an anyrange and return an anyrange[],
+# which doesn't exist.
+# As a workaround we define separate functions for each built-in range type.
+# This is what causes the mess in src/test/regress/expected/opr_sanity.out.
+{ oid => '8003', descr => 'aggregate transition function',
+  proname => 'int4range_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal int4range bool', prosrc => 'range_agg_transfn' },
+{ oid => '8004', descr => 'aggregate final function',
+  proname => 'int4range_agg_finalfn', proisstrict => 'f', prorettype => '_int4range',
+  proargtypes => 'internal int4range bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8005', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_int4range', proargtypes => 'int4range bool',
+  prosrc => 'aggregate_dummy' },
+{ oid => '8006', descr => 'aggregate transition function',
+  proname => 'int8range_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal int8range bool', prosrc => 'range_agg_transfn' },
+{ oid => '8007', descr => 'aggregate final function',
+  proname => 'int8range_agg_finalfn', proisstrict => 'f', prorettype => '_int8range',
+  proargtypes => 'internal int8range bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8008', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_int8range', proargtypes => 'int8range bool',
+  prosrc => 'aggregate_dummy' },
+{ oid => '8009', descr => 'aggregate transition function',
+  proname => 'numrange_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal numrange bool', prosrc => 'range_agg_transfn' },
+{ oid => '8010', descr => 'aggregate final function',
+  proname => 'numrange_agg_finalfn', proisstrict => 'f', prorettype => '_numrange',
+  proargtypes => 'internal numrange bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8011', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_numrange', proargtypes => 'numrange bool',
+  prosrc => 'aggregate_dummy' },
+{ oid => '8012', descr => 'aggregate transition function',
+  proname => 'tsrange_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal tsrange bool', prosrc => 'range_agg_transfn' },
+{ oid => '8013', descr => 'aggregate final function',
+  proname => 'tsrange_agg_finalfn', proisstrict => 'f', prorettype => '_tsrange',
+  proargtypes => 'internal tsrange bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8014', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_tsrange', proargtypes => 'tsrange bool',
+  prosrc => 'aggregate_dummy' },
+{ oid => '8015', descr => 'aggregate transition function',
+  proname => 'tstzrange_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal tstzrange bool', prosrc => 'range_agg_transfn' },
+{ oid => '8016', descr => 'aggregate final function',
+  proname => 'tstzrange_agg_finalfn', proisstrict => 'f', prorettype => '_tstzrange',
+  proargtypes => 'internal tstzrange bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8017', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_tstzrange', proargtypes => 'tstzrange bool',
+  prosrc => 'aggregate_dummy' },
+{ oid => '8018', descr => 'aggregate transition function',
+  proname => 'daterange_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal daterange bool', prosrc => 'range_agg_transfn' },
+{ oid => '8019', descr => 'aggregate final function',
+  proname => 'daterange_agg_finalfn', proisstrict => 'f', prorettype => '_daterange',
+  proargtypes => 'internal daterange bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8020', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_daterange', proargtypes => 'daterange bool',
+  prosrc => 'aggregate_dummy' },
+
+{ oid => '8021', descr => 'aggregate transition function',
+  proname => 'int4rangebb_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal int4range bool bool', prosrc => 'range_agg_transfn' },
+{ oid => '8022', descr => 'aggregate final function',
+  proname => 'int4rangebb_agg_finalfn', proisstrict => 'f', prorettype => '_int4range',
+  proargtypes => 'internal int4range bool bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8023', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_int4range', proargtypes => 'int4range bool bool',
+  prosrc => 'aggregate_dummy' },
+{ oid => '8024', descr => 'aggregate transition function',
+  proname => 'int8rangebb_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal int8range bool bool', prosrc => 'range_agg_transfn' },
+{ oid => '8025', descr => 'aggregate final function',
+  proname => 'int8rangebb_agg_finalfn', proisstrict => 'f', prorettype => '_int8range',
+  proargtypes => 'internal int8range bool bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8026', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_int8range', proargtypes => 'int8range bool bool',
+  prosrc => 'aggregate_dummy' },
+{ oid => '8027', descr => 'aggregate transition function',
+  proname => 'numrangebb_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal numrange bool bool', prosrc => 'range_agg_transfn' },
+{ oid => '8028', descr => 'aggregate final function',
+  proname => 'numrangebb_agg_finalfn', proisstrict => 'f', prorettype => '_numrange',
+  proargtypes => 'internal numrange bool bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8029', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_numrange', proargtypes => 'numrange bool bool',
+  prosrc => 'aggregate_dummy' },
+{ oid => '8030', descr => 'aggregate transition function',
+  proname => 'tsrangebb_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal tsrange bool bool', prosrc => 'range_agg_transfn' },
+{ oid => '8031', descr => 'aggregate final function',
+  proname => 'tsrangebb_agg_finalfn', proisstrict => 'f', prorettype => '_tsrange',
+  proargtypes => 'internal tsrange bool bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8032', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_tsrange', proargtypes => 'tsrange bool bool',
+  prosrc => 'aggregate_dummy' },
+{ oid => '8033', descr => 'aggregate transition function',
+  proname => 'tstzrangebb_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal tstzrange bool bool', prosrc => 'range_agg_transfn' },
+{ oid => '8034', descr => 'aggregate final function',
+  proname => 'tstzrangebb_agg_finalfn', proisstrict => 'f', prorettype => '_tstzrange',
+  proargtypes => 'internal tstzrange bool bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8035', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_tstzrange', proargtypes => 'tstzrange bool bool',
+  prosrc => 'aggregate_dummy' },
+{ oid => '8036', descr => 'aggregate transition function',
+  proname => 'daterangebb_agg_transfn', proisstrict => 'f', prorettype => 'internal',
+  proargtypes => 'internal daterange bool bool', prosrc => 'range_agg_transfn' },
+{ oid => '8037', descr => 'aggregate final function',
+  proname => 'daterangebb_agg_finalfn', proisstrict => 'f', prorettype => '_daterange',
+  proargtypes => 'internal daterange bool bool', prosrc => 'range_agg_finalfn' },
+{ oid => '8038', descr => 'combinate aggregate input into a range',
+  proname => 'range_agg', prokind => 'a', proisstrict => 'f',
+  prorettype => '_daterange', proargtypes => 'daterange bool bool',
+  prosrc => 'aggregate_dummy' },
+
 { oid => '3870', descr => 'less-equal-greater',
   proname => 'range_cmp', prorettype => 'int4',
   proargtypes => 'anyrange anyrange', prosrc => 'range_cmp' },
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 85af36ee5b..a2cd607b31 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -168,9 +168,105 @@ WHERE p1.oid < p2.oid AND
      p1.proretset != p2.proretset OR
      p1.provolatile != p2.provolatile OR
      p1.pronargs != p2.pronargs);
- oid | proname | oid | proname 
------+---------+-----+---------
-(0 rows)
+ oid  |        proname        | oid  |         proname         
+------+-----------------------+------+-------------------------
+ 8019 | daterange_agg_finalfn | 8028 | numrangebb_agg_finalfn
+ 8019 | daterange_agg_finalfn | 8037 | daterangebb_agg_finalfn
+ 8019 | daterange_agg_finalfn | 8034 | tstzrangebb_agg_finalfn
+ 8019 | daterange_agg_finalfn | 8031 | tsrangebb_agg_finalfn
+ 8019 | daterange_agg_finalfn | 8025 | int8rangebb_agg_finalfn
+ 8019 | daterange_agg_finalfn | 8022 | int4rangebb_agg_finalfn
+ 8016 | tstzrange_agg_finalfn | 8028 | numrangebb_agg_finalfn
+ 8016 | tstzrange_agg_finalfn | 8037 | daterangebb_agg_finalfn
+ 8016 | tstzrange_agg_finalfn | 8034 | tstzrangebb_agg_finalfn
+ 8016 | tstzrange_agg_finalfn | 8031 | tsrangebb_agg_finalfn
+ 8016 | tstzrange_agg_finalfn | 8025 | int8rangebb_agg_finalfn
+ 8016 | tstzrange_agg_finalfn | 8022 | int4rangebb_agg_finalfn
+ 8013 | tsrange_agg_finalfn   | 8028 | numrangebb_agg_finalfn
+ 8013 | tsrange_agg_finalfn   | 8037 | daterangebb_agg_finalfn
+ 8013 | tsrange_agg_finalfn   | 8034 | tstzrangebb_agg_finalfn
+ 8013 | tsrange_agg_finalfn   | 8031 | tsrangebb_agg_finalfn
+ 8013 | tsrange_agg_finalfn   | 8025 | int8rangebb_agg_finalfn
+ 8013 | tsrange_agg_finalfn   | 8022 | int4rangebb_agg_finalfn
+ 8010 | numrange_agg_finalfn  | 8028 | numrangebb_agg_finalfn
+ 8010 | numrange_agg_finalfn  | 8037 | daterangebb_agg_finalfn
+ 8010 | numrange_agg_finalfn  | 8034 | tstzrangebb_agg_finalfn
+ 8010 | numrange_agg_finalfn  | 8031 | tsrangebb_agg_finalfn
+ 8010 | numrange_agg_finalfn  | 8025 | int8rangebb_agg_finalfn
+ 8010 | numrange_agg_finalfn  | 8022 | int4rangebb_agg_finalfn
+ 8007 | int8range_agg_finalfn | 8028 | numrangebb_agg_finalfn
+ 8007 | int8range_agg_finalfn | 8037 | daterangebb_agg_finalfn
+ 8007 | int8range_agg_finalfn | 8034 | tstzrangebb_agg_finalfn
+ 8007 | int8range_agg_finalfn | 8031 | tsrangebb_agg_finalfn
+ 8007 | int8range_agg_finalfn | 8025 | int8rangebb_agg_finalfn
+ 8007 | int8range_agg_finalfn | 8022 | int4rangebb_agg_finalfn
+ 8004 | int4range_agg_finalfn | 8028 | numrangebb_agg_finalfn
+ 8004 | int4range_agg_finalfn | 8037 | daterangebb_agg_finalfn
+ 8004 | int4range_agg_finalfn | 8034 | tstzrangebb_agg_finalfn
+ 8004 | int4range_agg_finalfn | 8031 | tsrangebb_agg_finalfn
+ 8004 | int4range_agg_finalfn | 8025 | int8rangebb_agg_finalfn
+ 8004 | int4range_agg_finalfn | 8022 | int4rangebb_agg_finalfn
+ 8001 | range_agg_finalfn     | 8028 | numrangebb_agg_finalfn
+ 8001 | range_agg_finalfn     | 8037 | daterangebb_agg_finalfn
+ 8001 | range_agg_finalfn     | 8034 | tstzrangebb_agg_finalfn
+ 8001 | range_agg_finalfn     | 8031 | tsrangebb_agg_finalfn
+ 8001 | range_agg_finalfn     | 8025 | int8rangebb_agg_finalfn
+ 8001 | range_agg_finalfn     | 8022 | int4rangebb_agg_finalfn
+ 8001 | range_agg_finalfn     | 8019 | daterange_agg_finalfn
+ 8001 | range_agg_finalfn     | 8016 | tstzrange_agg_finalfn
+ 8001 | range_agg_finalfn     | 8013 | tsrange_agg_finalfn
+ 8001 | range_agg_finalfn     | 8010 | numrange_agg_finalfn
+ 8001 | range_agg_finalfn     | 8007 | int8range_agg_finalfn
+ 8001 | range_agg_finalfn     | 8004 | int4range_agg_finalfn
+ 8003 | int4range_agg_transfn | 8036 | daterangebb_agg_transfn
+ 8003 | int4range_agg_transfn | 8027 | numrangebb_agg_transfn
+ 8003 | int4range_agg_transfn | 8030 | tsrangebb_agg_transfn
+ 8003 | int4range_agg_transfn | 8021 | int4rangebb_agg_transfn
+ 8003 | int4range_agg_transfn | 8024 | int8rangebb_agg_transfn
+ 8003 | int4range_agg_transfn | 8033 | tstzrangebb_agg_transfn
+ 8018 | daterange_agg_transfn | 8036 | daterangebb_agg_transfn
+ 8018 | daterange_agg_transfn | 8027 | numrangebb_agg_transfn
+ 8018 | daterange_agg_transfn | 8030 | tsrangebb_agg_transfn
+ 8018 | daterange_agg_transfn | 8021 | int4rangebb_agg_transfn
+ 8018 | daterange_agg_transfn | 8024 | int8rangebb_agg_transfn
+ 8018 | daterange_agg_transfn | 8033 | tstzrangebb_agg_transfn
+ 8015 | tstzrange_agg_transfn | 8036 | daterangebb_agg_transfn
+ 8015 | tstzrange_agg_transfn | 8027 | numrangebb_agg_transfn
+ 8015 | tstzrange_agg_transfn | 8030 | tsrangebb_agg_transfn
+ 8015 | tstzrange_agg_transfn | 8021 | int4rangebb_agg_transfn
+ 8015 | tstzrange_agg_transfn | 8024 | int8rangebb_agg_transfn
+ 8015 | tstzrange_agg_transfn | 8033 | tstzrangebb_agg_transfn
+ 8006 | int8range_agg_transfn | 8036 | daterangebb_agg_transfn
+ 8006 | int8range_agg_transfn | 8027 | numrangebb_agg_transfn
+ 8006 | int8range_agg_transfn | 8030 | tsrangebb_agg_transfn
+ 8006 | int8range_agg_transfn | 8021 | int4rangebb_agg_transfn
+ 8006 | int8range_agg_transfn | 8024 | int8rangebb_agg_transfn
+ 8006 | int8range_agg_transfn | 8033 | tstzrangebb_agg_transfn
+ 8012 | tsrange_agg_transfn   | 8036 | daterangebb_agg_transfn
+ 8012 | tsrange_agg_transfn   | 8027 | numrangebb_agg_transfn
+ 8012 | tsrange_agg_transfn   | 8030 | tsrangebb_agg_transfn
+ 8012 | tsrange_agg_transfn   | 8021 | int4rangebb_agg_transfn
+ 8012 | tsrange_agg_transfn   | 8024 | int8rangebb_agg_transfn
+ 8012 | tsrange_agg_transfn   | 8033 | tstzrangebb_agg_transfn
+ 8000 | range_agg_transfn     | 8036 | daterangebb_agg_transfn
+ 8000 | range_agg_transfn     | 8027 | numrangebb_agg_transfn
+ 8000 | range_agg_transfn     | 8030 | tsrangebb_agg_transfn
+ 8000 | range_agg_transfn     | 8003 | int4range_agg_transfn
+ 8000 | range_agg_transfn     | 8021 | int4rangebb_agg_transfn
+ 8000 | range_agg_transfn     | 8018 | daterange_agg_transfn
+ 8000 | range_agg_transfn     | 8015 | tstzrange_agg_transfn
+ 8000 | range_agg_transfn     | 8024 | int8rangebb_agg_transfn
+ 8000 | range_agg_transfn     | 8006 | int8range_agg_transfn
+ 8000 | range_agg_transfn     | 8012 | tsrange_agg_transfn
+ 8000 | range_agg_transfn     | 8009 | numrange_agg_transfn
+ 8000 | range_agg_transfn     | 8033 | tstzrangebb_agg_transfn
+ 8009 | numrange_agg_transfn  | 8036 | daterangebb_agg_transfn
+ 8009 | numrange_agg_transfn  | 8027 | numrangebb_agg_transfn
+ 8009 | numrange_agg_transfn  | 8030 | tsrangebb_agg_transfn
+ 8009 | numrange_agg_transfn  | 8021 | int4rangebb_agg_transfn
+ 8009 | numrange_agg_transfn  | 8024 | int8rangebb_agg_transfn
+ 8009 | numrange_agg_transfn  | 8033 | tstzrangebb_agg_transfn
+(96 rows)
 
 -- Look for uses of different type OIDs in the argument/result type fields
 -- for different aliases of the same built-in function.
@@ -196,7 +292,28 @@ ORDER BY 1, 2;
 ------------+------------
          25 |       1043
        1114 |       1184
-(2 rows)
+       3831 |       3905
+       3831 |       3907
+       3831 |       3909
+       3831 |       3911
+       3831 |       3913
+       3831 |       3927
+       3905 |       3907
+       3905 |       3909
+       3905 |       3911
+       3905 |       3913
+       3905 |       3927
+       3907 |       3909
+       3907 |       3911
+       3907 |       3913
+       3907 |       3927
+       3909 |       3911
+       3909 |       3913
+       3909 |       3927
+       3911 |       3913
+       3911 |       3927
+       3913 |       3927
+(23 rows)
 
 SELECT DISTINCT p1.proargtypes[0], p2.proargtypes[0]
 FROM pg_proc AS p1, pg_proc AS p2
@@ -231,7 +348,28 @@ ORDER BY 1, 2;
           23 |          28
         1114 |        1184
         1560 |        1562
-(3 rows)
+        3831 |        3904
+        3831 |        3906
+        3831 |        3908
+        3831 |        3910
+        3831 |        3912
+        3831 |        3926
+        3904 |        3906
+        3904 |        3908
+        3904 |        3910
+        3904 |        3912
+        3904 |        3926
+        3906 |        3908
+        3906 |        3910
+        3906 |        3912
+        3906 |        3926
+        3908 |        3910
+        3908 |        3912
+        3908 |        3926
+        3910 |        3912
+        3910 |        3926
+        3912 |        3926
+(24 rows)
 
 SELECT DISTINCT p1.proargtypes[2], p2.proargtypes[2]
 FROM pg_proc AS p1, pg_proc AS p2
@@ -1716,10 +1854,58 @@ WHERE p1.oid < p2.oid AND p1.proname = p2.proname AND
     p1.prokind = 'a' AND p2.prokind = 'a' AND
     array_dims(p1.proargtypes) != array_dims(p2.proargtypes)
 ORDER BY 1;
-     oid      |   oid   
---------------+---------
- count("any") | count()
-(1 row)
+             oid              |                 oid                  
+------------------------------+--------------------------------------
+ count("any")                 | count()
+ range_agg(anyrange)          | range_agg(daterange,boolean,boolean)
+ range_agg(anyrange)          | range_agg(tstzrange,boolean,boolean)
+ range_agg(anyrange)          | range_agg(tsrange,boolean,boolean)
+ range_agg(anyrange)          | range_agg(numrange,boolean,boolean)
+ range_agg(anyrange)          | range_agg(int8range,boolean,boolean)
+ range_agg(anyrange)          | range_agg(int4range,boolean,boolean)
+ range_agg(anyrange)          | range_agg(daterange,boolean)
+ range_agg(anyrange)          | range_agg(tstzrange,boolean)
+ range_agg(anyrange)          | range_agg(tsrange,boolean)
+ range_agg(anyrange)          | range_agg(numrange,boolean)
+ range_agg(anyrange)          | range_agg(int8range,boolean)
+ range_agg(anyrange)          | range_agg(int4range,boolean)
+ range_agg(int4range,boolean) | range_agg(daterange,boolean,boolean)
+ range_agg(int4range,boolean) | range_agg(tstzrange,boolean,boolean)
+ range_agg(int4range,boolean) | range_agg(tsrange,boolean,boolean)
+ range_agg(int4range,boolean) | range_agg(numrange,boolean,boolean)
+ range_agg(int4range,boolean) | range_agg(int8range,boolean,boolean)
+ range_agg(int4range,boolean) | range_agg(int4range,boolean,boolean)
+ range_agg(int8range,boolean) | range_agg(daterange,boolean,boolean)
+ range_agg(int8range,boolean) | range_agg(tstzrange,boolean,boolean)
+ range_agg(int8range,boolean) | range_agg(tsrange,boolean,boolean)
+ range_agg(int8range,boolean) | range_agg(numrange,boolean,boolean)
+ range_agg(int8range,boolean) | range_agg(int8range,boolean,boolean)
+ range_agg(int8range,boolean) | range_agg(int4range,boolean,boolean)
+ range_agg(numrange,boolean)  | range_agg(daterange,boolean,boolean)
+ range_agg(numrange,boolean)  | range_agg(tstzrange,boolean,boolean)
+ range_agg(numrange,boolean)  | range_agg(tsrange,boolean,boolean)
+ range_agg(numrange,boolean)  | range_agg(numrange,boolean,boolean)
+ range_agg(numrange,boolean)  | range_agg(int8range,boolean,boolean)
+ range_agg(numrange,boolean)  | range_agg(int4range,boolean,boolean)
+ range_agg(tsrange,boolean)   | range_agg(daterange,boolean,boolean)
+ range_agg(tsrange,boolean)   | range_agg(tstzrange,boolean,boolean)
+ range_agg(tsrange,boolean)   | range_agg(tsrange,boolean,boolean)
+ range_agg(tsrange,boolean)   | range_agg(numrange,boolean,boolean)
+ range_agg(tsrange,boolean)   | range_agg(int8range,boolean,boolean)
+ range_agg(tsrange,boolean)   | range_agg(int4range,boolean,boolean)
+ range_agg(tstzrange,boolean) | range_agg(daterange,boolean,boolean)
+ range_agg(tstzrange,boolean) | range_agg(tstzrange,boolean,boolean)
+ range_agg(tstzrange,boolean) | range_agg(tsrange,boolean,boolean)
+ range_agg(tstzrange,boolean) | range_agg(numrange,boolean,boolean)
+ range_agg(tstzrange,boolean) | range_agg(int8range,boolean,boolean)
+ range_agg(tstzrange,boolean) | range_agg(int4range,boolean,boolean)
+ range_agg(daterange,boolean) | range_agg(daterange,boolean,boolean)
+ range_agg(daterange,boolean) | range_agg(tstzrange,boolean,boolean)
+ range_agg(daterange,boolean) | range_agg(tsrange,boolean,boolean)
+ range_agg(daterange,boolean) | range_agg(numrange,boolean,boolean)
+ range_agg(daterange,boolean) | range_agg(int8range,boolean,boolean)
+ range_agg(daterange,boolean) | range_agg(int4range,boolean,boolean)
+(49 rows)
 
 -- For the same reason, built-in aggregates with default arguments are no good.
 SELECT oid, proname
diff --git a/src/test/regress/expected/rangetypes.out b/src/test/regress/expected/rangetypes.out
index accf1e0d9e..703bc41491 100644
--- a/src/test/regress/expected/rangetypes.out
+++ b/src/test/regress/expected/rangetypes.out
@@ -1407,3 +1407,281 @@ create function table_fail(i anyelement) returns table(i anyelement, r anyrange)
   as $$ select $1, '[1,10]' $$ language sql;
 ERROR:  cannot determine result data type
 DETAIL:  A function returning "anyrange" must have at least one "anyrange" argument.
+--
+-- range_agg function
+--
+create table reservations ( room_id integer not null, booked_during daterange );
+insert into reservations values
+-- 1: has a meets and a gap
+(1, daterange('2018-07-01', '2018-07-07')),
+(1, daterange('2018-07-07', '2018-07-14')),
+(1, daterange('2018-07-20', '2018-07-22')),
+-- 2: just a single row
+(2, daterange('2018-07-01', '2018-07-03')),
+-- 3: one null range
+(3, NULL),
+-- 4: two null ranges
+(4, NULL),
+(4, NULL),
+-- 5: a null range and a non-null range
+(5, NULL),
+(5, daterange('2018-07-01', '2018-07-03')),
+-- 6: has overlap
+(6, daterange('2018-07-01', '2018-07-07')),
+(6, daterange('2018-07-05', '2018-07-10')),
+-- 7: two ranges that meet: no gap or overlap
+(7, daterange('2018-07-01', '2018-07-07')),
+(7, daterange('2018-07-07', '2018-07-14'))
+;
+-- range_agg with 1 arg:
+-- Forbidding gaps and overlaps:
+SELECT  room_id, range_agg(booked_during)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id
+ORDER BY room_id;
+ERROR:  range_agg: gap detected between lastRange and currentRange
+SELECT  room_id, range_agg(booked_during)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id
+ORDER BY room_id;
+ERROR:  range_agg: overlap detected between lastRange and currentRange
+SELECT  room_id, range_agg(booked_during)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id
+ORDER BY room_id;
+ room_id |        range_agg        
+---------+-------------------------
+       2 | [07-01-2018,07-03-2018)
+       3 | 
+       4 | 
+       5 | [07-01-2018,07-03-2018)
+       7 | [07-01-2018,07-14-2018)
+(5 rows)
+
+-- Obeying discrete base types:
+SELECT	range_agg(r)
+FROM		(VALUES
+  (int4range( 0,  9, '[]')),
+  (int4range(10, 19, '[]'))
+) t(r);
+ range_agg 
+-----------
+ [0,20)
+(1 row)
+
+-- range_agg with 2 args:
+-- Forbidding gaps (and overlaps):
+SELECT  room_id, range_agg(booked_during, false)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+ERROR:  range_agg: gap detected between lastRange and currentRange
+SELECT  room_id, range_agg(booked_during, false)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+ERROR:  range_agg: overlap detected between lastRange and currentRange
+SELECT  room_id, range_agg(booked_during, false)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+ room_id |          range_agg          
+---------+-----------------------------
+       3 | 
+       5 | {"[07-01-2018,07-03-2018)"}
+       4 | 
+       2 | {"[07-01-2018,07-03-2018)"}
+       7 | {"[07-01-2018,07-14-2018)"}
+(5 rows)
+
+-- Permitting gaps (but forbidding overlaps):
+SELECT  room_id, range_agg(booked_during, true)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+ room_id |                       range_agg                       
+---------+-------------------------------------------------------
+       1 | {"[07-01-2018,07-14-2018)","[07-20-2018,07-22-2018)"}
+(1 row)
+
+SELECT  room_id, range_agg(booked_during, true)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+ERROR:  range_agg: overlap detected between lastRange and currentRange
+SELECT  room_id, range_agg(booked_during, true)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+ room_id |          range_agg          
+---------+-----------------------------
+       3 | 
+       5 | {"[07-01-2018,07-03-2018)"}
+       4 | 
+       2 | {"[07-01-2018,07-03-2018)"}
+       7 | {"[07-01-2018,07-14-2018)"}
+(5 rows)
+
+-- range_agg with 3 args:
+-- Forbidding gaps and overlaps:
+SELECT  room_id, range_agg(booked_during, false, false)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+ERROR:  range_agg: gap detected between lastRange and currentRange
+SELECT  room_id, range_agg(booked_during, false, false)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+ERROR:  range_agg: overlap detected between lastRange and currentRange
+SELECT  room_id, range_agg(booked_during, false, false)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+ room_id |          range_agg          
+---------+-----------------------------
+       3 | 
+       5 | {"[07-01-2018,07-03-2018)"}
+       4 | 
+       2 | {"[07-01-2018,07-03-2018)"}
+       7 | {"[07-01-2018,07-14-2018)"}
+(5 rows)
+
+-- Forbidding gaps but permitting overlaps
+SELECT  room_id, range_agg(booked_during, false, true)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+ERROR:  range_agg: gap detected between lastRange and currentRange
+SELECT  room_id, range_agg(booked_during, false, true)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+ room_id |          range_agg          
+---------+-----------------------------
+       6 | {"[07-01-2018,07-10-2018)"}
+(1 row)
+
+SELECT  room_id, range_agg(booked_during, false, true)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+ room_id |          range_agg          
+---------+-----------------------------
+       3 | 
+       5 | {"[07-01-2018,07-03-2018)"}
+       4 | 
+       2 | {"[07-01-2018,07-03-2018)"}
+       7 | {"[07-01-2018,07-14-2018)"}
+(5 rows)
+
+-- Permitting gaps but forbidding overlaps
+SELECT  room_id, range_agg(booked_during, true, false)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+ room_id |                       range_agg                       
+---------+-------------------------------------------------------
+       1 | {"[07-01-2018,07-14-2018)","[07-20-2018,07-22-2018)"}
+(1 row)
+
+SELECT  room_id, range_agg(booked_during, true, false)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+ERROR:  range_agg: overlap detected between lastRange and currentRange
+SELECT  room_id, range_agg(booked_during, true, false)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+ room_id |          range_agg          
+---------+-----------------------------
+       3 | 
+       5 | {"[07-01-2018,07-03-2018)"}
+       4 | 
+       2 | {"[07-01-2018,07-03-2018)"}
+       7 | {"[07-01-2018,07-14-2018)"}
+(5 rows)
+
+-- Permitting gaps and overlaps:
+SELECT  room_id, range_agg(booked_during, true, true)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+ room_id |                       range_agg                       
+---------+-------------------------------------------------------
+       1 | {"[07-01-2018,07-14-2018)","[07-20-2018,07-22-2018)"}
+(1 row)
+
+SELECT  room_id, range_agg(booked_during, true, true)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+ room_id |          range_agg          
+---------+-----------------------------
+       6 | {"[07-01-2018,07-10-2018)"}
+(1 row)
+
+SELECT  room_id, range_agg(booked_during, true, true)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+ room_id |          range_agg          
+---------+-----------------------------
+       3 | 
+       5 | {"[07-01-2018,07-03-2018)"}
+       4 | 
+       2 | {"[07-01-2018,07-03-2018)"}
+       7 | {"[07-01-2018,07-14-2018)"}
+(5 rows)
+
+-- Obeying discrete base types:
+SELECT	range_agg(r, false, false)
+FROM		(VALUES
+  (int4range( 0,  5, '[]')),
+  (int4range( 7,  9, '[]'))
+) t(r);
+ERROR:  range_agg: gap detected between lastRange and currentRange
+SELECT	range_agg(r, false, false)
+FROM		(VALUES
+  (int4range( 0,  5, '[]')),
+  (int4range( 5,  9, '[]'))
+) t(r);
+ERROR:  range_agg: overlap detected between lastRange and currentRange
+SELECT	range_agg(r, true, true)
+FROM		(VALUES
+  (int4range( 0,  9, '[]')),
+  (int4range(10, 15, '[]')),
+  (int4range(20, 26, '[]')),
+  (int4range(26, 29, '[]'))
+) t(r);
+      range_agg       
+----------------------
+ {"[0,16)","[20,30)"}
+(1 row)
+
+-- It combines with UNNEST
+-- to implement the temporal database "coalesce" function
+-- (see Snodgrass 6.5.2):
+SELECT  room_id, t2.booked_during
+FROM    (
+          SELECT  room_id, range_agg(booked_during, true, true) AS booked_during
+          FROM    reservations
+          GROUP BY room_id
+        ) AS t1,
+        UNNEST(t1.booked_during) AS t2(booked_during)
+ORDER BY room_id, booked_during
+;
+ room_id |      booked_during      
+---------+-------------------------
+       1 | [07-01-2018,07-14-2018)
+       1 | [07-20-2018,07-22-2018)
+       2 | [07-01-2018,07-03-2018)
+       5 | [07-01-2018,07-03-2018)
+       6 | [07-01-2018,07-10-2018)
+       7 | [07-01-2018,07-14-2018)
+(6 rows)
+
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 8ff0da185e..395bfeb75d 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -170,6 +170,7 @@ quad_poly_tbl|t
 radix_text_tbl|t
 ramp|f
 real_city|f
+reservations|f
 road|t
 shighway|t
 slow_emp4000|f
diff --git a/src/test/regress/sql/rangetypes.sql b/src/test/regress/sql/rangetypes.sql
index 55638a85ee..9d1be6f3ca 100644
--- a/src/test/regress/sql/rangetypes.sql
+++ b/src/test/regress/sql/rangetypes.sql
@@ -503,3 +503,192 @@ create function inoutparam_fail(inout i anyelement, out r anyrange)
 --should fail
 create function table_fail(i anyelement) returns table(i anyelement, r anyrange)
   as $$ select $1, '[1,10]' $$ language sql;
+
+--
+-- range_agg function
+--
+create table reservations ( room_id integer not null, booked_during daterange );
+insert into reservations values
+-- 1: has a meets and a gap
+(1, daterange('2018-07-01', '2018-07-07')),
+(1, daterange('2018-07-07', '2018-07-14')),
+(1, daterange('2018-07-20', '2018-07-22')),
+-- 2: just a single row
+(2, daterange('2018-07-01', '2018-07-03')),
+-- 3: one null range
+(3, NULL),
+-- 4: two null ranges
+(4, NULL),
+(4, NULL),
+-- 5: a null range and a non-null range
+(5, NULL),
+(5, daterange('2018-07-01', '2018-07-03')),
+-- 6: has overlap
+(6, daterange('2018-07-01', '2018-07-07')),
+(6, daterange('2018-07-05', '2018-07-10')),
+-- 7: two ranges that meet: no gap or overlap
+(7, daterange('2018-07-01', '2018-07-07')),
+(7, daterange('2018-07-07', '2018-07-14'))
+;
+
+-- range_agg with 1 arg:
+
+-- Forbidding gaps and overlaps:
+SELECT  room_id, range_agg(booked_during)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id
+ORDER BY room_id;
+
+SELECT  room_id, range_agg(booked_during)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id
+ORDER BY room_id;
+
+SELECT  room_id, range_agg(booked_during)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id
+ORDER BY room_id;
+
+-- Obeying discrete base types:
+SELECT	range_agg(r)
+FROM		(VALUES
+  (int4range( 0,  9, '[]')),
+  (int4range(10, 19, '[]'))
+) t(r);
+
+-- range_agg with 2 args:
+
+-- Forbidding gaps (and overlaps):
+SELECT  room_id, range_agg(booked_during, false)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, false)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, false)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+
+-- Permitting gaps (but forbidding overlaps):
+SELECT  room_id, range_agg(booked_during, true)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, true)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, true)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+
+-- range_agg with 3 args:
+
+-- Forbidding gaps and overlaps:
+SELECT  room_id, range_agg(booked_during, false, false)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, false, false)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, false, false)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+
+-- Forbidding gaps but permitting overlaps
+SELECT  room_id, range_agg(booked_during, false, true)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, false, true)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, false, true)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+
+-- Permitting gaps but forbidding overlaps
+SELECT  room_id, range_agg(booked_during, true, false)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, true, false)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, true, false)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+
+-- Permitting gaps and overlaps:
+SELECT  room_id, range_agg(booked_during, true, true)
+FROM    reservations
+WHERE   room_id = 1
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, true, true)
+FROM    reservations
+WHERE   room_id = 6
+GROUP BY room_id;
+
+SELECT  room_id, range_agg(booked_during, true, true)
+FROM    reservations
+WHERE   room_id NOT IN (1, 6)
+GROUP BY room_id;
+
+-- Obeying discrete base types:
+SELECT	range_agg(r, false, false)
+FROM		(VALUES
+  (int4range( 0,  5, '[]')),
+  (int4range( 7,  9, '[]'))
+) t(r);
+
+SELECT	range_agg(r, false, false)
+FROM		(VALUES
+  (int4range( 0,  5, '[]')),
+  (int4range( 5,  9, '[]'))
+) t(r);
+
+SELECT	range_agg(r, true, true)
+FROM		(VALUES
+  (int4range( 0,  9, '[]')),
+  (int4range(10, 15, '[]')),
+  (int4range(20, 26, '[]')),
+  (int4range(26, 29, '[]'))
+) t(r);
+
+-- It combines with UNNEST
+-- to implement the temporal database "coalesce" function
+-- (see Snodgrass 6.5.2):
+SELECT  room_id, t2.booked_during
+FROM    (
+          SELECT  room_id, range_agg(booked_during, true, true) AS booked_during
+          FROM    reservations
+          GROUP BY room_id
+        ) AS t1,
+        UNNEST(t1.booked_during) AS t2(booked_during)
+ORDER BY room_id, booked_during
+;
