From 723d186a99a8eb6f5595713615d4ad85bc4167bc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 24 Oct 2017 23:35:27 +0200
Subject: [PATCH 2/2] brin multi-range v1

---
 src/backend/access/brin/Makefile            |   3 +-
 src/backend/access/brin/brin.c              |  36 +-
 src/backend/access/brin/brin_minmax_multi.c | 888 ++++++++++++++++++++++++++++
 src/include/catalog/pg_amop.h               |  52 ++
 src/include/catalog/pg_amproc.h             |  47 ++
 src/include/catalog/pg_opclass.h            |   4 +
 src/include/catalog/pg_opfamily.h           |   2 +
 src/include/catalog/pg_proc.h               |  10 +
 8 files changed, 1039 insertions(+), 3 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c

diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index a76d927..c87c796 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -13,6 +13,7 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = brin.o brin_pageops.o brin_revmap.o brin_tuple.o brin_xlog.o \
-       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o
+       brin_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o \
+       brin_minmax_multi.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index b3aa6d1..ab8cd05 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -449,6 +449,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			else
 			{
 				int			keyno;
+				bool	   *attnos;
+				
+				attnos = palloc0(sizeof(bool) * bdesc->bd_tupdesc->natts);
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -465,6 +468,11 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
 					Datum		add;
 
+					/* all scan keys for the attribute */
+					ScanKey	   *keys;
+					int			nkeys;
+					int			i;
+
 					/*
 					 * The collation of the scan key must match the collation
 					 * used in the index column (but only if the search is not
@@ -487,6 +495,27 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 									   CurrentMemoryContext);
 					}
 
+					/* we process all scankeys on the first lookup */
+					if (attnos[keyattno - 1])
+						continue;
+					else
+						attnos[keyattno - 1] = true;
+
+					/*
+					 * OK, collect all scan keys for this column.
+					 */
+					keys = (ScanKey *) palloc0(scan->numberOfKeys * sizeof(ScanKey));
+
+					nkeys = 0;
+					for (i = 0; i < scan->numberOfKeys; i++)
+					{
+						/* scan is for the *current* attribute, so keep it */
+						if (key->sk_attno == keyattno)
+							keys[nkeys++] = &scan->keyData[i];
+					}
+
+					Assert((nkeys > 0) && (nkeys <= scan->numberOfKeys));
+
 					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
@@ -497,15 +526,18 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					 * the range as a whole, so break out of the loop as soon
 					 * as a false return value is obtained.
 					 */
-					add = FunctionCall3Coll(&consistentFn[keyattno - 1],
+					add = FunctionCall4Coll(&consistentFn[keyattno - 1],
 											key->sk_collation,
 											PointerGetDatum(bdesc),
 											PointerGetDatum(bval),
-											PointerGetDatum(key));
+											PointerGetDatum(keys),
+											Int32GetDatum(nkeys));
 					addrange = DatumGetBool(add);
 					if (!addrange)
 						break;
 				}
+
+				pfree(attnos);
 			}
 		}
 
diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000..94d696e
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,888 @@
+/*
+ * brin_minmax_multi.c
+ *		Implementation of Multi Min/Max opclass for BRIN
+ *
+ * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/brin/brin_minmax_multi.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+
+typedef struct MinmaxMultiOpaque
+{
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+#define		MINMAX_MAX_VALUES	64
+
+typedef struct MinmaxMultiRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* maxvalues >= (2*nranges + nvalues) */
+	int		maxvalues;	/* maximum number of values in the buffer */
+	int		nranges;	/* number of ranges stored in the array */
+	int		nvalues;	/* number of values in the data array */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum 	values[FLEXIBLE_ARRAY_MEMBER];
+
+} MinmaxMultiRanges;
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno,
+							 Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the range list, allocate all the memory.
+ */
+static MinmaxMultiRanges *
+minmax_multi_init(int maxvalues)
+{
+	Size				len;
+	MinmaxMultiRanges  *ranges;
+
+	Assert(maxvalues > 0);
+	Assert(maxvalues <= 1024);	/* arbitrary limit */
+
+	/*
+	 * Allocate the range list with space for the max number of values.
+	 */
+	len = offsetof(MinmaxMultiRanges, values) + maxvalues * sizeof(Datum);
+
+	ranges = (MinmaxMultiRanges *) palloc0(len);
+
+	ranges->maxvalues = maxvalues;
+
+	SET_VARSIZE(ranges, len);
+
+	return ranges;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+typedef struct DatumRange {
+	Datum	minval;
+	Datum	maxval;
+	bool	collapsed;
+} DatumRange;
+
+static int
+compare_values(const void *a, const void *b, void *arg)
+{
+	Datum va = *(Datum *)a;
+	Datum vb = *(Datum *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, va, vb);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, vb, va);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+static int
+compare_ranges(const void *a, const void *b, void *arg)
+{
+	DatumRange ra = *(DatumRange *)a;
+	DatumRange rb = *(DatumRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra.minval, rb.minval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb.minval, ra.minval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+static void
+print_range(char * label, int numvalues, Datum *values)
+{
+	int idx;
+	StringInfoData str;
+
+	initStringInfo(&str);
+
+	idx = 0;
+	while (idx < 2*ranges->nranges)
+	{
+		if (idx == 0)
+			appendStringInfoString(&str, "RANGES: [");
+		else
+			appendStringInfoString(&str, ", ");
+
+		appendStringInfo(&str, "%d => [%.9f, %.9f]", idx/2, DatumGetFloat8(values[idx]), DatumGetFloat8(values[idx+1]));
+
+		idx += 2;
+	}
+
+	if (ranges->nranges > 0)
+		appendStringInfoString(&str, "]");
+
+	if ((ranges->nranges > 0) && (ranges->nvalues > 0))
+		appendStringInfoString(&str, " ");
+
+	while (idx < 2*ranges->nranges + ranges->nvalues)
+	{
+		if (idx == 2*ranges->nranges)
+			appendStringInfoString(&str, "VALUES: [");
+		else
+			appendStringInfoString(&str, ", ");
+
+		appendStringInfo(&str, "%.9f", DatumGetFloat8(values[idx]));
+
+		idx++;
+	}
+
+	if (ranges->nvalues > 0)
+		appendStringInfoString(&str, "]");
+
+	elog(WARNING, "%s : %s", label, str.data);
+
+	resetStringInfo(&str);
+	pfree(str.data);
+}
+*/
+
+/*
+ * minmax_multi_contains_value
+ * 		See if the new value is already contained in the range list.
+ */
+static bool
+minmax_multi_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Oid typid,
+							MinmaxMultiRanges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpFn;
+
+	/*
+	 * First inspect the ranges, if there are any. We first check the whole
+	 * range, and only when there's still a chance of getting a match we
+	 * inspect the individual ranges.
+	 */
+	if (ranges->nranges > 0)
+	{
+		Datum	compar;
+		bool	match = true;
+
+		Datum	minvalue = ranges->values[0];
+		Datum	maxvalue = ranges->values[2*ranges->nranges - 1];
+
+		/*
+		 * Otherwise, need to compare the new value with boundaries of all
+		 * the ranges. First check if it's less than the absolute minimum,
+		 * which is the first value in the array.
+		 */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTLessStrategyNumber);
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+		/* smaller than the smallest value in the range list */
+		if (DatumGetBool(compar))
+			match = false;
+
+		/*
+		 * And now compare it to the existing maximum (last value in the
+		 * data array). But only if we haven't already ruled out a possible
+		 * match in the minvalue check.
+		 */
+		if (match)
+		{
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			if (DatumGetBool(compar))
+				match = false;
+		}
+
+		/*
+		 * So it's in the general range, but is it actually covered by any
+		 * of the ranges? Repeat the check for each range.
+		 */
+		for (i = 0; i < ranges->nranges && match; i++)
+		{
+			/* copy the min/max values from the ranges */
+			minvalue = ranges->values[2*i];
+			maxvalue = ranges->values[2*i+1];
+
+			/*
+			 * Otherwise, need to compare the new value with boundaries of all
+			 * the ranges. First check if it's less than the absolute minimum,
+			 * which is the first value in the array.
+			 */
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTLessStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, minvalue);
+
+			/* smaller than the smallest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+												 BTGreaterStrategyNumber);
+			compar = FunctionCall2Coll(cmpFn, colloid, newval, maxvalue);
+
+			/* larger than the largest value in this range */
+			if (DatumGetBool(compar))
+				continue;
+
+			/* hey, we found a matching row */
+			return true;
+		}
+	}
+
+	/* so we're done with the ranges, now let's inspect the exact values */
+	for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++)
+	{
+		Datum compar;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid,
+											 BTEqualStrategyNumber);
+
+		compar = FunctionCall2Coll(cmpFn, colloid, newval, ranges->values[i]);
+
+		/* found an exact match */
+		if (DatumGetBool(compar))
+			return true;
+	}
+
+	/* the value is not covered by this BRIN tuple */
+	return false;
+}
+
+
+/*
+ * minmax_multi_add_value
+ * 		See if the new value is already contained in the range list.
+ */
+static void
+minmax_multi_add_value(BrinDesc *bdesc, Oid colloid,
+					   AttrNumber attno, Form_pg_attribute attr,
+					   MinmaxMultiRanges *ranges, Datum newval)
+{
+	int			i;
+
+	/* context for sorting */
+	compare_context cxt;
+
+	Assert(ranges->maxvalues >= 2*ranges->nranges + ranges->nvalues);
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * If we get duplicates, it doesn't matter as we'll deduplicate the
+	 * values later.
+	 */
+	if (ranges->maxvalues > 2*ranges->nranges + ranges->nvalues)
+	{
+		ranges->values[2*ranges->nranges + ranges->nvalues] = newval;
+		ranges->nvalues++;
+		return;
+	}
+
+	/*
+	 * There's not enough space, so try deduplicating the values array,
+	 * including the new value.
+	 *
+	 * XXX maybe try deduplicating using memcmp first, instead of using
+	 * the (possibly) fairly complex/expensive comparator.
+	 *
+	 * XXX The if is somewhat unnecessary, because nvalues is always >= 0
+	 * so we do this always.
+	 */
+	if (ranges->nvalues >= 0)
+	{
+		FmgrInfo   *cmpFn;
+		int			nvalues = ranges->nvalues + 1;	/* space for newval */
+		Datum	   *values = palloc(sizeof(Datum) * nvalues);
+		int			idx;
+		DatumRange *ranges_tmp;
+		int			nranges;
+		int			count;
+
+		/* sort the values */
+		cxt.colloid = colloid;
+		cxt.cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												 BTLessStrategyNumber);
+
+		/* copy the existing value and the new value too */
+		memcpy(values, &ranges->values[2*ranges->nranges], ranges->nvalues * sizeof(Datum));
+		values[ranges->nvalues] = newval;
+
+		/* the actual sort of all the values */
+		qsort_arg(values, nvalues, sizeof(Datum), compare_values, (void *) &cxt);
+
+		/* equality for duplicate detection */
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												 BTEqualStrategyNumber);
+
+		/* keep the first value */
+		idx = 1;
+		for (i = 1; i < nvalues; i++)
+		{
+			Datum compar;
+
+			/* is this a new value (different from the previous one)? */
+			compar = FunctionCall2Coll(cmpFn, colloid, values[i-1], values[i]);
+
+			/* not equal, we have to store it */
+			if (!DatumGetBool(compar))
+				values[idx++] = values[i];
+		}
+
+		/*
+		 * Have we managed to reduce the number of values? if yes, we can just
+		 * copy it back and we're done.
+		 */
+		if (idx < nvalues)
+		{
+			memcpy(&ranges->values[2*ranges->nranges], values, idx * sizeof(Datum));
+			ranges->nvalues = idx;
+			pfree(values);
+			return;
+		}
+
+		Assert(idx == nvalues);
+
+		/*
+		 * Nope, that didn't work, we have to merge some of the ranges. To do
+		 * that we'll turn the values to "collapsed" ranges (min==max), and
+		 * then merge a bunch of "closest ranges to cut the space requirements
+		 * in half.
+		 *
+		 * XXX Do a merge sort, instead of just using qsort.
+		 */
+		nranges = (ranges->nranges + nvalues);
+		ranges_tmp = palloc0(sizeof(DatumRange) * nranges);
+
+		idx = 0;
+
+		/* ranges */
+		for (i = 0; i < ranges->nranges; i++)
+		{
+			ranges_tmp[idx].minval = ranges->values[2*i];
+			ranges_tmp[idx].maxval = ranges->values[2*i+1];
+			ranges_tmp[idx].collapsed = false;
+			idx++;
+		}
+
+		/* values as collapsed ranges */
+		for (i = 0; i < nvalues; i++)
+		{
+			ranges_tmp[idx].minval = values[i];
+			ranges_tmp[idx].maxval = values[i];
+			ranges_tmp[idx].collapsed = true;
+			idx++;
+		}
+
+		Assert(idx == nranges);
+
+		/* sort the ranges */
+		qsort_arg(ranges_tmp, nranges, sizeof(DatumRange), compare_ranges, (void *) &cxt);
+
+		/* Now combine as many ranges until the number of values to store
+		 * gets to half of MINMAX_MAX_VALUES. The collapsed ranges will be
+		 * stored as a single value.
+		 */
+		count = ranges->nranges * 2 + nvalues;
+
+		while (count > MINMAX_MAX_VALUES/2)
+		{
+			int		minidx = 0;
+			double	mindistance = DatumGetFloat8(ranges_tmp[1].minval) - DatumGetFloat8(ranges_tmp[0].maxval);
+
+			/* pick the two closest ranges */
+			for (i = 1; i < (nranges-1); i++)
+			{
+				double	distance = DatumGetFloat8(ranges_tmp[i+1].minval) - DatumGetFloat8(ranges_tmp[i-1].maxval);
+				if (distance < mindistance)
+				{
+					mindistance = distance;
+					minidx = i;
+				}
+			}
+
+			/*
+			 * Update the count of Datum values we need to store, depending
+			 * on what type of ranges we merged.
+			 *
+			 * 2 - when both ranges are 'regular'
+			 * 1 - when regular + collapsed
+			 * 0 - when both collapsed
+			 */
+			if (!ranges_tmp[minidx].collapsed && !ranges_tmp[minidx+1].collapsed)	/* both regular */
+				count -= 2;
+			else if (!ranges_tmp[minidx].collapsed || !ranges_tmp[minidx+1].collapsed) /* one regular */
+				count -= 1;
+
+			/*
+			 * combine the two selected ranges, the new range is definiely
+			 * not collapsed
+			 */
+			ranges_tmp[minidx].maxval = ranges_tmp[minidx+1].maxval;
+			ranges_tmp[minidx].collapsed = false;
+
+			for (i = minidx+1; i < nranges-1; i++)
+				ranges_tmp[i] = ranges_tmp[i+1];
+
+			nranges--;
+
+			/*
+			 * we can never get zero values
+			 *
+			 * XXX Actually we should never get below (MINMAX_MAX_VALUES/2 - 1)
+			 * values or so.
+			 */
+			Assert(count > 0);
+		}
+
+		/* first copy in the regular ranges */
+		ranges->nranges = 0;
+		for (i = 0; i < nranges; i++)
+		{
+			if (!ranges_tmp[i].collapsed)
+			{
+				ranges->values[2*ranges->nranges    ] = ranges_tmp[i].minval;
+				ranges->values[2*ranges->nranges + 1] = ranges_tmp[i].maxval;
+				ranges->nranges++;
+			}
+		}
+
+		/* now copy in the collapsed ones */
+		ranges->nvalues = 0;
+		for (i = 0; i < nranges; i++)
+		{
+			if (ranges_tmp[i].collapsed)
+			{
+				ranges->values[2*ranges->nranges + ranges->nvalues] = ranges_tmp[i].minval;
+				ranges->nvalues++;
+			}
+		}
+
+		pfree(ranges_tmp);
+		pfree(values);
+	}
+}
+
+
+Datum
+brin_minmax_multi_opcinfo(PG_FUNCTION_ARGS)
+{
+	BrinOpcInfo *result;
+
+	/*
+	 * opaque->strategy_procinfos is initialized lazily; here it is set to
+	 * all-uninitialized by palloc0 which sets fn_oid to InvalidOid.
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(MinmaxMultiOpaque));
+	result->oi_nstored = 1;
+	result->oi_opaque = (MinmaxMultiOpaque *)
+		MAXALIGN((char *) result + SizeofBrinOpcInfo(1));
+	result->oi_typcache[0] = lookup_type_cache(BYTEAOID, 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+
+/*
+ * Examine the given index tuple (which contains partial status of a certain
+ * page range) by comparing it to the given value that comes from another heap
+ * tuple.  If the new value is outside the min/max range specified by the
+ * existing tuple values, update the index tuple and return true.  Otherwise,
+ * return false and do not modify in this case.
+ */
+Datum
+brin_minmax_multi_add_value(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	Datum		newval = PG_GETARG_DATUM(2);
+	bool		isnull = PG_GETARG_DATUM(3);
+	Oid			colloid = PG_GET_COLLATION();
+	bool		updated = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	MinmaxMultiRanges *ranges;
+
+	/*
+	 * If the new value is null, we record that we saw it if it's the first
+	 * one; otherwise, there's nothing to do.
+	 */
+	if (isnull)
+	{
+		if (column->bv_hasnulls)
+			PG_RETURN_BOOL(false);
+
+		column->bv_hasnulls = true;
+		PG_RETURN_BOOL(true);
+	}
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * If this is the first non-null value, we need to initialize the range
+	 * list. Otherwise just extract the existing range list from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		ranges = minmax_multi_init(MINMAX_MAX_VALUES);
+		column->bv_values[0] = PointerGetDatum(ranges);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		ranges = (MinmaxMultiRanges *) DatumGetPointer(column->bv_values[0]);
+
+	/*
+	 * If the new value is already covered by the existing values (or ranges)
+	 * in the BRIN tuple, we're done. We can't really exit when we just
+	 * created the ranges.
+	 */
+	if (minmax_multi_contains_value(bdesc, colloid, attno, attr->atttypid, ranges, newval))
+		PG_RETURN_BOOL(updated);
+
+	/* */
+	minmax_multi_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Given an index tuple corresponding to a certain page range and a scan key,
+ * return whether the scan key is consistent with the index tuple's min/max
+ * values.  Return true if so, false otherwise.
+ */
+Datum
+brin_minmax_multi_consistent(PG_FUNCTION_ARGS)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
+	Oid			colloid = PG_GET_COLLATION(),
+				subtype;
+	AttrNumber	attno;
+	Datum		value;
+	FmgrInfo   *finfo;
+	MinmaxMultiRanges	*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+
+	/*
+	 * First check if there are any IS NULL scan keys, and if we're
+	 * violating them. In that case we can terminate early, without
+	 * inspecting the ranges.
+	 */
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		Assert(key->sk_attno == column->bv_attno);
+
+		/* handle IS NULL/IS NOT NULL tests */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (key->sk_flags & SK_SEARCHNULL)
+			{
+				if (column->bv_allnulls || column->bv_hasnulls)
+					continue;	/* this key is fine, continue */
+
+				PG_RETURN_BOOL(false);
+			}
+
+			/*
+			 * For IS NOT NULL, we can only skip ranges that are known to have
+			 * only nulls.
+			 */
+			if (key->sk_flags & SK_SEARCHNOTNULL)
+			{
+				if (column->bv_allnulls)
+					PG_RETURN_BOOL(false);
+
+				continue;
+			}
+
+			/*
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
+			 */
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	/* if the range is all empty, it cannot possibly be consistent */
+	if (column->bv_allnulls)
+		PG_RETURN_BOOL(false);
+
+	ranges = (MinmaxMultiRanges *) DatumGetPointer(column->bv_values[0]);
+
+	/* inspect the ranges, and for each one evaluate the scan keys */
+	for (rangeno = 0; rangeno < ranges->nranges; rangeno++)
+	{
+		Datum	minval = ranges->values[2*rangeno];
+		Datum	maxval = ranges->values[2*rangeno+1];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* first value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, minval, value);
+					break;
+
+				case BTEqualStrategyNumber:
+				{
+					Datum		compar;
+					FmgrInfo   *cmpFn;
+
+					/* by default this range does not match */
+					matches = false;
+
+					/*
+					 * Otherwise, need to compare the new value with boundaries of all
+					 * the ranges. First check if it's less than the absolute minimum,
+					 * which is the first value in the array.
+					 */
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTLessStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, minval);
+
+					/* smaller than the smallest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   BTGreaterStrategyNumber);
+					compar = FunctionCall2Coll(cmpFn, colloid, value, maxval);
+
+					/* larger than the largest value in this range */
+					if (DatumGetBool(compar))
+						break;
+
+					/* haven't managed to eliminate this range, so consider it matching */
+					matches = true;
+
+					break;
+				}
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					/* last value from the array */
+					matches = FunctionCall2Coll(finfo, colloid, maxval, value);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	/* and now inspect the values */
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		Datum	val = ranges->values[2*ranges->nranges + i];
+
+		/* assume the range is matching, and we'll try to prove otherwise */
+		bool	matching = true;
+
+		for (keyno = 0; keyno < nkeys; keyno++)
+		{
+			Datum	matches;
+			ScanKey	key = keys[keyno];
+
+			/* we've already dealt with NULL keys at the beginning */
+			if (key->sk_flags & SK_ISNULL)
+				continue;
+
+			attno = key->sk_attno;
+			subtype = key->sk_subtype;
+			value = key->sk_argument;
+			switch (key->sk_strategy)
+			{
+				case BTLessStrategyNumber:
+				case BTLessEqualStrategyNumber:
+				case BTEqualStrategyNumber:
+				case BTGreaterEqualStrategyNumber:
+				case BTGreaterStrategyNumber:
+				
+					finfo = minmax_multi_get_strategy_procinfo(bdesc, attno, subtype,
+															   key->sk_strategy);
+					matches = FunctionCall2Coll(finfo, colloid, value, val);
+					break;
+
+				default:
+					/* shouldn't happen */
+					elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+					matches = 0;
+					break;
+			}
+
+			/* the range has to match all the scan keys */
+			matching &= DatumGetBool(matches);
+
+			/* once we find a non-matching key, we're done */
+			if (! matching)
+				break;
+		}
+
+		/* have we found a range matching all scan keys? if yes, we're
+		 * done */
+		if (matching)
+			PG_RETURN_DATUM(BoolGetDatum(true));
+	}
+
+	PG_RETURN_DATUM(BoolGetDatum(false));
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ */
+Datum
+brin_minmax_multi_union(PG_FUNCTION_ARGS)
+{
+	/* FIXME */
+	elog(WARNING, "brin_minmax_multi_union not implemented");
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors inclusion_get_strategy_procinfo; see notes
+ * there.  If changes are made here, see that function too.
+ */
+static FmgrInfo *
+minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype,
+							 uint16 strategynum)
+{
+	MinmaxMultiOpaque *opaque;
+
+	Assert(strategynum >= 1 &&
+		   strategynum <= BTMaxStrategyNumber);
+
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * We cache the procedures for the previous subtype in the opaque struct,
+	 * to avoid repetitive syscache lookups.  If the subtype changed,
+	 * invalidate all the cached entries.
+	 */
+	if (opaque->cached_subtype != subtype)
+	{
+		uint16		i;
+
+		for (i = 1; i <= BTMaxStrategyNumber; i++)
+			opaque->strategy_procinfos[i - 1].fn_oid = InvalidOid;
+		opaque->cached_subtype = subtype;
+	}
+
+	if (opaque->strategy_procinfos[strategynum - 1].fn_oid == InvalidOid)
+	{
+		Form_pg_attribute attr;
+		HeapTuple	tuple;
+		Oid			opfamily,
+					oprid;
+		bool		isNull;
+
+		opfamily = bdesc->bd_index->rd_opfamily[attno - 1];
+		attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+		tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily),
+								ObjectIdGetDatum(attr->atttypid),
+								ObjectIdGetDatum(subtype),
+								Int16GetDatum(strategynum));
+
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+				 strategynum, attr->atttypid, subtype, opfamily);
+
+		oprid = DatumGetObjectId(SysCacheGetAttr(AMOPSTRATEGY, tuple,
+												 Anum_pg_amop_amopopr, &isNull));
+		ReleaseSysCache(tuple);
+		Assert(!isNull && RegProcedureIsValid(oprid));
+
+		fmgr_info_cxt(get_opcode(oprid),
+					  &opaque->strategy_procinfos[strategynum - 1],
+					  bdesc->bd_context);
+	}
+
+	return &opaque->strategy_procinfos[strategynum - 1];
+}
diff --git a/src/include/catalog/pg_amop.h b/src/include/catalog/pg_amop.h
index ef5b692..2052ba7 100644
--- a/src/include/catalog/pg_amop.h
+++ b/src/include/catalog/pg_amop.h
@@ -1007,6 +1007,12 @@ DATA(insert (	4070	701  700 2 s	  1134	  3580 0 ));
 DATA(insert (	4070	701  700 3 s	  1130	  3580 0 ));
 DATA(insert (	4070	701  700 4 s	  1135	  3580 0 ));
 DATA(insert (	4070	701  700 5 s	  1133	  3580 0 ));
+DATA(insert (	4005	701  701 1 s	   672	  3580 0 ));
+DATA(insert (	4005	701  701 2 s	   673	  3580 0 ));
+DATA(insert (	4005	701  701 3 s	   670	  3580 0 ));
+DATA(insert (	4005	701  701 4 s	   675	  3580 0 ));
+DATA(insert (	4005	701  701 5 s	   674	  3580 0 ));
+/* minmax multi (float8) */
 DATA(insert (	4070	701  701 1 s	   672	  3580 0 ));
 DATA(insert (	4070	701  701 2 s	   673	  3580 0 ));
 DATA(insert (	4070	701  701 3 s	   670	  3580 0 ));
@@ -1127,6 +1133,52 @@ DATA(insert (	4059   1184 1184 2 s	  1323	  3580 0 ));
 DATA(insert (	4059   1184 1184 3 s	  1320	  3580 0 ));
 DATA(insert (	4059   1184 1184 4 s	  1325	  3580 0 ));
 DATA(insert (	4059   1184 1184 5 s	  1324	  3580 0 ));
+/* minmax multi (timestamp, timestamptz) */
+DATA(insert (	4006   1114 1114 1 s	  2062	  3580 0 ));
+DATA(insert (	4006   1114 1114 2 s	  2063	  3580 0 ));
+DATA(insert (	4006   1114 1114 3 s	  2060	  3580 0 ));
+DATA(insert (	4006   1114 1114 4 s	  2065	  3580 0 ));
+DATA(insert (	4006   1114 1114 5 s	  2064	  3580 0 ));
+DATA(insert (	4006   1114 1082 1 s	  2371	  3580 0 ));
+DATA(insert (	4006   1114 1082 2 s	  2372	  3580 0 ));
+DATA(insert (	4006   1114 1082 3 s	  2373	  3580 0 ));
+DATA(insert (	4006   1114 1082 4 s	  2374	  3580 0 ));
+DATA(insert (	4006   1114 1082 5 s	  2375	  3580 0 ));
+DATA(insert (	4006   1114 1184 1 s	  2534	  3580 0 ));
+DATA(insert (	4006   1114 1184 2 s	  2535	  3580 0 ));
+DATA(insert (	4006   1114 1184 3 s	  2536	  3580 0 ));
+DATA(insert (	4006   1114 1184 4 s	  2537	  3580 0 ));
+DATA(insert (	4006   1114 1184 5 s	  2538	  3580 0 ));
+DATA(insert (	4006   1082 1082 1 s	  1095	  3580 0 ));
+DATA(insert (	4006   1082 1082 2 s	  1096	  3580 0 ));
+DATA(insert (	4006   1082 1082 3 s	  1093	  3580 0 ));
+DATA(insert (	4006   1082 1082 4 s	  1098	  3580 0 ));
+DATA(insert (	4006   1082 1082 5 s	  1097	  3580 0 ));
+DATA(insert (	4006   1082 1114 1 s	  2345	  3580 0 ));
+DATA(insert (	4006   1082 1114 2 s	  2346	  3580 0 ));
+DATA(insert (	4006   1082 1114 3 s	  2347	  3580 0 ));
+DATA(insert (	4006   1082 1114 4 s	  2348	  3580 0 ));
+DATA(insert (	4006   1082 1114 5 s	  2349	  3580 0 ));
+DATA(insert (	4006   1082 1184 1 s	  2358	  3580 0 ));
+DATA(insert (	4006   1082 1184 2 s	  2359	  3580 0 ));
+DATA(insert (	4006   1082 1184 3 s	  2360	  3580 0 ));
+DATA(insert (	4006   1082 1184 4 s	  2361	  3580 0 ));
+DATA(insert (	4006   1082 1184 5 s	  2362	  3580 0 ));
+DATA(insert (	4006   1184 1082 1 s	  2384	  3580 0 ));
+DATA(insert (	4006   1184 1082 2 s	  2385	  3580 0 ));
+DATA(insert (	4006   1184 1082 3 s	  2386	  3580 0 ));
+DATA(insert (	4006   1184 1082 4 s	  2387	  3580 0 ));
+DATA(insert (	4006   1184 1082 5 s	  2388	  3580 0 ));
+DATA(insert (	4006   1184 1114 1 s	  2540	  3580 0 ));
+DATA(insert (	4006   1184 1114 2 s	  2541	  3580 0 ));
+DATA(insert (	4006   1184 1114 3 s	  2542	  3580 0 ));
+DATA(insert (	4006   1184 1114 4 s	  2543	  3580 0 ));
+DATA(insert (	4006   1184 1114 5 s	  2544	  3580 0 ));
+DATA(insert (	4006   1184 1184 1 s	  1322	  3580 0 ));
+DATA(insert (	4006   1184 1184 2 s	  1323	  3580 0 ));
+DATA(insert (	4006   1184 1184 3 s	  1320	  3580 0 ));
+DATA(insert (	4006   1184 1184 4 s	  1325	  3580 0 ));
+DATA(insert (	4006   1184 1184 5 s	  1324	  3580 0 ));
 /* bloom datetime (date, timestamp, timestamptz) */
 DATA(insert (	5038   1114 1114 1 s	  2060	  3580 0 ));
 DATA(insert (	5038   1114 1082 1 s	  2373	  3580 0 ));
diff --git a/src/include/catalog/pg_amproc.h b/src/include/catalog/pg_amproc.h
index cef4a7c..437d1fb 100644
--- a/src/include/catalog/pg_amproc.h
+++ b/src/include/catalog/pg_amproc.h
@@ -476,6 +476,13 @@ DATA(insert (	4070   701	 700  2  3384 ));
 DATA(insert (	4070   701	 700  3  3385 ));
 DATA(insert (	4070   701	 700  4  3386 ));
 
+/* minmax multi float */
+
+DATA(insert (	4005   701	 701  1  4001 ));
+DATA(insert (	4005   701	 701  2  4002 ));
+DATA(insert (	4005   701	 701  3  4003 ));
+DATA(insert (	4005   701	 701  4  4004 ));
+
 /* bloom float */
 DATA(insert (	5030   700	 700  1  5017 ));
 DATA(insert (	5030   700	 700  2  5018 ));
@@ -614,6 +621,46 @@ DATA(insert (	4059  1082	1184  2  3384 ));
 DATA(insert (	4059  1082	1184  3  3385 ));
 DATA(insert (	4059  1082	1184  4  3386 ));
 
+/* minmax multi (timestamp, timestamptz) */
+DATA(insert (	4006  1114	1114  1  4001 ));
+DATA(insert (	4006  1114	1114  2  4002 ));
+DATA(insert (	4006  1114	1114  3  4003 ));
+DATA(insert (	4006  1114	1114  4  4004 ));
+DATA(insert (	4006  1114	1184  1  4001 ));
+DATA(insert (	4006  1114	1184  2  4002 ));
+DATA(insert (	4006  1114	1184  3  4003 ));
+DATA(insert (	4006  1114	1184  4  4004 ));
+DATA(insert (	4006  1114	1082  1  4001 ));
+DATA(insert (	4006  1114	1082  2  4002 ));
+DATA(insert (	4006  1114	1082  3  4003 ));
+DATA(insert (	4006  1114	1082  4  4004 ));
+
+DATA(insert (	4006  1184	1184  1  4001 ));
+DATA(insert (	4006  1184	1184  2  4002 ));
+DATA(insert (	4006  1184	1184  3  4003 ));
+DATA(insert (	4006  1184	1184  4  4004 ));
+DATA(insert (	4006  1184	1114  1  4001 ));
+DATA(insert (	4006  1184	1114  2  4002 ));
+DATA(insert (	4006  1184	1114  3  4003 ));
+DATA(insert (	4006  1184	1114  4  4004 ));
+DATA(insert (	4006  1184	1082  1  4001 ));
+DATA(insert (	4006  1184	1082  2  4002 ));
+DATA(insert (	4006  1184	1082  3  4003 ));
+DATA(insert (	4006  1184	1082  4  4004 ));
+
+DATA(insert (	4006  1082	1082  1  4001 ));
+DATA(insert (	4006  1082	1082  2  4002 ));
+DATA(insert (	4006  1082	1082  3  4003 ));
+DATA(insert (	4006  1082	1082  4  4004 ));
+DATA(insert (	4006  1082	1114  1  4001 ));
+DATA(insert (	4006  1082	1114  2  4002 ));
+DATA(insert (	4006  1082	1114  3  4003 ));
+DATA(insert (	4006  1082	1114  4  4004 ));
+DATA(insert (	4006  1082	1184  1  4001 ));
+DATA(insert (	4006  1082	1184  2  4002 ));
+DATA(insert (	4006  1082	1184  3  4003 ));
+DATA(insert (	4006  1082	1184  4  4004 ));
+
 /* bloom datetime (date, timestamp, timestamptz) */
 DATA(insert (	5038  1114	1114  1  5017 ));
 DATA(insert (	5038  1114	1114  2  5018 ));
diff --git a/src/include/catalog/pg_opclass.h b/src/include/catalog/pg_opclass.h
index ce28469..8f43b66 100644
--- a/src/include/catalog/pg_opclass.h
+++ b/src/include/catalog/pg_opclass.h
@@ -232,6 +232,7 @@ DATA(insert (	3580	tid_minmax_ops			PGNSP PGUID 4069	27 t 27 ));
 DATA(insert (	3580	float4_minmax_ops		PGNSP PGUID 4070   700 t 700 ));
 DATA(insert (	3580	float4_bloom_ops		PGNSP PGUID 5030   700 f 700 ));
 DATA(insert (	3580	float8_minmax_ops		PGNSP PGUID 4070   701 t 701 ));
+DATA(insert (	3580	float8_minmax_multi_ops	PGNSP PGUID 4005   701 f 701 ));
 DATA(insert (	3580	float8_bloom_ops		PGNSP PGUID 5030   701 f 701 ));
 DATA(insert (	3580	abstime_minmax_ops		PGNSP PGUID 4072   702 t 702 ));
 DATA(insert (	3580	abstime_bloom_ops		PGNSP PGUID 5031   702 f 702 ));
@@ -249,10 +250,13 @@ DATA(insert (	3580	bpchar_bloom_ops		PGNSP PGUID 5036  1042 f 1042 ));
 DATA(insert (	3580	time_minmax_ops			PGNSP PGUID 4077  1083 t 1083 ));
 DATA(insert (	3580	time_bloom_ops			PGNSP PGUID 5037  1083 f 1083 ));
 DATA(insert (	3580	date_minmax_ops			PGNSP PGUID 4059  1082 t 1082 ));
+DATA(insert (	3580	date_minmax_multi_ops	PGNSP PGUID 4006  1082 f 1082 ));
 DATA(insert (	3580	date_bloom_ops			PGNSP PGUID 5038  1082 f 1082 ));
 DATA(insert (	3580	timestamp_minmax_ops	PGNSP PGUID 4059  1114 t 1114 ));
+DATA(insert (	3580	timestamp_minmax_multi_ops	PGNSP PGUID 4006  1114 f 1114 ));
 DATA(insert (	3580	timestamp_bloom_ops		PGNSP PGUID 5038  1114 f 1114 ));
 DATA(insert (	3580	timestamptz_minmax_ops	PGNSP PGUID 4059  1184 t 1184 ));
+DATA(insert (	3580	timestamptz_minmax_multi_ops	PGNSP PGUID 4006  1184 f 1184 ));
 DATA(insert (	3580	timestamptz_bloom_ops	PGNSP PGUID 5038  1184 f 1184 ));
 DATA(insert (	3580	interval_minmax_ops		PGNSP PGUID 4078  1186 t 1186 ));
 DATA(insert (	3580	interval_bloom_ops		PGNSP PGUID 5041  1186 f 1186 ));
diff --git a/src/include/catalog/pg_opfamily.h b/src/include/catalog/pg_opfamily.h
index bf9c578..90adbbb 100644
--- a/src/include/catalog/pg_opfamily.h
+++ b/src/include/catalog/pg_opfamily.h
@@ -168,6 +168,7 @@ DATA(insert OID = 5027 (	3580	text_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4058 (	3580	timetz_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 5042 (	3580	timetz_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4059 (	3580	datetime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4006 (	3580	datetime_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5038 (	3580	datetime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4062 (	3580	char_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 5022 (	3580	char_bloom_ops			PGNSP PGUID ));
@@ -179,6 +180,7 @@ DATA(insert OID = 4068 (	3580	oid_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 5028 (	3580	oid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4069 (	3580	tid_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 4070 (	3580	float_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4005 (	3580	float_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5030 (	3580	float_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4072 (	3580	abstime_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 5031 (	3580	abstime_bloom_ops		PGNSP PGUID ));
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 5852496..87e91de 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -4364,6 +4364,16 @@ DESCR("BRIN minmax support");
 DATA(insert OID = 3386 ( brin_minmax_union		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_union _null_ _null_ _null_ ));
 DESCR("BRIN minmax support");
 
+/* BRIN minmax multi */
+DATA(insert OID = 4001 ( brin_minmax_multi_opcinfo	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 1 0 2281 "2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_opcinfo _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+DATA(insert OID = 4002 ( brin_minmax_multi_add_value	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 4 0 16 "2281 2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_add_value _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+DATA(insert OID = 4003 ( brin_minmax_multi_consistent PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_consistent _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+DATA(insert OID = 4004 ( brin_minmax_multi_union		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 3 0 16 "2281 2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_union _null_ _null_ _null_ ));
+DESCR("BRIN minmax support");
+
 /* BRIN inclusion */
 DATA(insert OID = 4105 ( brin_inclusion_opcinfo PGNSP PGUID 12 1 0 0 0 f f f f t f i s 1 0 2281 "2281" _null_ _null_ _null_ _null_ _null_ brin_inclusion_opcinfo _null_ _null_ _null_ ));
 DESCR("BRIN inclusion support");
-- 
2.9.5

