pg.ddx.io  pgsql-hackers@postgresql.org mailing list archive  
help / color / mirror / Atom feed
From: Tomas Vondra <tomas.vondra@2ndquadrant.com>
To: Alvaro Herrera <alvherre@alvh.no-ip.org>
Cc: Mark Dilger <hornschnorter@gmail.com>
Cc: pgsql-hackers@postgreSQL.org
Subject: Re: WIP: BRIN multi-range indexes
Date: Mon, 5 Feb 2018 19:57:06 +0100
Message-ID: <c32f9f46-e0ca-0c02-45ec-718dd381f4be@2ndquadrant.com> (raw)
In-Reply-To: <821c4c1d-592e-8916-e323-04607b11777e@2ndquadrant.com>
References: <20180123200537.gfx5hxcp5k5hktpe@alvherre.pgsql>
	<821c4c1d-592e-8916-e323-04607b11777e@2ndquadrant.com>



On 01/23/2018 10:07 PM, Tomas Vondra wrote:
> 
> 
> On 01/23/2018 09:05 PM, Alvaro Herrera wrote:
>> This stuff sounds pretty nice.  However, have a look at this report:
>>
>> https://codecov.io/gh/postgresql-cfbot/postgresql/commit/2aa632dae3066900e15d2d42a4aad811dec11f08
>>
>> it seems to me that the new code is not tested at all.  Shouldn't you
>> add a few more tests?
>>
> 
> I have a hard time reading the report, but you're right I haven't added
> any tests for the new opclasses (bloom and minmax_multi). I agree that's
> something that needs to be addressed.
> 
>> I think 0004 should apply to unpatched master (except for the parts
>> that concern files not in master); sounds like a good candidate for
>> first apply. Then 0001, which seems mostly just refactoring. 0002 and
>> 0003 are the really interesting ones (minus the code removed by
>> 0004).
>>
> 
> That sounds like a reasonable plan. I'll reorder the patch series along
> those lines in the next few days.
> 

And here we go. Attached is a reworked patch series that moves the IS
NULL tweak to the beginning of the series, and also adds proper
regression tests both for the bloom and multi-minmax opclasses. I've
simply copied the brin.sql tests and tweaked it for the new opclasses.

I've also added a bunch of missing multi-minmax opclasses. At this point
all data types that have minmax opclass should also have multi-minmax
one, except for these types:

* bytea
* char
* name
* text
* bpchar
* bit
* varbit

The reason is that I'm not quite sure how to define the 'distance'
function, which is needed when picking ranges to merge when
building/updating the index.

BTW while working on the regression tests, I've noticed that brin.sql
fails to test a couple of minmax opclasses (e.g. abstime/reltime). Is
that intentional or is that something we should fix eventually?

regards

-- 
Tomas Vondra                  http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

  [text/x-patch] 0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch (29.3K, ../c32f9f46-e0ca-0c02-45ec-718dd381f4be@2ndquadrant.com/2-0001-Pass-all-keys-to-BRIN-consistent-function-at-once.patch)
  download | inline diff:
From bbaaf2dd5ac6fcfff37b226afa1197b02c48ce8b Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 9 Jan 2018 00:59:09 +0100
Subject: [PATCH 1/4] Pass all keys to BRIN consistent function at once

When building the bitmap, bringetbitmap was passing the scan keys
to consistent support procedure one by one. That works fine for
the simple default opclasses (minmax and inclusion), but not for
opclasses with more complicated summaries.

For example the multi-minmax opclass needs to see all the keys to
efficiently eliminate the partial ranges, otherwise it's mostly
just a more expensive minmax opclass.

So instead split the scan keys into per-attribute groups, and pass
the whole group at once.
---
 src/backend/access/brin/brin.c           |  97 +++++--
 src/backend/access/brin/brin_inclusion.c | 484 ++++++++++++++++++-------------
 src/backend/access/brin/brin_minmax.c    | 168 +++++++----
 3 files changed, 451 insertions(+), 298 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 68b3371..b568786 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -367,6 +367,9 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
+	ScanKey	  **keys;
+	int		   *nkeys;
+	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
 	bdesc = opaque->bo_bdesc;
@@ -388,6 +391,53 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	 */
 	consistentFn = palloc0(sizeof(FmgrInfo) * bdesc->bd_tupdesc->natts);
 
+	/*
+	 * Make room for per-attribute lists of scan keys that we'll pass to the
+	 * consistent support procedure.
+	 */
+	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+
+	/*
+	 * Preprocess the scan keys - split them into per-attribute arrays.
+	 */
+	for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+	{
+		ScanKey		key = &scan->keyData[keyno];
+		AttrNumber	keyattno = key->sk_attno;
+
+		/*
+		 * The collation of the scan key must match the collation
+		 * used in the index column (but only if the search is not
+		 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
+		 * this index ...
+		 */
+		Assert((key->sk_flags & SK_ISNULL) ||
+			   (key->sk_collation ==
+				TupleDescAttr(bdesc->bd_tupdesc,
+							  keyattno - 1)->attcollation));
+
+		/* First time we see this index attribute, so init as needed. */
+		if (!keys[keyattno-1])
+		{
+			FmgrInfo   *tmp;
+
+			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			/* First time this column, so look up consistent function */
+			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+
+			tmp = index_getprocinfo(idxRel, keyattno,
+									BRIN_PROCNUM_CONSISTENT);
+			fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
+						   CurrentMemoryContext);
+		}
+
+		/* Add key to the per-attribute array. */
+		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+		nkeys[keyattno - 1]++;
+	}
+
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
 	dtup = brin_new_memtuple(bdesc);
 
@@ -448,7 +498,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 			}
 			else
 			{
-				int			keyno;
+				int		attno;
 
 				/*
 				 * Compare scan keys with summary values stored for the range.
@@ -458,34 +508,26 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 				 * no keys.
 				 */
 				addrange = true;
-				for (keyno = 0; keyno < scan->numberOfKeys; keyno++)
+				for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++)
 				{
-					ScanKey		key = &scan->keyData[keyno];
-					AttrNumber	keyattno = key->sk_attno;
-					BrinValues *bval = &dtup->bt_columns[keyattno - 1];
+					BrinValues *bval;
 					Datum		add;
+					Oid			collation;
+
+					/* skip attributes without any san keys */
+					if (!nkeys[attno - 1])
+						continue;
+
+					bval = &dtup->bt_columns[attno - 1];
+
+					Assert((nkeys[attno - 1] > 0) &&
+						   (nkeys[attno - 1] <= scan->numberOfKeys));
 
 					/*
-					 * The collation of the scan key must match the collation
-					 * used in the index column (but only if the search is not
-					 * IS NULL/ IS NOT NULL).  Otherwise we shouldn't be using
-					 * this index ...
+					 * Collation from the first key (has to be the same for
+					 * all keys for the same attribue).
 					 */
-					Assert((key->sk_flags & SK_ISNULL) ||
-						   (key->sk_collation ==
-							TupleDescAttr(bdesc->bd_tupdesc,
-										  keyattno - 1)->attcollation));
-
-					/* First time this column? look up consistent function */
-					if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
-					{
-						FmgrInfo   *tmp;
-
-						tmp = index_getprocinfo(idxRel, keyattno,
-												BRIN_PROCNUM_CONSISTENT);
-						fmgr_info_copy(&consistentFn[keyattno - 1], tmp,
-									   CurrentMemoryContext);
-					}
+					collation = keys[attno - 1][0]->sk_collation;
 
 					/*
 					 * Check whether the scan key is consistent with the page
@@ -497,11 +539,12 @@ 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],
-											key->sk_collation,
+					add = FunctionCall4Coll(&consistentFn[attno - 1],
+											collation,
 											PointerGetDatum(bdesc),
 											PointerGetDatum(bval),
-											PointerGetDatum(key));
+											PointerGetDatum(keys[attno - 1]),
+											Int32GetDatum(nkeys[attno - 1]));
 					addrange = DatumGetBool(add);
 					if (!addrange)
 						break;
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index 2542877..fb7f2b0 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -248,7 +248,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION(),
 				subtype;
 	Datum		unionval;
@@ -256,235 +257,300 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Datum		query;
 	FmgrInfo   *finfo;
 	Datum		result;
+	int			keyno;
+	bool		matches;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* Handle IS NULL/IS NOT NULL tests. */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * 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++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
-		{
-			if (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
-			PG_RETURN_BOOL(false);
-		}
+		ScanKey	key = keys[keyno];
 
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
-	}
+		Assert(key->sk_attno == column->bv_attno);
 
-	/* If it is all nulls, it cannot possibly be consistent. */
-	if (column->bv_allnulls)
-		PG_RETURN_BOOL(false);
+		/* 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 */
 
-	/* It has to be checked, if it contains elements that are not mergeable. */
-	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
-		PG_RETURN_BOOL(true);
+				PG_RETURN_BOOL(false);
+			}
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	query = key->sk_argument;
-	unionval = column->bv_values[INCLUSION_UNION];
-	switch (key->sk_strategy)
-	{
 			/*
-			 * Placement strategies
-			 *
-			 * These are implemented by logically negating the result of the
-			 * converse placement operator; for this to work, the converse
-			 * operator must be part of the opclass.  An error will be thrown
-			 * by inclusion_get_strategy_procinfo() if the required strategy
-			 * is not part of the opclass.
-			 *
-			 * These all return false if either argument is empty, so there is
-			 * no need to check for empty elements.
+			 * 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);
 
-		case RTLeftStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverRightStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTOverLeftStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTRightStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTOverRightStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTLeftStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTRightStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverLeftStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTBelowStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverAboveStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTOverBelowStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAboveStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTOverAboveStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTBelowStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		case RTAboveStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverBelowStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
+				continue;
+			}
 
 			/*
-			 * Overlap and contains strategies
-			 *
-			 * These strategies are simple enough that we can simply call the
-			 * operator and return its result.  Empty elements don't change
-			 * the result.
+			 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
+			 * operators are strict and return false.
 			 */
+			PG_RETURN_BOOL(false);
+		}
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
+	}
 
-		case RTOverlapStrategyNumber:
-		case RTContainsStrategyNumber:
-		case RTOldContainsStrategyNumber:
-		case RTContainsElemStrategyNumber:
-		case RTSubStrategyNumber:
-		case RTSubEqualStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													key->sk_strategy);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
-
-			/*
-			 * Contained by strategies
-			 *
-			 * We cannot just call the original operator for the contained by
-			 * strategies because some elements can be contained even though
-			 * the union is not; instead we use the overlap operator.
-			 *
-			 * We check for empty elements separately as they are not merged
-			 * to the union but contained by everything.
-			 */
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
+		PG_RETURN_BOOL(false);
 
-		case RTContainedByStrategyNumber:
-		case RTOldContainedByStrategyNumber:
-		case RTSuperStrategyNumber:
-		case RTSuperEqualStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverlapStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
 
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+	/* It has to be checked, if it contains elements that are not mergeable. */
+	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
+		PG_RETURN_BOOL(true);
 
-			/*
-			 * Adjacent strategy
-			 *
-			 * We test for overlap first but to be safe we need to call the
-			 * actual adjacent operator also.
-			 *
-			 * An empty element cannot be adjacent to any other, so there is
-			 * no need to check for it.
-			 */
+	matches = true;
 
-		case RTAdjacentStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTOverlapStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
 
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTAdjacentStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_DATUM(result);
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		if (key->sk_flags & SK_ISNULL)
+			continue;
 
-			/*
-			 * Basic comparison strategies
-			 *
-			 * It is straightforward to support the equality strategies with
-			 * the contains operator.  Generally, inequality strategies do not
-			 * make much sense for the types which will be used with the
-			 * inclusion BRIN family of opclasses, but is possible to
-			 * implement them with logical negation of the left-of and
-			 * right-of operators.
-			 *
-			 * NB: These strategies cannot be used with geometric datatypes
-			 * that use comparison of areas!  The only exception is the "same"
-			 * strategy.
-			 *
-			 * Empty elements are considered to be less than the others.  We
-			 * cannot use the empty support function to check the query is an
-			 * empty element, because the query can be another data type than
-			 * the empty support function argument.  So we will return true,
-			 * if there is a possibility that empty elements will change the
-			 * result.
-			 */
+		attno = key->sk_attno;
+		subtype = key->sk_subtype;
+		query = key->sk_argument;
+		unionval = column->bv_values[INCLUSION_UNION];
+		switch (key->sk_strategy)
+		{
+				/*
+				* Placement strategies
+				*
+				* These are implemented by logically negating the result of the
+				* converse placement operator; for this to work, the converse
+				* operator must be part of the opclass.  An error will be thrown
+				* by inclusion_get_strategy_procinfo() if the required strategy
+				* is not part of the opclass.
+				*
+				* These all return false if either argument is empty, so there is
+				* no need to check for empty elements.
+				*/
+
+			case RTLeftStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverRightStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTOverLeftStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTRightStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTOverRightStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTLeftStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTRightStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverLeftStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTBelowStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverAboveStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTOverBelowStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTAboveStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTOverAboveStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTBelowStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			case RTAboveStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverBelowStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+				/*
+				* Overlap and contains strategies
+				*
+				* These strategies are simple enough that we can simply call the
+				* operator and return its result.  Empty elements don't change
+				* the result.
+				*/
+
+			case RTOverlapStrategyNumber:
+			case RTContainsStrategyNumber:
+			case RTOldContainsStrategyNumber:
+			case RTContainsElemStrategyNumber:
+			case RTSubStrategyNumber:
+			case RTSubEqualStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														key->sk_strategy);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = DatumGetBool(result);
+				break;
+
+				/*
+				* Contained by strategies
+				*
+				* We cannot just call the original operator for the contained by
+				* strategies because some elements can be contained even though
+				* the union is not; instead we use the overlap operator.
+				*
+				* We check for empty elements separately as they are not merged
+				* to the union but contained by everything.
+				*/
+
+			case RTContainedByStrategyNumber:
+			case RTOldContainedByStrategyNumber:
+			case RTSuperStrategyNumber:
+			case RTSuperEqualStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverlapStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				if (DatumGetBool(result))
+					matches = true;
+				else
+					matches = DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+
+				break;
+
+				/*
+				* Adjacent strategy
+				*
+				* We test for overlap first but to be safe we need to call the
+				* actual adjacent operator also.
+				*
+				* An empty element cannot be adjacent to any other, so there is
+				* no need to check for it.
+				*/
+
+			case RTAdjacentStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTOverlapStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				if (DatumGetBool(result))
+					matches = (true);
+				else
+				{
+
+					finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+															RTAdjacentStrategyNumber);
+					result = FunctionCall2Coll(finfo, colloid, unionval, query);
+					matches = DatumGetBool(result);
+				}
+
+				break;
+
+				/*
+				* Basic comparison strategies
+				*
+				* It is straightforward to support the equality strategies with
+				* the contains operator.  Generally, inequality strategies do not
+				* make much sense for the types which will be used with the
+				* inclusion BRIN family of opclasses, but is possible to
+				* implement them with logical negation of the left-of and
+				* right-of operators.
+				*
+				* NB: These strategies cannot be used with geometric datatypes
+				* that use comparison of areas!  The only exception is the "same"
+				* strategy.
+				*
+				* Empty elements are considered to be less than the others.  We
+				* cannot use the empty support function to check the query is an
+				* empty element, because the query can be another data type than
+				* the empty support function argument.  So we will return true,
+				* if there is a possibility that empty elements will change the
+				* result.
+				*/
+
+			case RTLessStrategyNumber:
+			case RTLessEqualStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTRightStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				if (!DatumGetBool(result))
+					matches = (true);
+				else
+					matches = DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+				break;
+
+			case RTSameStrategyNumber:
+			case RTEqualStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTContainsStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				if (DatumGetBool(result))
+					matches = (true);
+				else
+					matches = DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+				break;
+
+			case RTGreaterEqualStrategyNumber:
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTLeftStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				if (!DatumGetBool(result))
+					matches = (true);
+				else
+					matches = DatumGetBool(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
+				break;
+
+			case RTGreaterStrategyNumber:
+				/* no need to check for empty elements */
+				finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
+														RTLeftStrategyNumber);
+				result = FunctionCall2Coll(finfo, colloid, unionval, query);
+				matches = (!DatumGetBool(result));
+				break;
+
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = (false);
+		}
 
-		case RTLessStrategyNumber:
-		case RTLessEqualStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTRightStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
-
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
-
-		case RTSameStrategyNumber:
-		case RTEqualStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTContainsStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			if (DatumGetBool(result))
-				PG_RETURN_BOOL(true);
-
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
-
-		case RTGreaterEqualStrategyNumber:
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTLeftStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			if (!DatumGetBool(result))
-				PG_RETURN_BOOL(true);
-
-			PG_RETURN_DATUM(column->bv_values[INCLUSION_CONTAINS_EMPTY]);
-
-		case RTGreaterStrategyNumber:
-			/* no need to check for empty elements */
-			finfo = inclusion_get_strategy_procinfo(bdesc, attno, subtype,
-													RTLeftStrategyNumber);
-			result = FunctionCall2Coll(finfo, colloid, unionval, query);
-			PG_RETURN_BOOL(!DatumGetBool(result));
-
-		default:
-			/* shouldn't happen */
-			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			PG_RETURN_BOOL(false);
+		if (!matches)
+			break;
 	}
+
+	PG_RETURN_BOOL(matches);
 }
 
 /*
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index 0f6aa33..e58fd16 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -147,86 +147,130 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 {
 	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
 	BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1);
-	ScanKey		key = (ScanKey) PG_GETARG_POINTER(2);
+	ScanKey	   *keys = (ScanKey *) PG_GETARG_POINTER(2);
+	int			nkeys = PG_GETARG_INT32(3);
 	Oid			colloid = PG_GET_COLLATION(),
 				subtype;
 	AttrNumber	attno;
 	Datum		value;
 	Datum		matches;
 	FmgrInfo   *finfo;
+	int			keyno;
+	bool		regular_keys = false;
 
-	Assert(key->sk_attno == column->bv_attno);
-
-	/* handle IS NULL/IS NOT NULL tests */
-	if (key->sk_flags & SK_ISNULL)
+	/*
+	 * 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++)
 	{
-		if (key->sk_flags & SK_SEARCHNULL)
+		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 (column->bv_allnulls || column->bv_hasnulls)
-				PG_RETURN_BOOL(true);
+			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);
 		}
-
-		/*
-		 * For IS NOT NULL, we can only skip ranges that are known to have
-		 * only nulls.
-		 */
-		if (key->sk_flags & SK_SEARCHNOTNULL)
-			PG_RETURN_BOOL(!column->bv_allnulls);
-
-		/*
-		 * Neither IS NULL nor IS NOT NULL was used; assume all indexable
-		 * operators are strict and return false.
-		 */
-		PG_RETURN_BOOL(false);
+		else
+			/* note we have regular (non-NULL) scan keys */
+			regular_keys = true;
 	}
 
-	/* if the range is all empty, it cannot possibly be consistent */
-	if (column->bv_allnulls)
+	/*
+	 * If the page range is all nulls, it cannot possibly be consistent if
+	 * there are some regular scan keys.
+	 */
+	if (column->bv_allnulls && regular_keys)
 		PG_RETURN_BOOL(false);
 
-	attno = key->sk_attno;
-	subtype = key->sk_subtype;
-	value = key->sk_argument;
-	switch (key->sk_strategy)
+	/* If there are no regular keys, the page range is considered consistent. */
+	if (!regular_keys)
+		PG_RETURN_BOOL(true);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
 	{
-		case BTLessStrategyNumber:
-		case BTLessEqualStrategyNumber:
-			finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
-												 key->sk_strategy);
-			matches = FunctionCall2Coll(finfo, colloid, column->bv_values[0],
-										value);
-			break;
-		case BTEqualStrategyNumber:
+		ScanKey	key = keys[keyno];
 
-			/*
-			 * In the equality case (WHERE col = someval), we want to return
-			 * the current page range if the minimum value in the range <=
-			 * scan key, and the maximum value >= scan key.
-			 */
-			finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
-												 BTLessEqualStrategyNumber);
-			matches = FunctionCall2Coll(finfo, colloid, column->bv_values[0],
-										value);
-			if (!DatumGetBool(matches))
+		/* ignore IS NULL/IS NOT NULL tests handled above */
+		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_get_strategy_procinfo(bdesc, attno, subtype,
+													key->sk_strategy);
+				matches = FunctionCall2Coll(finfo, colloid, column->bv_values[0],
+											value);
 				break;
-			/* max() >= scankey */
-			finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
-												 BTGreaterEqualStrategyNumber);
-			matches = FunctionCall2Coll(finfo, colloid, column->bv_values[1],
-										value);
-			break;
-		case BTGreaterEqualStrategyNumber:
-		case BTGreaterStrategyNumber:
-			finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
-												 key->sk_strategy);
-			matches = FunctionCall2Coll(finfo, colloid, column->bv_values[1],
-										value);
-			break;
-		default:
-			/* shouldn't happen */
-			elog(ERROR, "invalid strategy number %d", key->sk_strategy);
-			matches = 0;
+			case BTEqualStrategyNumber:
+
+				/*
+				* In the equality case (WHERE col = someval), we want to return
+				* the current page range if the minimum value in the range <=
+				* scan key, and the maximum value >= scan key.
+				*/
+				finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
+													BTLessEqualStrategyNumber);
+				matches = FunctionCall2Coll(finfo, colloid, column->bv_values[0],
+											value);
+				if (!DatumGetBool(matches))
+					break;
+				/* max() >= scankey */
+				finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
+													BTGreaterEqualStrategyNumber);
+				matches = FunctionCall2Coll(finfo, colloid, column->bv_values[1],
+											value);
+				break;
+			case BTGreaterEqualStrategyNumber:
+			case BTGreaterStrategyNumber:
+				finfo = minmax_get_strategy_procinfo(bdesc, attno, subtype,
+													key->sk_strategy);
+				matches = FunctionCall2Coll(finfo, colloid, column->bv_values[1],
+											value);
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		/* found non-matching key */
+		if (!matches)
 			break;
 	}
 
-- 
2.9.5

  [text/x-patch] 0002-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch (10.3K, ../c32f9f46-e0ca-0c02-45ec-718dd381f4be@2ndquadrant.com/3-0002-Move-IS-NOT-NULL-checks-to-bringetbitmap.patch)
  download | inline diff:
From ed8b14643a8c313767f8285b40b44f96a9b8ece3 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 4 Feb 2018 14:27:42 +0100
Subject: [PATCH 2/4] Move IS [NOT] NULL checks to bringetbitmap

These checks are exactly the same in all BRIN opclasses, so it makes
sense to move them to bringetbimap() and evaluate them there. It also
allows us to handle some ranges without invoking the consistent
support procedure (e.g. when the key says IS NULL) but the range is
marked as not containing any NULL values.

Note: It may also be a good idea to first process NULL keys for all
attributes on a given page range, and then only proceed with the
remaining keys if it's still needed (when the page range is not
eliminated by NULL keys on any attribute).
---
 src/backend/access/brin/brin.c           | 116 ++++++++++++++++++++++++++++---
 src/backend/access/brin/brin_inclusion.c |  62 +----------------
 src/backend/access/brin/brin_minmax.c    |  62 +----------------
 3 files changed, 109 insertions(+), 131 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index b568786..7786f58 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -367,8 +367,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 	BrinMemTuple *dtup;
 	BrinTuple  *btup = NULL;
 	Size		btupsz = 0;
-	ScanKey	  **keys;
-	int		   *nkeys;
+	ScanKey	  **keys,
+			  **nullkeys;
+	int		   *nkeys,
+			   *nnullkeys;
 	int			keyno;
 
 	opaque = (BrinOpaque *) scan->opaque;
@@ -393,10 +395,13 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 
 	/*
 	 * Make room for per-attribute lists of scan keys that we'll pass to the
-	 * consistent support procedure.
+	 * consistent support procedure. We keep null and regular keys separate,
+	 * so that we can easily pass regular keys to the consistent function.
 	 */
 	keys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
+	nullkeys = palloc0(sizeof(ScanKey *) * bdesc->bd_tupdesc->natts);
 	nkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
+	nnullkeys = palloc0(sizeof(int) * bdesc->bd_tupdesc->natts);
 
 	/*
 	 * Preprocess the scan keys - split them into per-attribute arrays.
@@ -418,14 +423,12 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 							  keyattno - 1)->attcollation));
 
 		/* First time we see this index attribute, so init as needed. */
-		if (!keys[keyattno-1])
+		if (consistentFn[keyattno - 1].fn_oid == InvalidOid)
 		{
 			FmgrInfo   *tmp;
 
-			keys[keyattno-1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
-
-			/* First time this column, so look up consistent function */
-			Assert(consistentFn[keyattno - 1].fn_oid == InvalidOid);
+			Assert((keys[keyattno - 1] == NULL) && (nkeys[keyattno - 1] == 0));
+			Assert((nullkeys[keyattno - 1] == NULL) && (nnullkeys[keyattno - 1] == 0));
 
 			tmp = index_getprocinfo(idxRel, keyattno,
 									BRIN_PROCNUM_CONSISTENT);
@@ -433,9 +436,23 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 						   CurrentMemoryContext);
 		}
 
-		/* Add key to the per-attribute array. */
-		keys[keyattno - 1][nkeys[keyattno - 1]] = key;
-		nkeys[keyattno - 1]++;
+		/* Add key to the proper per-attribute array. */
+		if (key->sk_flags & SK_ISNULL)
+		{
+			if (!nullkeys[keyattno - 1])
+				nullkeys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			nullkeys[keyattno - 1][nnullkeys[keyattno - 1]] = key;
+			nnullkeys[keyattno - 1]++;
+		}
+		else
+		{
+			if (!keys[keyattno - 1])
+				keys[keyattno - 1] = palloc0(sizeof(ScanKey) * scan->numberOfKeys);
+
+			keys[keyattno - 1][nkeys[keyattno - 1]] = key;
+			nkeys[keyattno - 1]++;
+		}
 	}
 
 	/* allocate an initial in-memory tuple, out of the per-range memcxt */
@@ -530,6 +547,83 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 					collation = keys[attno - 1][0]->sk_collation;
 
 					/*
+					 * First check if there are any IS [NOT] NULL scan keys, and
+					 * if we're violating them. In that case we can terminate
+					 * early, without invoking the support function.
+					 *
+					 * As there may be more keys, we can only detemine mismatch
+					 * within this loop.
+					 */
+					for (keyno = 0; (keyno < nnullkeys[attno - 1]); keyno++)
+					{
+						ScanKey	key = nullkeys[attno - 1][keyno];
+
+						Assert(key->sk_attno == bval->bv_attno);
+
+						/* interrupt the loop as soon as we find a mismatch */
+						if (!addrange)
+							break;
+
+						/* handle IS NULL/IS NOT NULL tests */
+						if (key->sk_flags & SK_ISNULL)
+						{
+							/* IS NULL scan key, but range has no NULLs */
+							if (key->sk_flags & SK_SEARCHNULL)
+							{
+								if (!bval->bv_allnulls && !bval->bv_hasnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * For IS NOT NULL, we can only skip ranges that are
+							 * known to have only nulls.
+							 */
+							if (key->sk_flags & SK_SEARCHNOTNULL)
+							{
+								if (bval->bv_allnulls)
+									addrange = false;
+
+								continue;
+							}
+
+							/*
+							 * Neither IS NULL nor IS NOT NULL was used; assume all
+							 * indexable operators are strict and thus return false
+							 * with NULL value in the scan key.
+							 */
+							addrange = false;
+						}
+					}
+
+					/*
+					 * If any of the IS [NOT] NULL keys failed, the page range as
+					 * a whole can't pass. So terminate the loop.
+					 */
+					if (!addrange)
+						break;
+
+					/*
+					 * So either there are no IS [NOT] NULL keys, or all passed. If
+					 * there are no regular scan keys, we're done - the page range
+					 * matches. If there are regular keys, but the page range is
+					 * marked as 'all nulls' it can't possibly pass (we're assuming
+					 * the operators are strict).
+					 */
+
+					/* No regular scan keys - page range as a whole passes. */
+					if (!nkeys[attno - 1])
+						continue;
+
+					/* If it is all nulls, it cannot possibly be consistent. */
+					if (bval->bv_allnulls)
+					{
+						addrange = false;
+						break;
+					}
+
+					/*
 					 * Check whether the scan key is consistent with the page
 					 * range values; if so, have the pages in the range added
 					 * to the output bitmap.
diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c
index fb7f2b0..6d51c71 100644
--- a/src/backend/access/brin/brin_inclusion.c
+++ b/src/backend/access/brin/brin_inclusion.c
@@ -259,63 +259,6 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	Datum		result;
 	int			keyno;
 	bool		matches;
-	bool		regular_keys = false;
-
-	/*
-	 * 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);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	/* It has to be checked, if it contains elements that are not mergeable. */
 	if (DatumGetBool(column->bv_values[INCLUSION_UNMERGEABLE]))
@@ -327,9 +270,8 @@ brin_inclusion_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		attno = key->sk_attno;
 		subtype = key->sk_subtype;
diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c
index e58fd16..4621bea 100644
--- a/src/backend/access/brin/brin_minmax.c
+++ b/src/backend/access/brin/brin_minmax.c
@@ -156,63 +156,6 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	Datum		matches;
 	FmgrInfo   *finfo;
 	int			keyno;
-	bool		regular_keys = false;
-
-	/*
-	 * 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);
-		}
-		else
-			/* note we have regular (non-NULL) scan keys */
-			regular_keys = true;
-	}
-
-	/*
-	 * If the page range is all nulls, it cannot possibly be consistent if
-	 * there are some regular scan keys.
-	 */
-	if (column->bv_allnulls && regular_keys)
-		PG_RETURN_BOOL(false);
-
-	/* If there are no regular keys, the page range is considered consistent. */
-	if (!regular_keys)
-		PG_RETURN_BOOL(true);
 
 	matches = true;
 
@@ -220,9 +163,8 @@ brin_minmax_consistent(PG_FUNCTION_ARGS)
 	{
 		ScanKey	key = keys[keyno];
 
-		/* ignore IS NULL/IS NOT NULL tests handled above */
-		if (key->sk_flags & SK_ISNULL)
-			continue;
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
 
 		attno = key->sk_attno;
 		subtype = key->sk_subtype;
-- 
2.9.5

  [text/x-patch] 0003-BRIN-bloom-indexes.patch (89.8K, ../c32f9f46-e0ca-0c02-45ec-718dd381f4be@2ndquadrant.com/4-0003-BRIN-bloom-indexes.patch)
  download | inline diff:
From a67c77d7754de938f90fbd60044c5c0fcccc6395 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 9 Jan 2018 01:00:39 +0100
Subject: [PATCH 3/4] BRIN bloom indexes

An opclass implementing BRIN indexes with bloom filter summary.
Naturally, this opclass only supports equality searches, but that
is fine for many use cases (e.g. it's uncommon to do range queries
on identifiers or labels).

It's not quite clear how to size the bloom filter, or what to do
if it gets too full (more than 50% of bits set). The patch picks
some reasonable defaults, but perhaps we can improve this based on
more information (ndistinct from pg_stats, pages_per_range, ...).
Or make it configurable when creating the index.

Another option would be to implement scalable bloom filters, i.e.
instead of creating one filter and stop using it when it gets too
full, create another (larger) one and use it for new additions.
But considering we can only see very limited number of distinct
values per range, that seems like an overkill, and it might make
certain operations (particularly "union" support procedure) more
complicated.

The patch also implements an optimization that we initially keep
the values "raw", and only switch to the actual bloom filter once
we use the same space. This somewhat reduces the space usage when
the column is low-cardinality (at least within a page range), so
that the bloom filter defaults would be too high.
---
 doc/src/sgml/brin.sgml                   | 229 ++++++++++
 src/backend/access/brin/Makefile         |   2 +-
 src/backend/access/brin/brin_bloom.c     | 755 +++++++++++++++++++++++++++++++
 src/include/catalog/pg_amop.h            |  59 +++
 src/include/catalog/pg_amproc.h          | 153 +++++++
 src/include/catalog/pg_opclass.h         |  25 +
 src/include/catalog/pg_opfamily.h        |  20 +
 src/include/catalog/pg_proc.h            |  10 +
 src/test/regress/expected/brin_bloom.out | 438 ++++++++++++++++++
 src/test/regress/expected/opr_sanity.out |   3 +-
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/serial_schedule         |   1 +
 src/test/regress/sql/brin_bloom.sql      | 391 ++++++++++++++++
 13 files changed, 2085 insertions(+), 3 deletions(-)
 create mode 100644 src/backend/access/brin/brin_bloom.c
 create mode 100644 src/test/regress/expected/brin_bloom.out
 create mode 100644 src/test/regress/sql/brin_bloom.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 23c0e05..434538e 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -118,6 +118,13 @@
    </thead>
    <tbody>
     <row>
+     <entry><literal>abstime_bloom_ops</literal></entry>
+     <entry><type>abstime</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>abstime_minmax_ops</literal></entry>
      <entry><type>abstime</type></entry>
      <entry>
@@ -129,6 +136,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>int8_bloom_ops</literal></entry>
+     <entry><type>bigint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int8_minmax_ops</literal></entry>
      <entry><type>bigint</type></entry>
      <entry>
@@ -180,6 +194,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>bytea_bloom_ops</literal></entry>
+     <entry><type>bytea</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>bytea_minmax_ops</literal></entry>
      <entry><type>bytea</type></entry>
      <entry>
@@ -191,6 +212,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>bpchar_bloom_ops</literal></entry>
+     <entry><type>character</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>bpchar_minmax_ops</literal></entry>
      <entry><type>character</type></entry>
      <entry>
@@ -202,6 +230,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>char_bloom_ops</literal></entry>
+     <entry><type>"char"</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>char_minmax_ops</literal></entry>
      <entry><type>"char"</type></entry>
      <entry>
@@ -213,6 +248,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>date_bloom_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>date_minmax_ops</literal></entry>
      <entry><type>date</type></entry>
      <entry>
@@ -224,6 +266,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>float8_bloom_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>float8_minmax_ops</literal></entry>
      <entry><type>double precision</type></entry>
      <entry>
@@ -235,6 +284,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>inet_bloom_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>inet_minmax_ops</literal></entry>
      <entry><type>inet</type></entry>
      <entry>
@@ -258,6 +314,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>int4_bloom_ops</literal></entry>
+     <entry><type>integer</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int4_minmax_ops</literal></entry>
      <entry><type>integer</type></entry>
      <entry>
@@ -269,6 +332,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>interval_bloom_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>interval_minmax_ops</literal></entry>
      <entry><type>interval</type></entry>
      <entry>
@@ -280,6 +350,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>macaddr_bloom_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>macaddr_minmax_ops</literal></entry>
      <entry><type>macaddr</type></entry>
      <entry>
@@ -291,6 +368,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>macaddr8_bloom_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>macaddr8_minmax_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
      <entry>
@@ -302,6 +386,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>name_bloom_ops</literal></entry>
+     <entry><type>name</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>name_minmax_ops</literal></entry>
      <entry><type>name</type></entry>
      <entry>
@@ -313,6 +404,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>numeric_bloom_ops</literal></entry>
+     <entry><type>numeric</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>numeric_minmax_ops</literal></entry>
      <entry><type>numeric</type></entry>
      <entry>
@@ -324,6 +422,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>pg_lsn_bloom_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>pg_lsn_minmax_ops</literal></entry>
      <entry><type>pg_lsn</type></entry>
      <entry>
@@ -335,6 +440,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>oid_bloom_ops</literal></entry>
+     <entry><type>oid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>oid_minmax_ops</literal></entry>
      <entry><type>oid</type></entry>
      <entry>
@@ -366,6 +478,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>float4_bloom_ops</literal></entry>
+     <entry><type>real</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>float4_minmax_ops</literal></entry>
      <entry><type>real</type></entry>
      <entry>
@@ -377,6 +496,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>reltime_bloom_ops</literal></entry>
+     <entry><type>reltime</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>reltime_minmax_ops</literal></entry>
      <entry><type>reltime</type></entry>
      <entry>
@@ -388,6 +514,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>int2_bloom_ops</literal></entry>
+     <entry><type>smallint</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int2_minmax_ops</literal></entry>
      <entry><type>smallint</type></entry>
      <entry>
@@ -399,6 +532,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>text_bloom_ops</literal></entry>
+     <entry><type>text</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>text_minmax_ops</literal></entry>
      <entry><type>text</type></entry>
      <entry>
@@ -421,6 +561,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timestamp_bloom_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timestamp_minmax_ops</literal></entry>
      <entry><type>timestamp without time zone</type></entry>
      <entry>
@@ -432,6 +579,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timestamptz_bloom_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timestamptz_minmax_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
      <entry>
@@ -443,6 +597,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>time_bloom_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>time_minmax_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
      <entry>
@@ -454,6 +615,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timetz_bloom_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timetz_minmax_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
      <entry>
@@ -465,6 +633,13 @@
      </entry>
     </row>
     <row>
+     <entry><literal>uuid_bloom_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>=</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>uuid_minmax_ops</literal></entry>
      <entry><type>uuid</type></entry>
      <entry>
@@ -814,6 +989,60 @@ typedef struct BrinOpcInfo
  </para>
 
  <para>
+  To write an operator class for a data type that implements only an equality
+  operator and supports hashing, it is possible to use the bloom support procedures
+  alongside the corresponding operators, as shown in
+  <xref linkend="brin-extensibility-bloom-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-bloom-table">
+  <title>Procedure and Support Numbers for Bloom Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_bloom_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_bloom_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_bloom_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_bloom_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute hash of an element</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator equal-to</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
+    Support procedure numbers 1-10 are reserved for the BRIN internal
+    functions, so the SQL level functions start with number 11.  Support
+    function number 11 is the main function required to build the index.
+    It should accept one argument with the same data type as the operator class,
+    and return a hash of the value.
+ </para>
+
+ <para>
     Both minmax and inclusion operator classes support cross-data-type
     operators, though with these the dependencies become more complicated.
     The minmax operator class requires a full set of operators to be
diff --git a/src/backend/access/brin/Makefile b/src/backend/access/brin/Makefile
index 5aef925..a76d927 100644
--- a/src/backend/access/brin/Makefile
+++ b/src/backend/access/brin/Makefile
@@ -13,6 +13,6 @@ 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_minmax.o brin_inclusion.o brin_validate.o brin_bloom.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
new file mode 100644
index 0000000..461bfce
--- /dev/null
+++ b/src/backend/access/brin/brin_bloom.c
@@ -0,0 +1,755 @@
+/*
+ * brin_bloom.c
+ *		Implementation of Bloom 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_bloom.c
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/brin_internal.h"
+#include "access/brin_tuple.h"
+#include "access/hash.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"
+
+#include <math.h>
+
+#define BloomEqualStrategyNumber	1
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		BLOOM_MAX_PROCNUMS		4	/* maximum support procs we need */
+#define		PROCNUM_HASH			11	/* required */
+
+/*
+ * Subtract this from procnum to obtain index in BloomOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+#define		BLOOM_PHASE_SORTED		1
+#define		BLOOM_PHASE_HASH		2
+
+/* how many hashes to accumulate before hashing */
+#define		BLOOM_MAX_UNSORTED		32
+#define		BLOOM_GROW_BYTES		32
+#define		BLOOM_NDISTINCT			1000	/* number of distinct values */
+#define		BLOOM_ERROR_RATE		0.05	/* 2% false positive rate */
+
+/*
+ * Bloom Filter
+ *
+ * Represents a bloom filter, built on hashes of the indexed values. That is,
+ * we compute a uint32 hash of the value, and then store this hash into the
+ * bloom filter (and compute additional hashes on it).
+ *
+ * We use an optimisation that initially we store the uint32 values directly,
+ * without the extra hashing step. And only later filling the bitmap space,
+ * we switch to the regular bloom filter mode.
+ *
+ * PHASE_SORTED
+ *
+ * Initially we copy the uint32 hash into the bitmap, regularly sorting the
+ * hash values for fast lookup (we keep at most BLOOM_MAX_UNSORTED unsorted
+ * values).
+ *
+ * The idea is that if we only see very few distinct values, we can store
+ * them in less space compared to the (sparse) bloom filter bitmap. It also
+ * stores them exactly, although that's not a big advantage as almost-empty
+ * bloom filter has false positive rate close to zero anyway.
+ *
+ * PHASE_HASH
+ *
+ * Once we fill the bitmap space in the sorted phase, we switch to the hash
+ * phase, where we actually use the bloom filter. We treat the uint32 hashes
+ * as input values, and hash them again with different seeds (to get the k
+ * hash functions needed for bloom filter).
+ *
+ *
+ * XXX Maybe instead of explicitly limiting the number of unsorted values
+ * by BLOOM_MAX_UNSORTED, we should cap them by (filter size / 4B), i.e.
+ * allow up to the whole filter size.
+ *
+ * XXX Perhaps we could save a few bytes by using different data types, but
+ * considering the size of the bitmap, the difference is negligible.
+ *
+ * XXX We could also implement "sparse" bloom filters, keeping only the
+ * bytes that are not entirely 0. That might make the "sorted" phase
+ * mostly unnecessary.
+ *
+ * XXX We can also watch the number of bits set in the bloom filter, and
+ * then stop using it (and not store the bitmap, to save space) when the
+ * false positive rate gets too high.
+ */
+typedef struct BloomFilter
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* global bloom filter parameters */
+	uint32	phase;		/* phase (initially SORTED, then HASH) */
+	uint32	nhashes;	/* number of hash functions */
+	uint32	nbits;		/* number of bits in the bitmap (optimal) */
+	uint32	nbits_set;	/* number of bits set to 1 */
+
+	/* fields used only in the EXACT phase */
+	uint32	nvalues;	/* number of hashes stored (sorted + extra) */
+	uint32	nsorted;	/* number of uint32 hashes in sorted part */
+
+	/* bitmap of the bloom filter */
+	char 	bitmap[FLEXIBLE_ARRAY_MEMBER];
+
+} BloomFilter;
+
+/*
+ * bloom_init
+ * 		Initialize the Bloom Filter, allocate all the memory.
+ *
+ * The filter is initialized with optimal size for ndistinct expected
+ * values, and requested false positive rate. The filter is stored as
+ * varlena.
+ */
+static BloomFilter *
+bloom_init(int ndistinct, double false_positive_rate)
+{
+	Size			len;
+	BloomFilter	   *filter;
+
+	/* https://en.wikipedia.org/wiki/Bloom_filter */
+	int m;	/* number of bits */
+	int k;	/* number of hash functions */
+
+	Assert((ndistinct > 0) && (ndistinct < 10000));	/* 10k is mostly arbitrary limit */
+	Assert((false_positive_rate > 0) && (false_positive_rate < 1.0));
+
+	m = ceil((ndistinct * log(false_positive_rate)) / log(1.0 / (pow(2.0, log(2.0)))));
+
+	/* round m to whole bytes */
+	m = ((m + 7) / 8) * 8;
+
+	k = round(log(2.0) * m / ndistinct);
+
+	/*
+	 * Allocate the bloom filter with a minimum size 64B (about 40B in the
+	 * bitmap part). We require space at least for the header.
+	 */
+	len = Max(offsetof(BloomFilter, bitmap), 64);
+
+	filter = (BloomFilter *) palloc0(len);
+
+	filter->phase = BLOOM_PHASE_SORTED;
+	filter->nhashes = k;
+	filter->nbits = m;
+
+	SET_VARSIZE(filter, len);
+
+	return filter;
+}
+
+/* simple uint32 comparator, for pg_qsort and bsearch */
+static int
+cmp_uint32(const void *a, const void *b)
+{
+	uint32 *ia = (uint32 *) a;
+	uint32 *ib = (uint32 *) b;
+
+	if (*ia == *ib)
+		return 0;
+	else if (*ia < *ib)
+		return -1;
+	else
+		return 1;
+}
+
+/*
+ * bloom_compact
+ *		Compact the filter during the 'sorted' phase.
+ *
+ * We sort the uint32 hashes and remove duplicates, for two main reasons.
+ * Firstly, to keep most of the data sorted for bsearch lookups. Secondly,
+ * we try to save space by removing the duplicates, allowing us to stay
+ * in the sorted phase a bit longer.
+ *
+ * We currently don't repalloc the bitmap, i.e. we don't free the memory
+ * here - in the worst case we waste space for up to 32 unsorted hashes
+ * (if all of them are already in the sorted part), so about 128B. We can
+ * either reduce the number of unsorted items (e.g. to 8 hashes, which
+ * would mean 32B), or start doing the repalloc.
+ *
+ * XXX Actually, we don't need to do repalloc - we just need to set the
+ * varlena header length!
+ */
+static void
+bloom_compact(BloomFilter *filter)
+{
+	int		i,
+			nvalues;
+	uint32 *values;
+
+	/* never call compact on filters in HASH phase */
+	Assert(filter->phase == BLOOM_PHASE_SORTED);
+
+	/* no chance to compact anything */
+	if (filter->nvalues == filter->nsorted)
+		return;
+
+	values = (uint32 *) palloc(filter->nvalues * sizeof(uint32));
+
+	/* copy the data, then reset the bitmap */
+	memcpy(values, filter->bitmap, filter->nvalues * sizeof(uint32));
+	memset(filter->bitmap, 0, filter->nvalues * sizeof(uint32));
+
+	/* FIXME optimization: sort only the unsorted part, then merge */
+	pg_qsort(values, filter->nvalues, sizeof(uint32), cmp_uint32);
+
+	nvalues = 1;
+	for (i = 1; i < filter->nvalues; i++)
+	{
+		/* if a new value, keep it */
+		if (values[i] != values[i-1])
+		{
+			values[nvalues] = values[i];
+			nvalues++;
+		}
+	}
+
+	filter->nvalues = nvalues;
+	filter->nsorted = nvalues;
+
+	memcpy(filter->bitmap, values, nvalues * sizeof(uint32));
+
+	pfree(values);
+}
+
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter);
+
+/*
+ * bloom_add_value
+ * 		Add value to the bloom filter.
+ */
+static BloomFilter *
+bloom_add_value(BloomFilter *filter, uint32 value, bool *updated)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	/* assume 'not updated' by default */
+	Assert(filter);
+
+	/* if we're in the sorted phase, we store the hashes directly */
+	if (filter->phase == BLOOM_PHASE_SORTED)
+	{
+		/* how many uint32 hashes can we fit into the bitmap */
+		int maxvalues = filter->nbits / (8 * sizeof(uint32));
+
+		/* do not overflow the bitmap space or number of unsorted items */
+		Assert(filter->nvalues <= maxvalues);
+		Assert(filter->nvalues - filter->nsorted <= BLOOM_MAX_UNSORTED);
+
+		/*
+		 * In this branch we always update the filter - we either add the
+		 * hash to the unsorted part, or switch the filter to hashing.
+		 */
+		if (updated)
+			*updated = true;
+
+		/*
+		 * If the array is full, or if we reached the limit on unsorted
+		 * items, try to compact the filter first, before attempting to
+		 * add the new value.
+		 */
+		if ((filter->nvalues == maxvalues) ||
+			(filter->nvalues - filter->nsorted == BLOOM_MAX_UNSORTED))
+				bloom_compact(filter);
+
+		/*
+		 * Can we squeeze one more uint32 hash into the bitmap? Also make
+		 * sure there's enough space in the bytea value first.
+		 */
+		if (filter->nvalues < maxvalues)
+		{
+			Size len = VARSIZE_ANY(filter);
+			Size need = offsetof(BloomFilter,bitmap) + (filter->nvalues+1) * sizeof(uint32);
+
+			if (len < need)
+			{
+				/*
+				 * We don't double the size here, as in the first place we care about
+				 * reducing storage requirements, and the doubling happens automatically
+				 * in memory contexts anyway.
+				 *
+				 * XXX Zero the newly allocated part. Maybe not really needed?
+				 */
+				filter = (BloomFilter *) repalloc(filter, len + BLOOM_GROW_BYTES);
+				memset((char *)filter + len, 0, BLOOM_GROW_BYTES);
+				SET_VARSIZE(filter, len + BLOOM_GROW_BYTES);
+			}
+
+			/* copy the data into the bitmap */
+			memcpy(&filter->bitmap[filter->nvalues * sizeof(uint32)],
+				   &value, sizeof(uint32));
+
+			filter->nvalues++;
+
+			/* we're done */
+			return filter;
+		}
+
+		/* can't add any more exact hashes, so switch to hashing */
+		filter = bloom_switch_to_hashing(filter);
+	}
+
+	/* we better be in the hashing phase */
+	Assert(filter->phase == BLOOM_PHASE_HASH);
+
+    /* compute the hashes, used for the bloom filter */
+    big_h = ((uint32)DatumGetInt64(hash_uint32(value)));
+
+    h = big_h % filter->nbits;
+    d = big_h % (filter->nbits - 1); 
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, set it and remember we did that */
+		if (! (filter->bitmap[byte] & (0x01 << bit)))
+		{
+			filter->bitmap[byte] |= (0x01 << bit);
+			filter->nbits_set++;
+			if (updated)
+				*updated = true;
+		}
+
+		/* next bit */
+        h += d++;
+        if (h >= filter->nbits) h -= filter->nbits;
+        if (d == filter->nbits) d = 0; 
+	}
+
+	return filter;
+}
+
+/*
+ * bloom_switch_to_hashing
+ * 		Switch the bloom filter from sorted to hashing mode.
+ */
+static BloomFilter *
+bloom_switch_to_hashing(BloomFilter *filter)
+{
+	int		i;
+	uint32 *values;
+	Size			len;
+	BloomFilter	   *newfilter;
+
+	/*
+	 * The new filter is allocated with all the memory, directly into
+	 * the HASH phase.
+	 */
+	len = offsetof(BloomFilter, bitmap) + (filter->nbits / 8);
+
+	newfilter = (BloomFilter *) palloc0(len);
+
+	newfilter->nhashes = filter->nhashes;
+	newfilter->nbits = filter->nbits;
+	newfilter->phase = BLOOM_PHASE_HASH;
+
+	SET_VARSIZE(newfilter, len);
+
+	values = (uint32 *) filter->bitmap;
+
+	for (i = 0; i < filter->nvalues; i++)
+		/* ignore the return value here, re don't repalloc in hashing mode */
+		bloom_add_value(newfilter, values[i], NULL);
+
+	/* free the original filter, return the newly allocated one */
+	pfree(filter);
+
+	return newfilter;
+}
+
+/*
+ * bloom_contains_value
+ * 		Check if the bloom filter contains a particular value.
+ */
+static bool
+bloom_contains_value(BloomFilter *filter, uint32 value)
+{
+	int		i;
+	uint32	big_h, h, d;
+
+	Assert(filter);
+
+	/* in sorted mode we simply search the two arrays (sorted, unsorted) */
+	if (filter->phase == BLOOM_PHASE_SORTED)
+	{
+		int i;
+		uint32 *values = (uint32 *)filter->bitmap;
+
+		/* first search through the sorted part */
+		if ((filter->nsorted > 0) &&
+			(bsearch(&value, values, filter->nsorted, sizeof(uint32), cmp_uint32) != NULL))
+			return true;
+
+		/* now search through the unsorted part - linear search */
+		for (i = filter->nsorted; i < filter->nvalues; i++)
+			if (value == values[i])
+				return true;
+
+		/* nothing found */
+		return false;
+	}
+
+	/* now the regular hashing mode */
+	Assert(filter->phase == BLOOM_PHASE_HASH);
+
+    big_h = ((uint32)DatumGetInt64(hash_uint32(value)));
+
+    h = big_h % filter->nbits;
+    d = big_h % (filter->nbits - 1); 
+
+	/* compute the requested number of hashes */
+	for (i = 0; i < filter->nhashes; i++)
+	{
+		int byte = (h / 8);
+		int bit  = (h % 8);
+
+		/* if the bit is not set, the value is not there */
+		if (! (filter->bitmap[byte] & (0x01 << bit)))
+			return false;
+
+		/* next bit */
+        h += d++;
+        if (h >= filter->nbits) h -= filter->nbits;
+        if (d == filter->nbits) d = 0; 
+	}
+
+	/* all hashes found in bloom filter */
+	return true;
+}
+
+typedef struct BloomOpaque
+{
+	/*
+	 * XXX At this point we only need a single proc (to compute the hash),
+	 * but let's keep the array just like inclusion and minman opclasses,
+	 * for consistency. We may need additional procs in the future.
+	 */
+	FmgrInfo	extra_procinfos[BLOOM_MAX_PROCNUMS];
+	bool		extra_proc_missing[BLOOM_MAX_PROCNUMS];
+} BloomOpaque;
+
+static FmgrInfo *bloom_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+
+Datum
+brin_bloom_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.
+	 * 
+	 * bloom indexes only store a the filter as a single BYTEA column
+	 */
+
+	result = palloc0(MAXALIGN(SizeofBrinOpcInfo(1)) +
+					 sizeof(BloomOpaque));
+	result->oi_nstored = 1;
+	result->oi_opaque = (BloomOpaque *)
+		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 bloom filter 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_bloom_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();
+	FmgrInfo   *hashFn;
+	uint32		hashValue;
+	bool		updated = false;
+	AttrNumber	attno;
+	BloomFilter *filter;
+
+	/*
+	 * 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;
+
+	/*
+	 * If this is the first non-null value, we need to initialize the bloom
+	 * filter. Otherwise just extract the existing bloom filter from BrinValues.
+	 */
+	if (column->bv_allnulls)
+	{
+		filter = bloom_init(BLOOM_NDISTINCT, BLOOM_ERROR_RATE);
+		column->bv_values[0] = PointerGetDatum(filter);
+		column->bv_allnulls = false;
+		updated = true;
+	}
+	else
+		filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	/*
+	 * Compute the hash of the new value, using the supplied hash function,
+	 * and then add the hash value to the bloom filter.
+	 */
+	hashFn = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+	hashValue = DatumGetUInt32(FunctionCall1Coll(hashFn, colloid, newval));
+
+	filter = bloom_add_value(filter, hashValue, &updated);
+
+	column->bv_values[0] = PointerGetDatum(filter);
+
+	PG_RETURN_BOOL(updated);
+}
+
+/*
+ * 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 bloom
+ * filter.  Return true if so, false otherwise.
+ */
+Datum
+brin_bloom_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();
+	AttrNumber	attno;
+	Datum		value;
+	Datum		matches;
+	FmgrInfo   *finfo;
+	uint32		hashValue;
+	BloomFilter *filter;
+	int			keyno;
+
+	filter = (BloomFilter *) PG_DETOAST_DATUM(column->bv_values[0]);
+
+	Assert(filter);
+
+	matches = true;
+
+	for (keyno = 0; keyno < nkeys; keyno++)
+	{
+		ScanKey	key = keys[keyno];
+
+		/* NULL keys are handled and filtered-out in bringetbitmap */
+		Assert(!(key->sk_flags & SK_ISNULL));
+
+		attno = key->sk_attno;
+		value = key->sk_argument;
+
+		switch (key->sk_strategy)
+		{
+			case BloomEqualStrategyNumber:
+
+				/*
+				 * In the equality case (WHERE col = someval), we want to return
+				 * the current page range if the minimum value in the range <=
+				 * scan key, and the maximum value >= scan key.
+				 */
+				finfo = bloom_get_procinfo(bdesc, attno, PROCNUM_HASH);
+
+				hashValue = DatumGetUInt32(FunctionCall1Coll(finfo, colloid, value));
+				matches &= bloom_contains_value(filter, hashValue);
+
+				break;
+			default:
+				/* shouldn't happen */
+				elog(ERROR, "invalid strategy number %d", key->sk_strategy);
+				matches = 0;
+				break;
+		}
+
+		if (!matches)
+			break;
+	}
+
+	PG_RETURN_DATUM(matches);
+}
+
+/*
+ * Given two BrinValues, update the first of them as a union of the summary
+ * values contained in both.  The second one is untouched.
+ *
+ * XXX We assume the bloom filters have the same parameters fow now. In the
+ * future we should have 'can union' function, to decide if we can combine
+ * two particular bloom filters.
+ */
+Datum
+brin_bloom_union(PG_FUNCTION_ARGS)
+{
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	BloomFilter *filter_a;
+	BloomFilter *filter_b;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+
+	/* Adjust "hasnulls" */
+	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+		col_a->bv_hasnulls = true;
+
+	/* If there are no values in B, there's nothing left to do */
+	if (col_b->bv_allnulls)
+		PG_RETURN_VOID();
+
+	/*
+	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
+	 * B into A, and we're done.  We cannot run the operators in this case,
+	 * because values in A might contain garbage.  Note we already established
+	 * that B contains values.
+	 */
+	if (col_a->bv_allnulls)
+	{
+		col_a->bv_allnulls = false;
+		col_a->bv_values[0] = datumCopy(col_b->bv_values[0], false, -1);
+		PG_RETURN_VOID();
+	}
+
+	filter_a = (BloomFilter *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	filter_b = (BloomFilter *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	/* make sure neither of the bloom filters is NULL */
+	Assert(filter_a && filter_b);
+	Assert(filter_a->nbits == filter_b->nbits);
+
+	/*
+	 * Merging of the filters depends on the phase of both filters.
+	 */
+	if (filter_b->phase == BLOOM_PHASE_SORTED)
+	{
+		/*
+		 * Simply read all items from 'b' and add them to 'a' (the phase of
+		 * 'a' does not really matter).
+		 */
+		int		i;
+		uint32 *values = (uint32 *) filter_b->bitmap;
+
+		for (i = 0; i < filter_b->nvalues; i++)
+			filter_a = bloom_add_value(filter_a, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+	else if (filter_a->phase == BLOOM_PHASE_SORTED)
+	{
+		/*
+		 * 'b' hashed, 'a' sorted - copy 'b' into 'a' and then add all values
+		 * from 'a' into the new copy.
+		 */
+		int		i;
+		BloomFilter *filter_c;
+		uint32 *values = (uint32 *) filter_a->bitmap;
+
+		filter_c = (BloomFilter *) PG_DETOAST_DATUM(datumCopy(PointerGetDatum(filter_b), false, -1));
+
+		for (i = 0; i < filter_a->nvalues; i++)
+			filter_c = bloom_add_value(filter_c, values[i], NULL);
+
+		col_a->bv_values[0] = PointerGetDatum(filter_c);
+	}
+	else if (filter_a->phase == BLOOM_PHASE_HASH)
+	{
+		/*
+		 * 'b' hashed, 'a' hashed - merge the bitmaps by OR
+		 */
+		int		i;
+		int		nbytes = (filter_a->nbits + 7) / 8;
+
+		for (i = 0; i < nbytes; i++)
+			filter_a->bitmap[i] |= filter_b->bitmap[i];
+	}
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return inclusion opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+bloom_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	BloomOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (BloomOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
diff --git a/src/include/catalog/pg_amop.h b/src/include/catalog/pg_amop.h
index 03af581..84dc6e3 100644
--- a/src/include/catalog/pg_amop.h
+++ b/src/include/catalog/pg_amop.h
@@ -910,18 +910,24 @@ DATA(insert (	4064	 17   17 2 s	  1958	  3580 0 ));
 DATA(insert (	4064	 17   17 3 s	  1955	  3580 0 ));
 DATA(insert (	4064	 17   17 4 s	  1960	  3580 0 ));
 DATA(insert (	4064	 17   17 5 s	  1959	  3580 0 ));
+/* bloom bytea */
+DATA(insert (	5021	 17   17 1 s	  1955	  3580 0 ));
 /* minmax "char" */
 DATA(insert (	4062	 18   18 1 s	   631	  3580 0 ));
 DATA(insert (	4062	 18   18 2 s	   632	  3580 0 ));
 DATA(insert (	4062	 18   18 3 s		92	  3580 0 ));
 DATA(insert (	4062	 18   18 4 s	   634	  3580 0 ));
 DATA(insert (	4062	 18   18 5 s	   633	  3580 0 ));
+/* bloom "char" */
+DATA(insert (	5022	 18   18 1 s		92	  3580 0 ));
 /* minmax name */
 DATA(insert (	4065	 19   19 1 s	   660	  3580 0 ));
 DATA(insert (	4065	 19   19 2 s	   661	  3580 0 ));
 DATA(insert (	4065	 19   19 3 s		93	  3580 0 ));
 DATA(insert (	4065	 19   19 4 s	   663	  3580 0 ));
 DATA(insert (	4065	 19   19 5 s	   662	  3580 0 ));
+/* bloom name */
+DATA(insert (	5023	 19   19 1 s		93	  3580 0 ));
 /* minmax integer */
 DATA(insert (	4054	 20   20 1 s	   412	  3580 0 ));
 DATA(insert (	4054	 20   20 2 s	   414	  3580 0 ));
@@ -968,6 +974,16 @@ DATA(insert (	4054	 23   20 2 s		80	  3580 0 ));
 DATA(insert (	4054	 23   20 3 s		15	  3580 0 ));
 DATA(insert (	4054	 23   20 4 s		82	  3580 0 ));
 DATA(insert (	4054	 23   20 5 s		76	  3580 0 ));
+/* bloom integer */
+DATA(insert (	5024	 20   20 1 s	   410	  3580 0 ));
+DATA(insert (	5024	 20   21 1 s	  1868	  3580 0 ));
+DATA(insert (	5024	 20   23 1 s	   416	  3580 0 ));
+DATA(insert (	5024	 21   21 1 s		94	  3580 0 ));
+DATA(insert (	5024	 21   20 1 s	  1862	  3580 0 ));
+DATA(insert (	5024	 21   23 1 s	   532	  3580 0 ));
+DATA(insert (	5024	 23   23 1 s		96	  3580 0 ));
+DATA(insert (	5024	 23   21 1 s	   533	  3580 0 ));
+DATA(insert (	5024	 23   20 1 s		15	  3580 0 ));
 
 /* minmax text */
 DATA(insert (	4056	 25   25 1 s	   664	  3580 0 ));
@@ -975,12 +991,16 @@ DATA(insert (	4056	 25   25 2 s	   665	  3580 0 ));
 DATA(insert (	4056	 25   25 3 s		98	  3580 0 ));
 DATA(insert (	4056	 25   25 4 s	   667	  3580 0 ));
 DATA(insert (	4056	 25   25 5 s	   666	  3580 0 ));
+/* bloom text */
+DATA(insert (	5027	 25   25 1 s		98	  3580 0 ));
 /* minmax oid */
 DATA(insert (	4068	 26   26 1 s	   609	  3580 0 ));
 DATA(insert (	4068	 26   26 2 s	   611	  3580 0 ));
 DATA(insert (	4068	 26   26 3 s	   607	  3580 0 ));
 DATA(insert (	4068	 26   26 4 s	   612	  3580 0 ));
 DATA(insert (	4068	 26   26 5 s	   610	  3580 0 ));
+/* bloom oid */
+DATA(insert (	5029	 26   26 1 s	   607	  3580 0 ));
 /* minmax tid */
 DATA(insert (	4069	 27   27 1 s	  2799	  3580 0 ));
 DATA(insert (	4069	 27   27 2 s	  2801	  3580 0 ));
@@ -1008,6 +1028,11 @@ DATA(insert (	4070	701  701 2 s	   673	  3580 0 ));
 DATA(insert (	4070	701  701 3 s	   670	  3580 0 ));
 DATA(insert (	4070	701  701 4 s	   675	  3580 0 ));
 DATA(insert (	4070	701  701 5 s	   674	  3580 0 ));
+/* bloom float (float4, float8) */
+DATA(insert (	5030	700  700 1 s	   620	  3580 0 ));
+DATA(insert (	5030	700  701 1 s	  1120	  3580 0 ));
+DATA(insert (	5030	701  700 1 s	  1130	  3580 0 ));
+DATA(insert (	5030	701  701 1 s	   670	  3580 0 ));
 
 /* minmax abstime */
 DATA(insert (	4072	702  702 1 s	   562	  3580 0 ));
@@ -1015,24 +1040,32 @@ DATA(insert (	4072	702  702 2 s	   564	  3580 0 ));
 DATA(insert (	4072	702  702 3 s	   560	  3580 0 ));
 DATA(insert (	4072	702  702 4 s	   565	  3580 0 ));
 DATA(insert (	4072	702  702 5 s	   563	  3580 0 ));
+/* bloom abstime */
+DATA(insert (	5031	702  702 1 s	   560	  3580 0 ));
 /* minmax reltime */
 DATA(insert (	4073	703  703 1 s	   568	  3580 0 ));
 DATA(insert (	4073	703  703 2 s	   570	  3580 0 ));
 DATA(insert (	4073	703  703 3 s	   566	  3580 0 ));
 DATA(insert (	4073	703  703 4 s	   571	  3580 0 ));
 DATA(insert (	4073	703  703 5 s	   569	  3580 0 ));
+/* bloom reltime */
+DATA(insert (	5032	703  703 1 s	   566	  3580 0 ));
 /* minmax macaddr */
 DATA(insert (	4074	829  829 1 s	  1222	  3580 0 ));
 DATA(insert (	4074	829  829 2 s	  1223	  3580 0 ));
 DATA(insert (	4074	829  829 3 s	  1220	  3580 0 ));
 DATA(insert (	4074	829  829 4 s	  1225	  3580 0 ));
 DATA(insert (	4074	829  829 5 s	  1224	  3580 0 ));
+/* bloom macaddr */
+DATA(insert (	5033	829  829 1 s	  1220	  3580 0 ));
 /* minmax macaddr8 */
 DATA(insert (	4109	774  774 1 s	  3364	  3580 0 ));
 DATA(insert (	4109	774  774 2 s	  3365	  3580 0 ));
 DATA(insert (	4109	774  774 3 s	  3362	  3580 0 ));
 DATA(insert (	4109	774  774 4 s	  3367	  3580 0 ));
 DATA(insert (	4109	774  774 5 s	  3366	  3580 0 ));
+/* bloom macaddr8 */
+DATA(insert (	5034	774  774 1 s	  3362	  3580 0 ));
 /* minmax inet */
 DATA(insert (	4075	869  869 1 s	  1203	  3580 0 ));
 DATA(insert (	4075	869  869 2 s	  1204	  3580 0 ));
@@ -1046,18 +1079,24 @@ DATA(insert (	4102	869  869 8 s	   932	  3580 0 ));
 DATA(insert (	4102	869  869 18 s	  1201	  3580 0 ));
 DATA(insert (	4102	869  869 24 s	   933	  3580 0 ));
 DATA(insert (	4102	869  869 26 s	   931	  3580 0 ));
+/* bloom inet */
+DATA(insert (	5035	869  869 1 s	  1201	  3580 0 ));
 /* minmax character */
 DATA(insert (	4076   1042 1042 1 s	  1058	  3580 0 ));
 DATA(insert (	4076   1042 1042 2 s	  1059	  3580 0 ));
 DATA(insert (	4076   1042 1042 3 s	  1054	  3580 0 ));
 DATA(insert (	4076   1042 1042 4 s	  1061	  3580 0 ));
 DATA(insert (	4076   1042 1042 5 s	  1060	  3580 0 ));
+/* bloom character */
+DATA(insert (	5036   1042 1042 1 s	  1054	  3580 0 ));
 /* minmax time without time zone */
 DATA(insert (	4077   1083 1083 1 s	  1110	  3580 0 ));
 DATA(insert (	4077   1083 1083 2 s	  1111	  3580 0 ));
 DATA(insert (	4077   1083 1083 3 s	  1108	  3580 0 ));
 DATA(insert (	4077   1083 1083 4 s	  1113	  3580 0 ));
 DATA(insert (	4077   1083 1083 5 s	  1112	  3580 0 ));
+/* bloom time without time zone */
+DATA(insert (	5037   1083 1083 1 s	  1108	  3580 0 ));
 /* minmax datetime (date, timestamp, timestamptz) */
 DATA(insert (	4059   1114 1114 1 s	  2062	  3580 0 ));
 DATA(insert (	4059   1114 1114 2 s	  2063	  3580 0 ));
@@ -1104,6 +1143,16 @@ 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 ));
+/* 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 ));
+DATA(insert (	5038   1114 1184 1 s	  2536	  3580 0 ));
+DATA(insert (	5038   1082 1082 1 s	  1093	  3580 0 ));
+DATA(insert (	5038   1082 1114 1 s	  2347	  3580 0 ));
+DATA(insert (	5038   1082 1184 1 s	  2360	  3580 0 ));
+DATA(insert (	5038   1184 1082 1 s	  2386	  3580 0 ));
+DATA(insert (	5038   1184 1114 1 s	  2542	  3580 0 ));
+DATA(insert (	5038   1184 1184 1 s	  1320	  3580 0 ));
 
 /* minmax interval */
 DATA(insert (	4078   1186 1186 1 s	  1332	  3580 0 ));
@@ -1111,12 +1160,16 @@ DATA(insert (	4078   1186 1186 2 s	  1333	  3580 0 ));
 DATA(insert (	4078   1186 1186 3 s	  1330	  3580 0 ));
 DATA(insert (	4078   1186 1186 4 s	  1335	  3580 0 ));
 DATA(insert (	4078   1186 1186 5 s	  1334	  3580 0 ));
+/* bloom interval */
+DATA(insert (	5041   1186 1186 1 s	  1330	  3580 0 ));
 /* minmax time with time zone */
 DATA(insert (	4058   1266 1266 1 s	  1552	  3580 0 ));
 DATA(insert (	4058   1266 1266 2 s	  1553	  3580 0 ));
 DATA(insert (	4058   1266 1266 3 s	  1550	  3580 0 ));
 DATA(insert (	4058   1266 1266 4 s	  1555	  3580 0 ));
 DATA(insert (	4058   1266 1266 5 s	  1554	  3580 0 ));
+/* bloom time with time zone */
+DATA(insert (	5042   1266 1266 1 s	  1550	  3580 0 ));
 /* minmax bit */
 DATA(insert (	4079   1560 1560 1 s	  1786	  3580 0 ));
 DATA(insert (	4079   1560 1560 2 s	  1788	  3580 0 ));
@@ -1135,12 +1188,16 @@ DATA(insert (	4055   1700 1700 2 s	  1755	  3580 0 ));
 DATA(insert (	4055   1700 1700 3 s	  1752	  3580 0 ));
 DATA(insert (	4055   1700 1700 4 s	  1757	  3580 0 ));
 DATA(insert (	4055   1700 1700 5 s	  1756	  3580 0 ));
+/* bloom numeric */
+DATA(insert (	5045   1700 1700 1 s	  1752	  3580 0 ));
 /* minmax uuid */
 DATA(insert (	4081   2950 2950 1 s	  2974	  3580 0 ));
 DATA(insert (	4081   2950 2950 2 s	  2976	  3580 0 ));
 DATA(insert (	4081   2950 2950 3 s	  2972	  3580 0 ));
 DATA(insert (	4081   2950 2950 4 s	  2977	  3580 0 ));
 DATA(insert (	4081   2950 2950 5 s	  2975	  3580 0 ));
+/* bloom uuid */
+DATA(insert (	5046   2950 2950 1 s	  2972	  3580 0 ));
 /* inclusion range types */
 DATA(insert (	4103   3831 3831  1 s	  3893	  3580 0 ));
 DATA(insert (	4103   3831 3831  2 s	  3895	  3580 0 ));
@@ -1162,6 +1219,8 @@ DATA(insert (	4082   3220 3220 2 s	  3226	  3580 0 ));
 DATA(insert (	4082   3220 3220 3 s	  3222	  3580 0 ));
 DATA(insert (	4082   3220 3220 4 s	  3227	  3580 0 ));
 DATA(insert (	4082   3220 3220 5 s	  3225	  3580 0 ));
+/* bloom pg_lsn */
+DATA(insert (	5047   3220 3220 1 s	  3222	  3580 0 ));
 /* inclusion box */
 DATA(insert (	4104	603  603  1 s	   493	  3580 0 ));
 DATA(insert (	4104	603  603  2 s	   494	  3580 0 ));
diff --git a/src/include/catalog/pg_amproc.h b/src/include/catalog/pg_amproc.h
index f545a058..4871836 100644
--- a/src/include/catalog/pg_amproc.h
+++ b/src/include/catalog/pg_amproc.h
@@ -347,16 +347,34 @@ DATA(insert (	4064	17	  17  1  3383 ));
 DATA(insert (	4064	17	  17  2  3384 ));
 DATA(insert (	4064	17	  17  3  3385 ));
 DATA(insert (	4064	17	  17  4  3386 ));
+/* bloom bytea */
+DATA(insert (	5021	17	  17  1  5017 ));
+DATA(insert (	5021	17	  17  2  5018 ));
+DATA(insert (	5021	17	  17  3  5019 ));
+DATA(insert (	5021	17	  17  4  5020 ));
+DATA(insert (	5021	17	  17 11   456 ));
 /* minmax "char" */
 DATA(insert (	4062	18	  18  1  3383 ));
 DATA(insert (	4062	18	  18  2  3384 ));
 DATA(insert (	4062	18	  18  3  3385 ));
 DATA(insert (	4062	18	  18  4  3386 ));
+/* bloom "char" */
+DATA(insert (	5022	18	  18  1  5017 ));
+DATA(insert (	5022	18	  18  2  5018 ));
+DATA(insert (	5022	18	  18  3  5019 ));
+DATA(insert (	5022	18	  18  4  5020 ));
+DATA(insert (	5022	18	  18 11   454 ));
 /* minmax name */
 DATA(insert (	4065	19	  19  1  3383 ));
 DATA(insert (	4065	19	  19  2  3384 ));
 DATA(insert (	4065	19	  19  3  3385 ));
 DATA(insert (	4065	19	  19  4  3386 ));
+/* bloom name */
+DATA(insert (	5023	19	  19  1  5017 ));
+DATA(insert (	5023	19	  19  2  5018 ));
+DATA(insert (	5023	19	  19  3  5019 ));
+DATA(insert (	5023	19	  19  4  5020 ));
+DATA(insert (	5023	19	  19 11   455 ));
 /* minmax integer: int2, int4, int8 */
 DATA(insert (	4054	20	  20  1  3383 ));
 DATA(insert (	4054	20	  20  2  3384 ));
@@ -397,16 +415,47 @@ DATA(insert (	4054	23	  21  2  3384 ));
 DATA(insert (	4054	23	  21  3  3385 ));
 DATA(insert (	4054	23	  21  4  3386 ));
 
+/* bloom integer: int2, int4, int8 */
+DATA(insert (	5024	20	  20  1  5017 ));
+DATA(insert (	5024	20	  20  2  5018 ));
+DATA(insert (	5024	20	  20  3  5019 ));
+DATA(insert (	5024	20	  20  4  5020 ));
+DATA(insert (	5024	20	  20 11   949 ));
+
+DATA(insert (	5024	21	  21  1  5017 ));
+DATA(insert (	5024	21	  21  2  5018 ));
+DATA(insert (	5024	21	  21  3  5019 ));
+DATA(insert (	5024	21	  21  4  5020 ));
+DATA(insert (	5024	21	  21 11   449 ));
+
+DATA(insert (	5024	23	  23  1  5017 ));
+DATA(insert (	5024	23	  23  2  5018 ));
+DATA(insert (	5024	23	  23  3  5019 ));
+DATA(insert (	5024	23	  23  4  5020 ));
+DATA(insert (	5024	23	  23 11   450 ));
+
 /* minmax text */
 DATA(insert (	4056	25	  25  1  3383 ));
 DATA(insert (	4056	25	  25  2  3384 ));
 DATA(insert (	4056	25	  25  3  3385 ));
 DATA(insert (	4056	25	  25  4  3386 ));
+/* bloom text */
+DATA(insert (	5027	25	  25  1  5017 ));
+DATA(insert (	5027	25	  25  2  5018 ));
+DATA(insert (	5027	25	  25  3  5019 ));
+DATA(insert (	5027	25	  25  4  5020 ));
+DATA(insert (	5027	25	  25 11   400 ));
 /* minmax oid */
 DATA(insert (	4068	26	  26  1  3383 ));
 DATA(insert (	4068	26	  26  2  3384 ));
 DATA(insert (	4068	26	  26  3  3385 ));
 DATA(insert (	4068	26	  26  4  3386 ));
+/* bloom oid */
+DATA(insert (	5029	26	  26  1  5017 ));
+DATA(insert (	5029	26	  26  2  5018 ));
+DATA(insert (	5029	26	  26  3  5019 ));
+DATA(insert (	5029	26	  26  4  5020 ));
+DATA(insert (	5029	26	  26 11   453 ));
 /* minmax tid */
 DATA(insert (	4069	27	  27  1  3383 ));
 DATA(insert (	4069	27	  27  2  3384 ));
@@ -433,26 +482,63 @@ DATA(insert (	4070   701	 700  2  3384 ));
 DATA(insert (	4070   701	 700  3  3385 ));
 DATA(insert (	4070   701	 700  4  3386 ));
 
+/* bloom float */
+DATA(insert (	5030   700	 700  1  5017 ));
+DATA(insert (	5030   700	 700  2  5018 ));
+DATA(insert (	5030   700	 700  3  5019 ));
+DATA(insert (	5030   700	 700  4  5020 ));
+DATA(insert (	5030   700	 700 11   451 ));
+
+DATA(insert (	5030   701	 701  1  5017 ));
+DATA(insert (	5030   701	 701  2  5018 ));
+DATA(insert (	5030   701	 701  3  5019 ));
+DATA(insert (	5030   701	 701  4  5020 ));
+DATA(insert (	5030   701	 701 11   452 ));
+
 /* minmax abstime */
 DATA(insert (	4072   702	 702  1  3383 ));
 DATA(insert (	4072   702	 702  2  3384 ));
 DATA(insert (	4072   702	 702  3  3385 ));
 DATA(insert (	4072   702	 702  4  3386 ));
+/* bloom abstime */
+DATA(insert (	5031   702	 702  1  5017 ));
+DATA(insert (	5031   702	 702  2  5018 ));
+DATA(insert (	5031   702	 702  3  5019 ));
+DATA(insert (	5031   702	 702  4  5020 ));
+DATA(insert (	5031   702	 702 11   450 ));
 /* minmax reltime */
 DATA(insert (	4073   703	 703  1  3383 ));
 DATA(insert (	4073   703	 703  2  3384 ));
 DATA(insert (	4073   703	 703  3  3385 ));
 DATA(insert (	4073   703	 703  4  3386 ));
+/* bloom reltime */
+DATA(insert (	5032   703	 703  1  5017 ));
+DATA(insert (	5032   703	 703  2  5018 ));
+DATA(insert (	5032   703	 703  3  5019 ));
+DATA(insert (	5032   703	 703  4  5020 ));
+DATA(insert (	5032   703	 703 11   450 ));
 /* minmax macaddr */
 DATA(insert (	4074   829	 829  1  3383 ));
 DATA(insert (	4074   829	 829  2  3384 ));
 DATA(insert (	4074   829	 829  3  3385 ));
 DATA(insert (	4074   829	 829  4  3386 ));
+/* bloom macaddr */
+DATA(insert (	5033   829	 829  1  5017 ));
+DATA(insert (	5033   829	 829  2  5018 ));
+DATA(insert (	5033   829	 829  3  5019 ));
+DATA(insert (	5033   829	 829  4  5020 ));
+DATA(insert (	5033   829	 829 11   399 ));
 /* minmax macaddr8 */
 DATA(insert (	4109   774	 774  1  3383 ));
 DATA(insert (	4109   774	 774  2  3384 ));
 DATA(insert (	4109   774	 774  3  3385 ));
 DATA(insert (	4109   774	 774  4  3386 ));
+/* bloom macaddr8 */
+DATA(insert (	5034   774	 774  1  5017 ));
+DATA(insert (	5034   774	 774  2  5018 ));
+DATA(insert (	5034   774	 774  3  5019 ));
+DATA(insert (	5034   774	 774  4  5020 ));
+DATA(insert (	5034   774	 774 11   328 ));
 /* minmax inet */
 DATA(insert (	4075   869	 869  1  3383 ));
 DATA(insert (	4075   869	 869  2  3384 ));
@@ -466,16 +552,34 @@ DATA(insert (	4102   869	 869  4  4108 ));
 DATA(insert (	4102   869	 869 11  4063 ));
 DATA(insert (	4102   869	 869 12  4071 ));
 DATA(insert (	4102   869	 869 13   930 ));
+/* bloom inet */
+DATA(insert (	5035   869	 869  1  5017 ));
+DATA(insert (	5035   869	 869  2  5018 ));
+DATA(insert (	5035   869	 869  3  5019 ));
+DATA(insert (	5035   869	 869  4  5020 ));
+DATA(insert (	5035   869	 869 11   422 ));
 /* minmax character */
 DATA(insert (	4076  1042	1042  1  3383 ));
 DATA(insert (	4076  1042	1042  2  3384 ));
 DATA(insert (	4076  1042	1042  3  3385 ));
 DATA(insert (	4076  1042	1042  4  3386 ));
+/* bloom character */
+DATA(insert (	5036  1042	1042  1  5017 ));
+DATA(insert (	5036  1042	1042  2  5018 ));
+DATA(insert (	5036  1042	1042  3  5019 ));
+DATA(insert (	5036  1042	1042  4  5020 ));
+DATA(insert (	5036  1042	1042 11   454 ));
 /* minmax time without time zone */
 DATA(insert (	4077  1083	1083  1  3383 ));
 DATA(insert (	4077  1083	1083  2  3384 ));
 DATA(insert (	4077  1083	1083  3  3385 ));
 DATA(insert (	4077  1083	1083  4  3386 ));
+/* bloom time without time zone */
+DATA(insert (	5037  1083	1083  1  5017 ));
+DATA(insert (	5037  1083	1083  2  5018 ));
+DATA(insert (	5037  1083	1083  3  5019 ));
+DATA(insert (	5037  1083	1083  4  5020 ));
+DATA(insert (	5037  1083	1083 11  1688 ));
 /* minmax datetime (date, timestamp, timestamptz) */
 DATA(insert (	4059  1114	1114  1  3383 ));
 DATA(insert (	4059  1114	1114  2  3384 ));
@@ -516,16 +620,47 @@ DATA(insert (	4059  1082	1184  2  3384 ));
 DATA(insert (	4059  1082	1184  3  3385 ));
 DATA(insert (	4059  1082	1184  4  3386 ));
 
+/* bloom datetime (date, timestamp, timestamptz) */
+DATA(insert (	5038  1114	1114  1  5017 ));
+DATA(insert (	5038  1114	1114  2  5018 ));
+DATA(insert (	5038  1114	1114  3  5019 ));
+DATA(insert (	5038  1114	1114  4  5020 ));
+DATA(insert (	5038  1114	1114 11  2039 ));
+
+DATA(insert (	5038  1184	1184  1  5017 ));
+DATA(insert (	5038  1184	1184  2  5018 ));
+DATA(insert (	5038  1184	1184  3  5019 ));
+DATA(insert (	5038  1184	1184  4  5020 ));
+DATA(insert (	5038  1184	1184 11  2039 ));
+
+DATA(insert (	5038  1082	1082  1  5017 ));
+DATA(insert (	5038  1082	1082  2  5018 ));
+DATA(insert (	5038  1082	1082  3  5019 ));
+DATA(insert (	5038  1082	1082  4  5020 ));
+DATA(insert (	5038  1082	1082 11  450 ));
+
 /* minmax interval */
 DATA(insert (	4078  1186	1186  1  3383 ));
 DATA(insert (	4078  1186	1186  2  3384 ));
 DATA(insert (	4078  1186	1186  3  3385 ));
 DATA(insert (	4078  1186	1186  4  3386 ));
+/* bloom interval */
+DATA(insert (	5041  1186	1186  1  5017 ));
+DATA(insert (	5041  1186	1186  2  5018 ));
+DATA(insert (	5041  1186	1186  3  5019 ));
+DATA(insert (	5041  1186	1186  4  5020 ));
+DATA(insert (	5041  1186	1186 11  1697 ));
 /* minmax time with time zone */
 DATA(insert (	4058  1266	1266  1  3383 ));
 DATA(insert (	4058  1266	1266  2  3384 ));
 DATA(insert (	4058  1266	1266  3  3385 ));
 DATA(insert (	4058  1266	1266  4  3386 ));
+/* bloom time with time zone */
+DATA(insert (	5042  1266	1266  1  5017 ));
+DATA(insert (	5042  1266	1266  2  5018 ));
+DATA(insert (	5042  1266	1266  3  5019 ));
+DATA(insert (	5042  1266	1266  4  5020 ));
+DATA(insert (	5042  1266	1266 11  1696 ));
 /* minmax bit */
 DATA(insert (	4079  1560	1560  1  3383 ));
 DATA(insert (	4079  1560	1560  2  3384 ));
@@ -541,11 +676,23 @@ DATA(insert (	4055  1700	1700  1  3383 ));
 DATA(insert (	4055  1700	1700  2  3384 ));
 DATA(insert (	4055  1700	1700  3  3385 ));
 DATA(insert (	4055  1700	1700  4  3386 ));
+/* bloom numeric */
+DATA(insert (	5045  1700	1700  1  5017 ));
+DATA(insert (	5045  1700	1700  2  5018 ));
+DATA(insert (	5045  1700	1700  3  5019 ));
+DATA(insert (	5045  1700	1700  4  5020 ));
+DATA(insert (	5045  1700	1700 11   432 ));
 /* minmax uuid */
 DATA(insert (	4081  2950	2950  1  3383 ));
 DATA(insert (	4081  2950	2950  2  3384 ));
 DATA(insert (	4081  2950	2950  3  3385 ));
 DATA(insert (	4081  2950	2950  4  3386 ));
+/* bloom uuid */
+DATA(insert (	5046  2950	2950  1  5017 ));
+DATA(insert (	5046  2950	2950  2  5018 ));
+DATA(insert (	5046  2950	2950  3  5019 ));
+DATA(insert (	5046  2950	2950  4  5020 ));
+DATA(insert (	5046  2950	2950 11  2963 ));
 /* inclusion range types */
 DATA(insert (	4103  3831	3831  1  4105 ));
 DATA(insert (	4103  3831	3831  2  4106 ));
@@ -559,6 +706,12 @@ DATA(insert (	4082  3220	3220  1  3383 ));
 DATA(insert (	4082  3220	3220  2  3384 ));
 DATA(insert (	4082  3220	3220  3  3385 ));
 DATA(insert (	4082  3220	3220  4  3386 ));
+/* bloom pg_lsn */
+DATA(insert (	5047  3220	3220  1  5017 ));
+DATA(insert (	5047  3220	3220  2  5018 ));
+DATA(insert (	5047  3220	3220  3  5019 ));
+DATA(insert (	5047  3220	3220  4  5020 ));
+DATA(insert (	5047  3220	3220 11  3252 ));
 /* inclusion box */
 DATA(insert (	4104   603	 603  1  4105 ));
 DATA(insert (	4104   603	 603  2  4106 ));
diff --git a/src/include/catalog/pg_opclass.h b/src/include/catalog/pg_opclass.h
index e11d52e..7dbd4ff 100644
--- a/src/include/catalog/pg_opclass.h
+++ b/src/include/catalog/pg_opclass.h
@@ -214,36 +214,61 @@ DATA(insert (	2742	jsonb_path_ops		PGNSP PGUID 4037  3802 f 23 ));
 /* BRIN operator classes */
 /* no brin opclass for bool */
 DATA(insert (	3580	bytea_minmax_ops		PGNSP PGUID 4064	17 t 17 ));
+DATA(insert (	3580	bytea_bloom_ops			PGNSP PGUID 5021	17 f 17 ));
 DATA(insert (	3580	char_minmax_ops			PGNSP PGUID 4062	18 t 18 ));
+DATA(insert (	3580	char_bloom_ops			PGNSP PGUID 5022	18 f 18 ));
 DATA(insert (	3580	name_minmax_ops			PGNSP PGUID 4065	19 t 19 ));
+DATA(insert (	3580	name_bloom_ops			PGNSP PGUID 5023	19 f 19 ));
 DATA(insert (	3580	int8_minmax_ops			PGNSP PGUID 4054	20 t 20 ));
+DATA(insert (	3580	int8_bloom_ops			PGNSP PGUID 5024	20 f 20 ));
 DATA(insert (	3580	int2_minmax_ops			PGNSP PGUID 4054	21 t 21 ));
+DATA(insert (	3580	int2_bloom_ops			PGNSP PGUID 5024	21 f 21 ));
 DATA(insert (	3580	int4_minmax_ops			PGNSP PGUID 4054	23 t 23 ));
+DATA(insert (	3580	int4_bloom_ops			PGNSP PGUID 5024	23 f 23 ));
 DATA(insert (	3580	text_minmax_ops			PGNSP PGUID 4056	25 t 25 ));
+DATA(insert (	3580	text_bloom_ops			PGNSP PGUID 5027	25 f 25 ));
 DATA(insert (	3580	oid_minmax_ops			PGNSP PGUID 4068	26 t 26 ));
+DATA(insert (	3580	oid_bloom_ops			PGNSP PGUID 5029	26 f 26 ));
 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_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 ));
 DATA(insert (	3580	reltime_minmax_ops		PGNSP PGUID 4073   703 t 703 ));
+DATA(insert (	3580	reltime_bloom_ops		PGNSP PGUID 5032   703 f 703 ));
 DATA(insert (	3580	macaddr_minmax_ops		PGNSP PGUID 4074   829 t 829 ));
+DATA(insert (	3580	macaddr_bloom_ops		PGNSP PGUID 5033   829 f 829 ));
 DATA(insert (	3580	macaddr8_minmax_ops		PGNSP PGUID 4109   774 t 774 ));
+DATA(insert (	3580	macaddr8_bloom_ops		PGNSP PGUID 5034   774 f 774 ));
 DATA(insert (	3580	inet_minmax_ops			PGNSP PGUID 4075   869 f 869 ));
 DATA(insert (	3580	inet_inclusion_ops		PGNSP PGUID 4102   869 t 869 ));
+DATA(insert (	3580	inet_bloom_ops			PGNSP PGUID 5035   869 f 869 ));
 DATA(insert (	3580	bpchar_minmax_ops		PGNSP PGUID 4076  1042 t 1042 ));
+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_bloom_ops			PGNSP PGUID 5038  1082 f 1082 ));
 DATA(insert (	3580	timestamp_minmax_ops	PGNSP PGUID 4059  1114 t 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_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 ));
 DATA(insert (	3580	timetz_minmax_ops		PGNSP PGUID 4058  1266 t 1266 ));
+DATA(insert (	3580	timetz_bloom_ops		PGNSP PGUID 5042  1266 f 1266 ));
 DATA(insert (	3580	bit_minmax_ops			PGNSP PGUID 4079  1560 t 1560 ));
 DATA(insert (	3580	varbit_minmax_ops		PGNSP PGUID 4080  1562 t 1562 ));
 DATA(insert (	3580	numeric_minmax_ops		PGNSP PGUID 4055  1700 t 1700 ));
+DATA(insert (	3580	numeric_bloom_ops		PGNSP PGUID 5045  1700 f 1700 ));
 /* no brin opclass for record, anyarray */
 DATA(insert (	3580	uuid_minmax_ops			PGNSP PGUID 4081  2950 t 2950 ));
+DATA(insert (	3580	uuid_bloom_ops			PGNSP PGUID 5046  2950 f 2950 ));
 DATA(insert (	3580	range_inclusion_ops		PGNSP PGUID 4103  3831 t 3831 ));
 DATA(insert (	3580	pg_lsn_minmax_ops		PGNSP PGUID 4082  3220 t 3220 ));
+DATA(insert (	3580	pg_lsn_bloom_ops		PGNSP PGUID 5047  3220 f 3220 ));
 /* no brin opclass for enum, tsvector, tsquery, jsonb */
 DATA(insert (	3580	box_inclusion_ops		PGNSP PGUID 4104   603 t 603 ));
 /* no brin opclass for the geometric types except box */
diff --git a/src/include/catalog/pg_opfamily.h b/src/include/catalog/pg_opfamily.h
index b544474..bc22ce3 100644
--- a/src/include/catalog/pg_opfamily.h
+++ b/src/include/catalog/pg_opfamily.h
@@ -160,30 +160,50 @@ DATA(insert OID = 4036 (	2742	jsonb_ops		PGNSP PGUID ));
 DATA(insert OID = 4037 (	2742	jsonb_path_ops	PGNSP PGUID ));
 
 DATA(insert OID = 4054 (	3580	integer_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5024 (	3580	integer_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4055 (	3580	numeric_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5045 (	3580	numeric_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4056 (	3580	text_minmax_ops			PGNSP PGUID ));
+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 = 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 ));
 DATA(insert OID = 4064 (	3580	bytea_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5021 (	3580	bytea_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4065 (	3580	name_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5023 (	3580	name_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4068 (	3580	oid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5029 (	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 = 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 ));
 DATA(insert OID = 4073 (	3580	reltime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5032 (	3580	reltime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4074 (	3580	macaddr_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5033 (	3580	macaddr_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4109 (	3580	macaddr8_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5034 (	3580	macaddr8_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4075 (	3580	network_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 4102 (	3580	network_inclusion_ops	PGNSP PGUID ));
+DATA(insert OID = 5035 (	3580	network_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4076 (	3580	bpchar_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5036 (	3580	bpchar_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4077 (	3580	time_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5037 (	3580	time_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4078 (	3580	interval_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5041 (	3580	interval_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4079 (	3580	bit_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 4080 (	3580	varbit_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 4081 (	3580	uuid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 5046 (	3580	uuid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4103 (	3580	range_inclusion_ops		PGNSP PGUID ));
 DATA(insert OID = 4082 (	3580	pg_lsn_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 5047 (	3580	pg_lsn_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4104 (	3580	box_inclusion_ops		PGNSP PGUID ));
 DATA(insert OID = 5000 (	4000	box_ops		PGNSP PGUID ));
 DATA(insert OID = 5008 (	4000	poly_ops				PGNSP PGUID ));
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index f01648c..7154e5e 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -4374,6 +4374,16 @@ DESCR("BRIN inclusion support");
 DATA(insert OID = 4108 ( brin_inclusion_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_inclusion_union _null_ _null_ _null_ ));
 DESCR("BRIN inclusion support");
 
+/* BRIN bloom */
+DATA(insert OID = 5017 ( brin_bloom_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_bloom_opcinfo _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+DATA(insert OID = 5018 ( brin_bloom_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_bloom_add_value _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+DATA(insert OID = 5019 ( brin_bloom_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_bloom_consistent _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+DATA(insert OID = 5020 ( brin_bloom_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_bloom_union _null_ _null_ _null_ ));
+DESCR("BRIN bloom support");
+
 /* userlock replacements */
 DATA(insert OID = 2880 (  pg_advisory_lock				PGNSP PGUID 12 1 0 0 0 f f f f t f v u 1 0 2278 "20" _null_ _null_ _null_ _null_ _null_ pg_advisory_lock_int8 _null_ _null_ _null_ ));
 DESCR("obtain exclusive advisory lock");
diff --git a/src/test/regress/expected/brin_bloom.out b/src/test/regress/expected/brin_bloom.out
new file mode 100644
index 0000000..ec6e27a
--- /dev/null
+++ b/src/test/regress/expected/brin_bloom.out
@@ -0,0 +1,438 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+ERROR:  "brintest_bloom" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_bloom
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_bloom_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_bloom
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 684f7f2..91b2ecc 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -1819,6 +1819,7 @@ ORDER BY 1, 2, 3;
        2742 |           11 | ?&
        3580 |            1 | <
        3580 |            1 | <<
+       3580 |            1 | =
        3580 |            2 | &<
        3580 |            2 | <=
        3580 |            3 | &&
@@ -1880,7 +1881,7 @@ ORDER BY 1, 2, 3;
        4000 |           25 | <<=
        4000 |           26 | >>
        4000 |           27 | >>=
-(121 rows)
+(122 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ad9434f..149999d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -84,7 +84,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password
+test: brin brin_bloom gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 27cd498..06c3488 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -106,6 +106,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: brin
+test: brin_bloom
 test: gin
 test: gist
 test: spgist
diff --git a/src/test/regress/sql/brin_bloom.sql b/src/test/regress/sql/brin_bloom.sql
new file mode 100644
index 0000000..8b2c005
--- /dev/null
+++ b/src/test/regress/sql/brin_bloom.sql
@@ -0,0 +1,391 @@
+CREATE TABLE brintest_bloom (byteacol bytea,
+	charcol "char",
+	namecol name,
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	textcol text,
+	oidcol oid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	bpcharcol character,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 8)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 8),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_bloom (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+CREATE INDEX brinidx_bloom ON brintest_bloom USING brin (
+	byteacol bytea_bloom_ops,
+	charcol char_bloom_ops,
+	namecol name_bloom_ops,
+	int8col int8_bloom_ops,
+	int2col int2_bloom_ops,
+	int4col int4_bloom_ops,
+	textcol text_bloom_ops,
+	oidcol oid_bloom_ops,
+	float4col float4_bloom_ops,
+	float8col float8_bloom_ops,
+	macaddrcol macaddr_bloom_ops,
+	inetcol inet_bloom_ops,
+	cidrcol inet_bloom_ops,
+	bpcharcol bpchar_bloom_ops,
+	datecol date_bloom_ops,
+	timecol time_bloom_ops,
+	timestampcol timestamp_bloom_ops,
+	timestamptzcol timestamptz_bloom_ops,
+	intervalcol interval_bloom_ops,
+	timetzcol timetz_bloom_ops,
+	numericcol numeric_bloom_ops,
+	uuidcol uuid_bloom_ops,
+	lsncol pg_lsn_bloom_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_bloom (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_bloom VALUES
+	('byteacol', 'bytea',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('charcol', '"char"',
+	 '{=}',
+	 '{M}',
+	 '{6}'),
+	('namecol', 'name',
+	 '{=}',
+	 '{MAAAAA}',
+	 '{2}'),
+	('int2col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int2col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int2',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int4',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int4col', 'int8',
+	 '{=}',
+	 '{800}',
+	 '{1}'),
+	('int8col', 'int8',
+	 '{=}',
+	 '{1257141600}',
+	 '{1}'),
+	('textcol', 'text',
+	 '{=}',
+	 '{BNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAABNAAAA}',
+	 '{1}'),
+	('oidcol', 'oid',
+	 '{=}',
+	 '{8800}',
+	 '{1}'),
+	('float4col', 'float4',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float4col', 'float8',
+	 '{=}',
+	 '{1}',
+	 '{4}'),
+	('float8col', 'float4',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('float8col', 'float8',
+	 '{=}',
+	 '{0}',
+	 '{1}'),
+	('macaddrcol', 'macaddr',
+	 '{=}',
+	 '{2c:00:2d:00:16:00}',
+	 '{2}'),
+	('inetcol', 'inet',
+	 '{=}',
+	 '{10.2.14.231/24}',
+	 '{1}'),
+	('inetcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'inet',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{10.2.14/24}',
+	 '{2}'),
+	('cidrcol', 'cidr',
+	 '{=}',
+	 '{fe80::6e40:8ff:fea9:8c46}',
+	 '{1}'),
+	('bpcharcol', 'bpchar',
+	 '{=}',
+	 '{W}',
+	 '{6}'),
+	('datecol', 'date',
+	 '{=}',
+	 '{2009-12-01}',
+	 '{1}'),
+	('timecol', 'time',
+	 '{=}',
+	 '{02:28:57}',
+	 '{1}'),
+	('timestampcol', 'timestamp',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestampcol', 'timestamptz',
+	 '{=}',
+	 '{1964-03-24 19:26:45}',
+	 '{1}'),
+	('timestamptzcol', 'timestamptz',
+	 '{=}',
+	 '{1972-10-19 09:00:00-07}',
+	 '{1}'),
+	('intervalcol', 'interval',
+	 '{=}',
+	 '{1 mons 13 days 12:24}',
+	 '{1}'),
+	('timetzcol', 'timetz',
+	 '{=}',
+	 '{01:35:50+02}',
+	 '{2}'),
+	('numericcol', 'numeric',
+	 '{=}',
+	 '{2268164.347826086956521739130434782609}',
+	 '{1}'),
+	('uuidcol', 'uuid',
+	 '{=}',
+	 '{52225222-5222-5222-5222-522252225222}',
+	 '{1}'),
+	('lsncol', 'pg_lsn',
+	 '{=, IS, IS NOT}',
+	 '{44/455222, NULL, NULL}',
+	 '{1, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_bloom, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_bloom%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_bloom WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_bloom WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_bloom SELECT
+	repeat(stringu1, 42)::bytea,
+	substr(stringu1, 1, 1)::"char",
+	stringu1::name, 142857 * tenthous,
+	thousand,
+	twothousand,
+	repeat(stringu1, 42),
+	unique1::oid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	substr(stringu1, 1, 1)::bpchar,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+VACUUM brintest_bloom;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_bloom SET int8col = int8col * int4col;
+UPDATE brintest_bloom SET textcol = '' WHERE textcol IS NOT NULL;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_bloom'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_bloom'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_bloom', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 0);
+SELECT brin_desummarize_range('brinidx_bloom', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_bloom (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_bloom_idx ON brin_summarize_bloom USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_bloom VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_bloom_idx', -1);
+SELECT brin_summarize_range('brin_summarize_bloom_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_bloom (a INT, b INT);
+INSERT INTO brin_test_bloom SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_bloom_a_idx ON brin_test_bloom USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_bloom_b_idx ON brin_test_bloom USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_bloom;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_bloom WHERE b = 1;
-- 
2.9.5

  [text/x-patch] 0004-BRIN-multi-range-minmax-indexes.patch (146.8K, ../c32f9f46-e0ca-0c02-45ec-718dd381f4be@2ndquadrant.com/5-0004-BRIN-multi-range-minmax-indexes.patch)
  download | inline diff:
From 871bab2388b79b5d4c3de2bb008a762ed2246986 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Tue, 9 Jan 2018 01:01:26 +0100
Subject: [PATCH 4/4] BRIN multi-range minmax indexes

The built-in minmax opclass works well for column correlated with
physical location in the table (e.g. timestamp in append-only
tables). But when the data is not perfectly correlated (possibly
due to later DELETE/UPDATE/INSERT commands), the minmax ranges
get very wide and ineffective to eliminate the page ranges.

This opclass deals with that by replacing the one [min,max] range
with multiple smaller ones. This allows us to track gaps, and deal
with outlier values.

Note: Currently, this only works with float8-based data types.
Supporting additional data types is not a big issue, but will
require extending the opclass with "subtract" operator (used to
compute distance between values when merging ranges).
---
 doc/src/sgml/brin.sgml                      |  234 ++-
 src/backend/access/brin/Makefile            |    3 +-
 src/backend/access/brin/brin_minmax_multi.c | 2200 +++++++++++++++++++++++++++
 src/include/catalog/pg_amop.h               |  191 +++
 src/include/catalog/pg_amproc.h             |  201 +++
 src/include/catalog/pg_opclass.h            |   21 +
 src/include/catalog/pg_opfamily.h           |   16 +
 src/include/catalog/pg_proc.h               |   44 +
 src/test/regress/expected/brin_multi.out    |  408 +++++
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/sql/brin_multi.sql         |  361 +++++
 11 files changed, 3672 insertions(+), 9 deletions(-)
 create mode 100644 src/backend/access/brin/brin_minmax_multi.c
 create mode 100644 src/test/regress/expected/brin_multi.out
 create mode 100644 src/test/regress/sql/brin_multi.sql

diff --git a/doc/src/sgml/brin.sgml b/doc/src/sgml/brin.sgml
index 434538e..184c886 100644
--- a/doc/src/sgml/brin.sgml
+++ b/doc/src/sgml/brin.sgml
@@ -136,6 +136,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>abstime_minmax_multi_ops</literal></entry>
+     <entry><type>abstime</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int8_bloom_ops</literal></entry>
      <entry><type>bigint</type></entry>
      <entry>
@@ -266,6 +277,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>date_minmax_multi_ops</literal></entry>
+     <entry><type>date</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>float8_bloom_ops</literal></entry>
      <entry><type>double precision</type></entry>
      <entry>
@@ -284,6 +306,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>float8_minmax_multi_ops</literal></entry>
+     <entry><type>double precision</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>inet_bloom_ops</literal></entry>
      <entry><type>inet</type></entry>
      <entry>
@@ -302,6 +335,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>inet_minmax_multi_ops</literal></entry>
+     <entry><type>inet</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>network_inclusion_ops</literal></entry>
      <entry><type>inet</type></entry>
      <entry>
@@ -350,6 +394,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>interval_minmax_multi_ops</literal></entry>
+     <entry><type>interval</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>macaddr_bloom_ops</literal></entry>
      <entry><type>macaddr</type></entry>
      <entry>
@@ -368,6 +423,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>macaddr_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>macaddr8_bloom_ops</literal></entry>
      <entry><type>macaddr8</type></entry>
      <entry>
@@ -386,6 +452,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>macaddr8_minmax_multi_ops</literal></entry>
+     <entry><type>macaddr8</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>name_bloom_ops</literal></entry>
      <entry><type>name</type></entry>
      <entry>
@@ -440,6 +517,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>pg_lsn_minmax_multi_ops</literal></entry>
+     <entry><type>pg_lsn</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>oid_bloom_ops</literal></entry>
      <entry><type>oid</type></entry>
      <entry>
@@ -514,6 +602,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>reltime_minmax_multi_ops</literal></entry>
+     <entry><type>reltime</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>int2_bloom_ops</literal></entry>
      <entry><type>smallint</type></entry>
      <entry>
@@ -579,6 +678,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timestamp_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timestamptz_bloom_ops</literal></entry>
      <entry><type>timestamp with time zone</type></entry>
      <entry>
@@ -597,6 +707,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timestamptz_minmax_multi_ops</literal></entry>
+     <entry><type>timestamp with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>time_bloom_ops</literal></entry>
      <entry><type>time without time zone</type></entry>
      <entry>
@@ -615,6 +736,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>time_minmax_multi_ops</literal></entry>
+     <entry><type>time without time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>timetz_bloom_ops</literal></entry>
      <entry><type>time with time zone</type></entry>
      <entry>
@@ -633,6 +765,17 @@
      </entry>
     </row>
     <row>
+     <entry><literal>timetz_minmax_multi_ops</literal></entry>
+     <entry><type>time with time zone</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
+    <row>
      <entry><literal>uuid_bloom_ops</literal></entry>
      <entry><type>uuid</type></entry>
      <entry>
@@ -650,6 +793,17 @@
       <literal>&gt;</literal>
      </entry>
     </row>
+    <row>
+     <entry><literal>uuid_minmax_multi_ops</literal></entry>
+     <entry><type>uuid</type></entry>
+     <entry>
+      <literal>&lt;</literal>
+      <literal>&lt;=</literal>
+      <literal>=</literal>
+      <literal>&gt;=</literal>
+      <literal>&gt;</literal>
+     </entry>
+    </row>
    </tbody>
   </tgroup>
  </table>
@@ -744,13 +898,13 @@ typedef struct BrinOpcInfo
    </varlistentry>
   </variablelist>
 
-  The core distribution includes support for two types of operator classes:
-  minmax and inclusion.  Operator class definitions using them are shipped for
-  in-core data types as appropriate.  Additional operator classes can be
-  defined by the user for other data types using equivalent definitions,
-  without having to write any source code; appropriate catalog entries being
-  declared is enough.  Note that assumptions about the semantics of operator
-  strategies are embedded in the support procedures' source code.
+  The core distribution includes support for four types of operator classes:
+  minmax, multi-minmax, inclusion and bloom.  Operator class definitions using
+  them are shipped for in-core data types as appropriate.  Additional operator
+  classes can be defined by the user for other data types using equivalent
+  definitions, without having to write any source code; appropriate catalog
+  entries being declared is enough.  Note that assumptions about the semantics
+  of operator strategies are embedded in the support procedures' source code.
  </para>
 
  <para>
@@ -820,6 +974,72 @@ typedef struct BrinOpcInfo
  </table>
 
  <para>
+  The multi-minmax operator class is also intended for data types implementing
+  a totally ordered sets, and may be seen as a simple extension of the minmax
+  operator class. While minmax operator class summarizes values from each block
+  range into a single contiguous interval, multi-minmax allows summarization
+  into multiple smaller intervals to improve handling of outlier values.
+  It is possible to use the multi-minmax support procedures alongside the
+  corresponding operators, as shown in
+  <xref linkend="brin-extensibility-multi-minmax-table"/>.
+  All operator class members (procedures and operators) are mandatory.
+ </para>
+
+ <table id="brin-extensibility-multi-minmax-table">
+  <title>Procedure and Support Numbers for Multi-Minmax Operator Classes</title>
+  <tgroup cols="2">
+   <thead>
+    <row>
+     <entry>Operator class member</entry>
+     <entry>Object</entry>
+    </row>
+   </thead>
+   <tbody>
+    <row>
+     <entry>Support Procedure 1</entry>
+     <entry>internal function <function>brin_minmax_multi_opcinfo()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 2</entry>
+     <entry>internal function <function>brin_minmax_multi_add_value()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 3</entry>
+     <entry>internal function <function>brin_minmax_multi_consistent()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 4</entry>
+     <entry>internal function <function>brin_minmax_multi_union()</function></entry>
+    </row>
+    <row>
+     <entry>Support Procedure 11</entry>
+     <entry>function to compute distance between two values (length of a range)</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 1</entry>
+     <entry>operator less-than</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 2</entry>
+     <entry>operator less-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 3</entry>
+     <entry>operator equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 4</entry>
+     <entry>operator greater-than-or-equal-to</entry>
+    </row>
+    <row>
+     <entry>Operator Strategy 5</entry>
+     <entry>operator greater-than</entry>
+    </row>
+   </tbody>
+  </tgroup>
+ </table>
+
+ <para>
   To write an operator class for a complex data type which has values
   included within another type, it's possible to use the inclusion support
   procedures alongside the corresponding operators, as shown
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_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c
new file mode 100644
index 0000000..cdb2857
--- /dev/null
+++ b/src/backend/access/brin/brin_minmax_multi.c
@@ -0,0 +1,2200 @@
+/*
+ * 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
+ *
+ *
+ * Implements a variant of minmax opclass, where the summary is composed of
+ * multiple smaller intervals. This allows us to handle outliers, which
+ * usually makes the simple minmax opclass inefficient.
+ *
+ * Consider for example page range with simple minmax interval [1000,2000],
+ * and assume a new row gets inserted into the range with value 1000000.
+ * Due to that the interval gets [1000,1000000]. I.e. the minmax interval
+ * got 1000x wider and won't be useful to eliminate scan keys between 2001
+ * and 1000000.
+ *
+ * With multi-minmax opclass, we may have [1000,2000] interval initially,
+ * but after adding the new row we start tracking it as two interval:
+ *
+ *   [1000,2000] and [1000000,1000000]
+ *
+ * This allow us to still eliminate the page range when the scan keys hit
+ * the gap between 2000 and 1000000, making it useful in cases when the
+ * simple minmax opclass gets inefficient.
+ *
+ * The number of intervals tracked per page range is somewhat flexible.
+ * What is restricted is the number of values per page range, and the limit
+ * is currently 64 (see MINMAX_MAX_VALUES). Collapsed intervals (with equal
+ * minimum and maximum value) are stored as a single value, while regular
+ * intervals require two values.
+ *
+ * When the number of values gets too high (by adding new values to the
+ * summary), we merge some of the intervals to free space for more values.
+ * This is done in a greedy way - we simply pick the two closest intervals,
+ * merge them, and repeat this until the number of values to store gets
+ * sufficiently low (below 75% of MINMAX_MAX_VALUES), but that is mostly
+ * arbitrary threshold and may be changed easily).
+ *
+ * To pick the closest intervals we use the "distance" support procedure,
+ * which measures space between two ranges (i.e. length of an interval).
+ * The computed value may be an approximation - in the worst case we will
+ * merge two ranges that are slightly less optimal at that step, but the
+ * index should still produce correct results.
+ *
+ *
+ * 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 "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_amop.h"
+#include "utils/builtins.h"
+#include "utils/date.h"
+#include "utils/datum.h"
+#include "utils/inet.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/nabstime.h"
+#include "utils/numeric.h"
+#include "utils/pg_lsn.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/uuid.h"
+
+/*
+ * Maximum number of values (individual points or range boundaries) to
+ * keep in the summary values.
+ */
+#define		MINMAX_MAX_VALUES	64
+
+/*
+ * Additional SQL level support functions
+ *
+ * Procedure numbers must not use values reserved for BRIN itself; see
+ * brin_internal.h.
+ */
+#define		MINMAX_MAX_PROCNUMS		1	/* maximum support procs we need */
+#define		PROCNUM_DISTANCE		11	/* required, distance between values*/
+
+/*
+ * Subtract this from procnum to obtain index in MinmaxMultiOpaque arrays
+ * (Must be equal to minimum of private procnums).
+ */
+#define		PROCNUM_BASE			11
+
+
+typedef struct MinmaxMultiOpaque
+{
+	FmgrInfo	extra_procinfos[MINMAX_MAX_PROCNUMS];
+	bool		extra_proc_missing[MINMAX_MAX_PROCNUMS];
+	Oid			cached_subtype;
+	FmgrInfo	strategy_procinfos[BTMaxStrategyNumber];
+} MinmaxMultiOpaque;
+
+
+/*
+ * The summary of multi-minmax indexes has two representations - Ranges for
+ * convenient processing, and SerializedRanges for storage in bytea value.
+ *
+ * The Ranges struct stores the boundary values in a single array, but we
+ * treat regular and single-point ranges differently to save space. For
+ * regular ranges (with different boundary values) we have to store both
+ * values, while for "single-point ranges" we only need to save one value.
+ *
+ * The 'values' array stores boundary values for regular ranges first (there
+ * are 2*nranges values to store), and then the nvalues boundary values for
+ * single-point ranges. That is, we have (2*nranges + nvalues) boundary
+ * values in the array.
+ *
+ * +---------------------------------+-------------------------------+
+ * | ranges (sorted pairs of values) | sorted values (single points) |
+ * +---------------------------------+-------------------------------+
+ *
+ * This allows us to quickly add new values, and store outliers without
+ * making the other ranges very wide.
+ *
+ * We never store more than MINMAX_MAX_VALUES values - if needed we perform
+ * compaction by merging some of the ranges.
+ *
+ * To minimize palloc overhead, we always allocate the full array with
+ * space for MINMAX_MAX_VALUES elements. This should be fine as long as
+ * the MINMAX_MAX_VALUES is reasonably small (64 seems fine).
+ */
+typedef struct Ranges
+{
+	/* (2*nranges + nvalues) <= MINMAX_MAX_VALUES */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+
+	/* values stored for this range - either raw values, or ranges */
+	Datum	values[FLEXIBLE_ARRAY_MEMBER];
+} Ranges;
+
+/*
+ * On-disk the summary is stored as a bytea value, represented by the
+ * SerializedRanges structure. It has a 4B varlena header, so can treated
+ * as varlena directly.
+ *
+ * See range_serialize/range_deserialize methods for serialization details.
+ */
+typedef struct SerializedRanges
+{
+	/* varlena header (do not touch directly!) */
+	int32	vl_len_;
+
+	/* (2*nranges + nvalues) <= MINMAX_MAX_VALUES */
+	int		nranges;	/* number of ranges in the array (stored) */
+	int		nvalues;	/* number of values in the data array (all) */
+
+	/* contains the actual data */
+	char	data[FLEXIBLE_ARRAY_MEMBER];
+} SerializedRanges;
+
+static SerializedRanges *range_serialize(Ranges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+static Ranges *range_deserialize(SerializedRanges *range,
+				AttrNumber attno, Form_pg_attribute attr);
+
+/* Cache for support and strategy procesures. */
+
+static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno,
+					   uint16 procnum);
+
+static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc,
+					   uint16 attno, Oid subtype, uint16 strategynum);
+
+
+/*
+ * minmax_multi_init
+ * 		Initialize the deserialized range list, allocate all the memory.
+ *
+ * This is only in-memory representation of the ranges, so we allocate
+ * everything enough space for the maximum number of values (so as not
+ * to have to do repallocs as the ranges grow).
+ */
+static Ranges *
+minmax_multi_init(void)
+{
+	Size				len;
+
+	len = offsetof(Ranges, values);	/* fixed header */
+	len += MINMAX_MAX_VALUES * sizeof(Datum); /* Datum values */
+
+	return (Ranges *) palloc0(len);
+}
+
+/*
+ * range_serialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static SerializedRanges *
+range_serialize(Ranges *range, AttrNumber attno, Form_pg_attribute attr)
+{
+	Size	len;
+	int		nvalues;
+	SerializedRanges *serialized;
+
+	int		i;
+	char   *ptr;
+
+	/* simple sanity checks */
+	Assert(range->nranges >= 0);
+	Assert(range->nvalues >= 0);
+
+	/* see how many Datum values we actually have */
+	nvalues = 2*range->nranges + range->nvalues;
+
+	Assert(2*range->nranges + range->nvalues <= MINMAX_MAX_VALUES);
+
+	/* header is always needed */
+	len = offsetof(SerializedRanges,data);
+
+	/*
+	 * The space needed depends on data type - for fixed-length data types
+	 * (by-value and some by-reference) it's pretty simple, just multiply
+	 * (attlen * nvalues) and we're done. For variable-length by-reference
+	 * types we need to actually walk all the values and sum the lengths.
+	 */
+	if (attr->attlen == -1)	/* varlena */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			len += VARSIZE_ANY(range->values[i]);
+		}
+	}
+	else if (attr->attlen == -2)	/* cstring */
+	{
+		int i;
+		for (i = 0; i < nvalues; i++)
+		{
+			/* don't forget to include the null terminator ;-) */
+			len += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+	}
+	else /* fixed-length types (even by-reference) */
+	{
+		Assert(attr->attlen > 0);
+		len += nvalues * attr->attlen;
+	}
+
+	/*
+	 * Allocate the serialized object, copy the basic information. The
+	 * serialized object is a varlena, so update the header.
+	 */
+	serialized = (SerializedRanges *) palloc0(len);
+	SET_VARSIZE(serialized, len);
+
+	serialized->nranges = range->nranges;
+	serialized->nvalues = range->nvalues;
+
+	/*
+	 * And now copy also the boundary values (like the length calculation
+	 * this depends on the particular data type).
+	 */
+	ptr = serialized->data;	/* start of the serialized data */
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(ptr, &range->values[i], attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			memcpy(ptr, DatumGetPointer(range->values[i]), attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			int tmp = VARSIZE_ANY(DatumGetPointer(range->values[i]));
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			int tmp = strlen(DatumGetPointer(range->values[i])) + 1;
+			memcpy(ptr, DatumGetPointer(range->values[i]), tmp);
+			ptr += tmp;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + len));
+	}
+
+		/* exact size */
+	Assert(ptr == ((char *)serialized + len));
+
+	return serialized;
+}
+
+/*
+ * range_deserialize
+ *	  Serialize the in-memory representation into a compact varlena value.
+ *
+ * Simply copy the header and then also the individual values, as stored
+ * in the in-memory value array.
+ */
+static Ranges *
+range_deserialize(SerializedRanges *serialized,
+				AttrNumber attno, Form_pg_attribute attr)
+{
+	int		i,
+			nvalues;
+	char   *ptr;
+
+	Ranges *range = minmax_multi_init();
+
+	Assert(serialized->nranges >= 0);
+	Assert(serialized->nvalues >= 0);
+
+	nvalues = 2*serialized->nranges + serialized->nvalues;
+
+	Assert(nvalues <= MINMAX_MAX_VALUES);
+
+	/* copy the header info */
+	range->nranges = serialized->nranges;
+	range->nvalues = serialized->nvalues;
+
+	/*
+	 * And now deconstruct the values into Datum array. We don't need
+	 * to copy the values and will instead just point the values to the
+	 * serialized varlena value (assuming it will be kept around).
+	 */
+	ptr = serialized->data;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		if (attr->attbyval)	/* simple by-value data types */
+		{
+			memcpy(&range->values[i], ptr, attr->attlen);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen > 0)	/* fixed-length by-ref types */
+		{
+			/* no copy, just set the value to the pointer */
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += attr->attlen;
+		}
+		else if (attr->attlen == -1)	/* varlena */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += VARSIZE_ANY(DatumGetPointer(range->values[i]));
+		}
+		else if (attr->attlen == -2)	/* cstring */
+		{
+			range->values[i] = PointerGetDatum(ptr);
+			ptr += strlen(DatumGetPointer(range->values[i])) + 1;
+		}
+
+		/* make sure we haven't overflown the buffer end */
+		Assert(ptr <= ((char *)serialized + VARSIZE_ANY(serialized)));
+	}
+
+	/* should have consumed the whole input value exactly */
+	Assert(ptr == ((char *)serialized + VARSIZE_ANY(serialized)));
+
+	/* return the deserialized value */
+	return range;
+}
+
+typedef struct compare_context
+{
+	FmgrInfo   *cmpFn;
+	Oid			colloid;
+} compare_context;
+
+/*
+ * Used to represent ranges expanded during merging and combining (to
+ * reduce number of boundary values to store).
+ *
+ * XXX CombineRange name seems a bit weird. Consider renaming, perhaps to
+ * something ExpandedRange or so.
+ */
+typedef struct CombineRange
+{
+	Datum	minval;		/* lower boundary */
+	Datum	maxval;		/* upper boundary */
+	bool	collapsed;	/* true if minval==maxval */
+} CombineRange;
+
+/*
+ * compare_combine_ranges
+ *	  Compare the combine ranges - first by minimum, then by maximum.
+ *
+ * We do guarantee that ranges in a single Range object do not overlap,
+ * so it may seem strange that we don't order just by minimum. But when
+ * merging two Ranges (which happens in the union function), the ranges
+ * may in fact overlap. So we do compare both.
+ */
+static int
+compare_combine_ranges(const void *a, const void *b, void *arg)
+{
+	CombineRange *ra = (CombineRange *)a;
+	CombineRange *rb = (CombineRange *)b;
+	Datum r;
+
+	compare_context *cxt = (compare_context *)arg;
+
+	/* first compare minvals */
+	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;
+
+	/* then compare maxvals */
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, ra->maxval, rb->maxval);
+
+	if (DatumGetBool(r))
+		return -1;
+
+	r = FunctionCall2Coll(cxt->cmpFn, cxt->colloid, rb->maxval, ra->maxval);
+
+	if (DatumGetBool(r))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * range_contains_value
+ * 		See if the new value is already contained in the range list.
+ *
+ * We first inspect the list of intervals. We use a small trick - we check
+ * the value against min/max of the whole range (min of the first interval,
+ * max of the last one) first, and only inspect the individual intervals if
+ * this passes.
+ *
+ * If the value matches none of the intervals, we check the exact values.
+ * We simply loop through them and invoke equality operator on them.
+ *
+ * XXX This might benefit from the fact that both the intervals and exact
+ * values are sorted - we might do bsearch or something. Currently that
+ * does not make much difference (there are only ~32 intervals), but if
+ * this gets increased and/or the comparator function is more expensive,
+ * it might be a huge win.
+ */
+static bool
+range_contains_value(BrinDesc *bdesc, Oid colloid,
+							AttrNumber attno, Form_pg_attribute attr,
+							Ranges *ranges, Datum newval)
+{
+	int			i;
+	FmgrInfo   *cmpFn;
+	Oid			typid = attr->atttypid;
+
+	/*
+	 * 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.
+		 *
+		 * XXX We simply walk the ranges sequentially, but maybe we could
+		 * further leverage the ordering and non-overlap and use bsearch to
+		 * speed this up a bit.
+		 */
+		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;
+		}
+	}
+
+	/*
+	 * We're done with the ranges, now let's inspect the exact values.
+	 *
+	 * XXX Again, we do sequentially search the values - consider leveraging
+	 * the ordering of values to improve performance.
+	 */
+	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;
+}
+
+/*
+ * insert_value
+ *	  Adds a new value into the single-point part, while maintaining ordering.
+ *
+ * The function inserts the new value to the right place in the single-point
+ * part of the range. It assumes there's enough free space, and then does
+ * essentially an insert-sort.
+ *
+ * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that
+ * only the first nvalues are used.
+ */
+static void
+insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues,
+			 Datum newvalue)
+{
+	int	i;
+	Datum	lt;
+
+	/* If there are no values yet, store the new one and we're done. */
+	if (!nvalues)
+	{
+		values[0] = newvalue;
+		return;
+	}
+
+	/*
+	 * A common case is that the new value is entirely out of the existing
+	 * range, i.e. it's either smaller or larger than all previous values.
+	 * So we check and handle this case first - first we check the larger
+	 * case, because in that case we can just append the value to the end
+	 * of the array and we're done.
+	 */
+
+	/* Is it greater than all existing values in the array? */
+	lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue);
+	if (DatumGetBool(lt))
+	{
+		/* just copy it in-place and we're done */
+		values[nvalues] = newvalue;
+		return;
+	}
+
+	/*
+	 * OK, I lied a bit - we won't check the smaller case explicitly, but
+	 * we'll just compare the value to all existing values in the array.
+	 * But we happen to start with the smallest value, so we're actually
+	 * doing the check anyway.
+	 *
+	 * XXX We do walk the values sequentially. Perhaps we could/should be
+	 * smarter and do some sort of bisection, to improve performance?
+	 */
+	for (i = 0; i < nvalues; i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]);
+		if (DatumGetBool(lt))
+		{
+			/*
+			 * Move values to make space for the new entry, which should go
+			 * to index 'i'. Entries 0 ... (i-1) should stay where they are.
+			 */
+			memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum));
+			values[i] = newvalue;
+			return;
+		}
+	}
+
+	/* We should never really get here. */
+	Assert(false);
+}
+
+/*
+ * Check that the order of the array values is correct, using the cmp
+ * function (which should be BTLessStrategyNumber).
+ */
+static void
+AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues)
+{
+#ifdef USE_ASSERT_CHECKING
+	int	i;
+	Datum lt;
+
+	for (i = 0; i < (nvalues-1); i++)
+	{
+		lt = FunctionCall2Coll(cmp, colloid, values[i], values[i+1]);
+		Assert(DatumGetBool(lt));
+	}
+#endif
+}
+
+/*
+ * Check ordering of boundary values in both parts of the ranges, using
+ * the cmp function (which should be BTLessStrategyNumber).
+ *
+ * XXX We might also check that the ranges and points do not overlap. But
+ * that will break this check sooner or later anyway.
+ */
+static void
+AssertRangeOrdering(FmgrInfo *cmpFn, Oid colloid, Ranges *ranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	/* first the ranges (there are 2*nranges boundary values) */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges);
+
+	/* then the single-point ranges (with nvalues boundar values ) */
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges],
+					 ranges->nvalues);
+#endif
+}
+
+/*
+ * Check that the expanded ranges (built when reducing the number of ranges
+ * by combining some of them) are correctly sorted and do not overlap.
+ */
+static void
+AssertValidCombineRanges(BrinDesc *bdesc, Oid colloid, AttrNumber attno,
+						 Form_pg_attribute attr, CombineRange *ranges,
+						 int nranges)
+{
+#ifdef USE_ASSERT_CHECKING
+	int i;
+	FmgrInfo *eq;
+	FmgrInfo *lt;
+
+	eq = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTEqualStrategyNumber);
+
+	lt = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											BTLessStrategyNumber);
+
+	/*
+	 * Each range independently should be valid, i.e. that for the boundary
+	 * values (lower <= upper).
+	 */
+	for (i = 0; i < nranges; i++)
+	{
+		Datum r;
+		Datum minval = ranges[i].minval;
+		Datum maxval = ranges[i].maxval;
+
+		if (ranges[i].collapsed)	/* collapsed: minval == maxval */
+			r = FunctionCall2Coll(eq, colloid, minval, maxval);
+		else						/* non-collapsed: minval < maxval */
+			r = FunctionCall2Coll(lt, colloid, minval, maxval);
+
+		Assert(DatumGetBool(r));
+	}
+
+	/*
+	 * And the ranges should be ordered and must nor overlap, i.e.
+	 * upper < lower for boundaries of consecutive ranges.
+	 */
+	for (i = 0; i < nranges-1; i++)
+	{
+		Datum r;
+		Datum maxval = ranges[i].maxval;
+		Datum minval = ranges[i+1].minval;
+
+		r = FunctionCall2Coll(lt, colloid, maxval, minval);
+
+		Assert(DatumGetBool(r));
+	}
+#endif
+}
+
+/*
+ * Expand ranges from Ranges into CombineRange array. This expects the
+ * cranges to be pre-allocated and sufficiently large (there needs to be
+ * at least (nranges + nvalues) slots).
+ */
+static void
+fill_combine_ranges(CombineRange *cranges, int ncranges, Ranges *ranges)
+{
+	int idx;
+	int i;
+
+	idx = 0;
+	for (i = 0; i < ranges->nranges; i++)
+	{
+		cranges[idx].minval = ranges->values[2*i];
+		cranges[idx].maxval = ranges->values[2*i+1];
+		cranges[idx].collapsed = false;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	for (i = 0; i < ranges->nvalues; i++)
+	{
+		cranges[idx].minval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].maxval = ranges->values[2*ranges->nranges + i];
+		cranges[idx].collapsed = true;
+		idx++;
+
+		Assert(idx <= ncranges);
+	}
+
+	Assert(idx == ncranges);
+
+	return;
+}
+
+/*
+ * Sort combine ranges using qsort (with BTLessStrategyNumber function).
+ */
+static void
+sort_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					CombineRange *cranges, int ncranges)
+{
+	compare_context cxt;
+
+	/* sort the values */
+	cxt.colloid = colloid;
+	cxt.cmpFn = cmp;
+
+	qsort_arg(cranges, ncranges, sizeof(CombineRange),
+			  compare_combine_ranges, (void *) &cxt);
+}
+
+/*
+ * When combining multiple Range values (in union function), some of the
+ * ranges may overlap. We simply merge the overlapping ranges to fix that.
+ *
+ * XXX This assumes the combine ranges were previously sorted (by minval
+ * and then maxval). We leverage this when detecting overlap.
+ */
+static int
+merge_combine_ranges(FmgrInfo *cmp, Oid colloid,
+					 CombineRange *cranges, int ncranges)
+{
+	int		idx;
+
+	/* TODO: add assert checking the ordering of input ranges */
+
+	/* try merging ranges (idx) and (idx+1) if they overlap */
+	idx = 0;
+	while (idx < (ncranges-1))
+	{
+		Datum	r;
+
+		/*
+		 * comparing [?,maxval] vs. [minval,?] - the ranges overlap
+		 * if (minval < maxval)
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].minval);
+
+		/*
+		 * Nope, maxval < minval, so no overlap. And we know the ranges
+		 * are ordered, so there are no more overlaps, because all the
+		 * remaining ranges have greater or equal minval.
+		 */
+		if (DatumGetBool(r))
+		{
+			/* proceed to the next range */
+			idx += 1;
+			continue;
+		}
+
+		/*
+		 * So ranges 'idx' and 'idx+1' do overlap, but we don't know if
+		 * 'idx+1' is contained in 'idx', or if they only overlap only
+		 * partially. So compare the upper bounds and keep the larger one.
+		 */
+		r = FunctionCall2Coll(cmp, colloid,
+							  cranges[idx].maxval,
+							  cranges[idx+1].maxval);
+
+		if (DatumGetBool(r))
+			cranges[idx].maxval = cranges[idx+1].maxval;
+
+		/*
+		 * The range certainly is no longer collapsed (irrespectedly of
+		 * the previous state).
+		 */
+		cranges[idx].collapsed = false;
+
+		/*
+		 * Now get rid of the (idx+1) range entirely by shifting the
+		 * remaining ranges by 1. There are ncranges elements, and we
+		 * need to move elements from (idx+2). That means the number
+		 * of elements to move is [ncranges - (idx+2)].
+		 */
+		memmove(&cranges[idx+1], &cranges[idx+2],
+				(ncranges - (idx + 2)) * sizeof(CombineRange));
+
+		/*
+		 * Decrease the number of ranges, and repeat (with the same range,
+		 * as it might overlap with additional ranges thanks to the merge).
+		 */
+		ncranges--;
+	}
+
+	/* TODO: add assert checking the ordering etc. of produced ranges */
+
+	return ncranges;
+}
+
+/*
+ * Represents a distance between two ranges (identified by index into
+ * an array of combine ranges).
+ */
+typedef struct DistanceValue
+{
+	int		index;
+	double	value;
+} DistanceValue;
+
+/*
+ * Simple comparator for distance values, comparing the double value.
+ */
+static int
+compare_distances(const void *a, const void *b)
+{
+	DistanceValue *da = (DistanceValue *)a;
+	DistanceValue *db = (DistanceValue *)b;
+
+	if (da->value < db->value)
+		return -1;
+	else if (da->value > db->value)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Given an array of combine ranges, compute distance of the gaps betwen
+ * the ranges - for ncranges there are (ncranges-1) gaps.
+ *
+ * We simply call the "distance" function to compute the (min-max) for pairs
+ * of consecutive ganges. The function may be fairly expensive, so we do that
+ * just once (and then use it to pick as many ranges to merge as possible).
+ *
+ * See reduce_combine_ranges for details.
+ */
+static DistanceValue *
+build_distances(FmgrInfo *distanceFn, Oid colloid,
+				CombineRange *cranges, int ncranges)
+{
+	int i;
+	DistanceValue *distances;
+
+	Assert(ncranges >= 2);
+
+	distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * (ncranges - 1));
+
+	/*
+	 * Walk though the ranges once and compute distance between the ranges
+	 * so that we can sort them once.
+	 */
+	for (i = 0; i < (ncranges-1); i++)
+	{
+		Datum a1, a2, r;
+
+		a1 = cranges[i].maxval;
+		a2 = cranges[i+1].minval;
+
+		/* compute length of the empty gap (distance between max/min) */
+		r = FunctionCall2Coll(distanceFn, colloid, a1, a2);
+
+		/* remember the index */
+		distances[i].index = i;
+		distances[i].value = DatumGetFloat8(r);
+	}
+
+	/* sort the distances in ascending order */
+	pg_qsort(distances, (ncranges-1), sizeof(DistanceValue), compare_distances);
+
+	return distances;
+}
+
+/*
+ * Builds combine ranges for the existing ranges (and single-point ranges),
+ * and also the new value (which did not fit into the array).
+ * 
+ * XXX We do perform qsort on all the values, but we could also leverage
+ * the fact that the input data is already sorted and do merge sort.
+ */
+static CombineRange *
+build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges,
+					 Datum newvalue, int *nranges)
+{
+	int				ncranges;
+	CombineRange   *cranges;
+
+	/* now do the actual merge sort */
+	ncranges = ranges->nranges + ranges->nvalues + 1;
+	cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange));
+	*nranges = ncranges;
+
+	/* put the new value at the beginning */
+	cranges[0].minval = newvalue;
+	cranges[0].maxval = newvalue;
+	cranges[0].collapsed = true;
+
+	/* then the regular and collapsed ranges */
+	fill_combine_ranges(&cranges[1], ncranges-1, ranges);
+
+	/* and sort the ranges */
+	sort_combine_ranges(cmp, colloid, cranges, ncranges);
+
+	return cranges;
+}
+
+/*
+ * Counts bondary values needed to store the ranges. Each single-point
+ * range is stored using a single value, each regular range needs two.
+ */
+static int
+count_values(CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	count;
+
+	count = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+			count += 1;
+		else
+			count += 2;
+	}
+
+	return count;
+}
+
+/*
+ * Combines ranges until the number of boundary values drops below 75%
+ * of the capacity (MINMAX_MAX_VALUES).
+ *
+ * XXX The ranges to merge are selected solely using the distance. But
+ * that may not be the best strategy, for example when multiple gaps
+ * are of equal (or very similar) length.
+ *
+ * Consider for example points 1, 2, 3, .., 64, which have gaps of the
+ * same length 1 of course. In that case we tend to pick the first
+ * gap of that length, which leads to this:
+ *
+ *    step 1:  [1, 2], 3, 4, 5, .., 64
+ *    step 2:  [1, 3], 4, 5,    .., 64
+ *    step 3:  [1, 4], 5,       .., 64
+ *    ...
+ *
+ * So in the end we'll have one "large" range and multiple small points.
+ * That may be fine, but it seems a bit strange and non-optimal. Maybe
+ * we should consider other things when picking ranges to merge - e.g.
+ * length of the ranges? Or perhaps randomize the choice of ranges, with
+ * probability inversely proportional to the distance (the gap lengths
+ * may be very close, but not exactly the same).
+ */
+static int
+reduce_combine_ranges(CombineRange *cranges, int ncranges,
+					  DistanceValue *distances)
+{
+	int i;
+	int	ndistances = (ncranges - 1);
+
+	/*
+	 * We have one fewer 'gaps' than the ranges. We'll be decrementing
+	 * the number of combine ranges (reduction is the primary goal of
+	 * this function), so we must use a separate value.
+	 */
+	for (i = 0; i < ndistances; i++)
+	{
+		int j;
+		int shortest;
+
+		if (count_values(cranges, ncranges) <= MINMAX_MAX_VALUES * 0.75)
+			break;
+
+		shortest = distances[i].index;
+
+		/*
+		 * The index must be still valid with respect to the current size
+		 * of cranges array (and it always points to the first range, so
+		 * never to the last one - hence the -1 in the condition).
+		 */
+		Assert(shortest < (ncranges - 1));
+
+		/*
+		 * Move the values to join the two selected ranges. The new range is
+		 * definiely not collapsed but a regular range.
+		 */
+		cranges[shortest].maxval = cranges[shortest+1].maxval;
+		cranges[shortest].collapsed = false;
+
+		/* shuffle the subsequent combine ranges */
+		memmove(&cranges[shortest+1], &cranges[shortest+2],
+				(ncranges - shortest - 2) * sizeof(CombineRange));
+
+		/* also, shuffle all higher indexes (we've just moved the ranges) */
+		for (j = i; j < ndistances; j++)
+		{
+			if (distances[j].index > shortest)
+				distances[j].index--;
+		}
+
+		ncranges--;
+
+		Assert(ncranges > 0);
+	}
+
+	return ncranges;
+}
+
+/*
+ * Store the boundary values from CombineRanges back into Range (using
+ * only the minimal number of values needed).
+ */
+static void
+store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges)
+{
+	int	i;
+	int	idx = 0;
+
+	/* first copy in the regular ranges */
+	ranges->nranges = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (!cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->values[idx++] = cranges[i].maxval;
+			ranges->nranges++;
+		}
+	}
+
+	/* now copy in the collapsed ones */
+	ranges->nvalues = 0;
+	for (i = 0; i < ncranges; i++)
+	{
+		if (cranges[i].collapsed)
+		{
+			ranges->values[idx++] = cranges[i].minval;
+			ranges->nvalues++;
+		}
+	}
+}
+
+/*
+ * range_add_value
+ * 		Add the new value to the multi-minmax range.
+ */
+static bool
+range_add_value(BrinDesc *bdesc, Oid colloid,
+				AttrNumber attno, Form_pg_attribute attr,
+				Ranges *ranges, Datum newval)
+{
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+
+	/* combine ranges */
+	CombineRange   *cranges;
+	int				ncranges;
+	DistanceValue  *distances;
+
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(2*ranges->nranges + ranges->nvalues <= MINMAX_MAX_VALUES);
+
+	/*
+	 * Bail out if the value already is covered by the range.
+	 *
+	 * We could also add values until we hit MINMAX_MAX_VALUES, and then
+	 * do the deduplication in a batch, hoping for better efficiency. But
+	 * that would mean we actually modify the range every time, which means
+	 * having to serialize the value, which does palloc, walks the values,
+	 * copies them, etc. Not exactly cheap.
+	 *
+	 * So instead we do the check, which should be fairly cheap - assuming
+	 * the comparator function is not very expensive.
+	 *
+	 * This also implies means the values array can't contain duplicities.
+	 */
+	if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval))
+		return false;
+
+	/*
+	 * If there's space in the values array, copy it in and we're done.
+	 *
+	 * We do want to keep the values sorted (to speed up searches), so we
+	 * do a simple insertion sort. We could do something more elaborate,
+	 * e.g. by sorting the values only now and then, but for small counts
+	 * (e.g. when MINMAX_MAX_VALUES is 64) this should be fine.
+	 */
+	if (2*ranges->nranges + ranges->nvalues < MINMAX_MAX_VALUES)
+	{
+		FmgrInfo   *cmpFn;
+		Datum	   *values;
+
+		cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+												   BTLessStrategyNumber);
+
+		/* beginning of the 'single value' part (for convenience) */
+		values = &ranges->values[2*ranges->nranges];
+
+		insert_value(cmpFn, colloid, values, ranges->nvalues, newval);
+
+		ranges->nvalues++;
+
+		/*
+		 * Check we haven't broken the ordering of boundary values (checks
+		 * both parts, but that doesn't hurt).
+		 */
+		AssertRangeOrdering(cmpFn, colloid, ranges);
+
+		/* yep, we've modified the range */
+		return true;
+	}
+
+	/*
+	 * Damn - the new value is not in the range yet, but we don't have space
+	 * to just insert it. So we need to combine some of the existing ranges,
+	 * to reduce the number of values we need to store (joining two intervals
+	 * reduces the number of boundaries to store by 2).
+	 *
+	 * To do that we first construct an array of CombineRange items - each
+	 * combine range tracks if it's a regular range or collapsed range, where
+	 * "collapsed" means "single point."
+	 *
+	 * Existing ranges (we have ranges->nranges of them) map to combine ranges
+	 * directly, while single points (ranges->nvalues of them) have to be
+	 * expanded. We neet the combine ranges to be sorted, and we do that by
+	 * performing a merge sort of ranges, values and new value.
+	 */
+
+	/* we'll certainly need the comparator, so just look it up now */
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											   BTLessStrategyNumber);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	/* and we'll also need the 'distance' procedure */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* OK build the combine ranges */
+	cranges = build_combine_ranges(cmpFn, colloid, ranges, newval, &ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * Combine ranges until we release at least 25% of the space. This
+	 * threshold is somewhat arbitrary, perhaps needs tuning. We must not
+	 * use too low or high value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances);
+
+	Assert(count_values(cranges, ncranges) <= MINMAX_MAX_VALUES * 0.75);
+
+	/* decompose the combine ranges into regular ranges and single values */
+	store_combine_ranges(ranges, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* Check the ordering invariants are not violated (for both parts). */
+	AssertArrayOrder(cmpFn, colloid, ranges->values, ranges->nranges*2);
+	AssertArrayOrder(cmpFn, colloid, &ranges->values[ranges->nranges*2],
+					 ranges->nvalues);
+
+	return true;
+}
+
+
+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);
+}
+
+/*
+ * Compute distance between two float4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float4(PG_FUNCTION_ARGS)
+{
+	float a1 = PG_GETARG_FLOAT4(0);
+	float a2 = PG_GETARG_FLOAT4(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two float8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_float8(PG_FUNCTION_ARGS)
+{
+	double a1 = PG_GETARG_FLOAT8(0);
+	double a2 = PG_GETARG_FLOAT8(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8(a2 - a1);
+}
+
+/*
+ * Compute distance between two int2 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int2(PG_FUNCTION_ARGS)
+{
+	int16	a1 = PG_GETARG_INT16(0);
+	int16	a2 = PG_GETARG_INT16(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int4 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int4(PG_FUNCTION_ARGS)
+{
+	int32	a1 = PG_GETARG_INT32(0);
+	int32	a2 = PG_GETARG_INT32(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two int8 values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_int8(PG_FUNCTION_ARGS)
+{
+	int64	a1 = PG_GETARG_INT64(0);
+	int64	a2 = PG_GETARG_INT64(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(a1 <= a2);
+
+	PG_RETURN_FLOAT8((double) a2 - (double) a1);
+}
+
+/*
+ * Compute distance between two tid values (by mapping them to float8
+ * and then subtracting them).
+ */
+Datum
+brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS)
+{
+	double	da1,
+			da2;
+
+	ItemPointer	pa1 = (ItemPointer) PG_GETARG_DATUM(0);
+	ItemPointer	pa2 = (ItemPointer) PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(ItemPointerCompare(pa1, pa2) <= 0);
+
+	da1 = ItemPointerGetBlockNumber(pa1) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa1);
+
+	da2 = ItemPointerGetBlockNumber(pa2) * MaxHeapTuplesPerPage +
+		  ItemPointerGetOffsetNumber(pa2);
+
+	PG_RETURN_FLOAT8(da2 - da1);
+}
+
+/*
+ * Comutes distance between two numeric values (plain subtraction).
+ */
+Datum
+brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS)
+{
+	Datum	d;
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(numeric_le, a1, a2)));
+
+	d = DirectFunctionCall2(numeric_sub, a2, a1);	/* a2 - a1 */
+
+	PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d));
+}
+
+/*
+ * Computes approximate distance between two UUID values.
+ *
+ * XXX We do not need a perfectly accurate value, so we approximate the
+ * deltas (which would have to be 128-bit integers) with a 64-bit float.
+ * The small inaccuracies do not matter in practice, in the worst case
+ * we'll decide to merge ranges that are not the closest ones.
+ */
+Datum
+brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
+{
+	int		i;
+	double	delta = 0;
+
+	Datum	a1 = PG_GETARG_DATUM(0);
+	Datum	a2 = PG_GETARG_DATUM(1);
+
+	pg_uuid_t  *u1 = DatumGetUUIDP(a1);
+	pg_uuid_t  *u2 = DatumGetUUIDP(a2);
+
+	/*
+	 * We know the values are range boundaries, but the range may be
+	 * collapsed (i.e. single points), with equal values.
+	 */
+	Assert(DatumGetBool(DirectFunctionCall2(uuid_le, a1, a2)));
+
+	/* compute approximate delta as a double precision value */
+	for (i = UUID_LEN-1; i >= 0; i--)
+	{
+		delta += (int) u2->data[i] - (int) u1->data[i];
+		delta /= 256;
+	}
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two time (without tz) values.
+ *
+ * TimeADT is just an int64, so we simply subtract the values directly.
+ */
+Datum
+brin_minmax_multi_distance_time(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT	ta = PG_GETARG_TIMEADT(0);
+	TimeADT	tb = PG_GETARG_TIMEADT(1);
+
+	delta = (tb - ta);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes approximate distance between two timetz values.
+ *
+ * Simply subtracts the TimeADT (int64) values embedded in TimeTzADT.
+ *
+ * XXX Does this need to consider the time zones?
+ */
+Datum
+brin_minmax_multi_distance_timetz(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeTzADT  *ta = PG_GETARG_TIMETZADT_P(0);
+	TimeTzADT  *tb = PG_GETARG_TIMETZADT_P(1);
+
+	delta = tb->time - ta->time;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Computes distance between two interval values.
+ *
+ * Intervals are internally just 'time' values, so use the same approach
+ * as for in brin_minmax_multi_distance_time.
+ *
+ * XXX Do we actually need two separate functions, then?
+ */
+Datum
+brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	TimeADT		ia = PG_GETARG_TIMEADT(0);
+	TimeADT		ib = PG_GETARG_TIMEADT(1);
+
+	delta = (ib - ia);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two pg_lsn values.
+ *
+ * LSN is just an int64 encoding position in the stream, so just subtract
+ * those int64 values directly.
+ */
+Datum
+brin_minmax_multi_distance_pg_lsn(PG_FUNCTION_ARGS)
+{
+	double	delta = 0;
+
+	XLogRecPtr	lsna = PG_GETARG_LSN(0);
+	XLogRecPtr	lsnb = PG_GETARG_LSN(1);
+
+	delta = (lsnb - lsna);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+/*
+ * Compute distance between two macaddr values.
+ *
+ * mac addresses are treated as 6 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr    *a = PG_GETARG_MACADDR_P(0);
+	macaddr    *b = PG_GETARG_MACADDR_P(1);
+
+	delta = ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+
+/*
+ * Compute distance between two macaddr8 values.
+ *
+ * macaddr8 addresses are 8 unsigned chars, so do the same thing we
+ * already do for UUID values.
+ */
+Datum
+brin_minmax_multi_distance_macaddr8(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	macaddr8   *a = PG_GETARG_MACADDR8_P(0);
+	macaddr8   *b = PG_GETARG_MACADDR8_P(1);
+
+	delta = ((double)b->h - (double)a->h);
+	delta /= 256;
+
+	delta += ((double)b->g - (double)a->g);
+	delta /= 256;
+
+	delta += ((double)b->f - (double)a->f);
+	delta /= 256;
+
+	delta += ((double)b->e - (double)a->e);
+	delta /= 256;
+
+	delta += ((double)b->d - (double)a->d);
+	delta /= 256;
+
+	delta += ((double)b->c - (double)a->c);
+	delta /= 256;
+
+	delta += ((double)b->b - (double)a->b);
+	delta /= 256;
+
+	delta += ((double)b->a - (double)a->a);
+	delta /= 256;
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+
+/*
+ * Compute distance between two inet values.
+ *
+ * The distance is defined as difference between 32-bit/128-bit values,
+ * depending on the IP version. The distance is computed by subtracting
+ * the bytes and normalizing it to [0,1] range for each IP family.
+ * Addresses from difference families are consider to be in maximum
+ * distance, which is 1.0.
+ *
+ * XXX Does this need to consider the mask (bits)? For now it's ignored.
+ */
+Datum
+brin_minmax_multi_distance_inet(PG_FUNCTION_ARGS)
+{
+	double			delta;
+	int				i;
+	int				len;
+	unsigned char  *addra,
+				   *addrb;
+
+	inet	   *ipa = PG_GETARG_INET_PP(0);
+	inet	   *ipb = PG_GETARG_INET_PP(1);
+
+	/*
+	 * If the addresses are from different families, consider them to be
+	 * in maximal possible distance (which is 1.0).
+	 */
+	if (ip_family(ipa) != ip_family(ipb))
+		return 1.0;
+
+	/* ipv4 or ipv6 */
+	if (ip_family(ipa) == PGSQL_AF_INET)
+		len = 4;
+	else
+		len = 16; /* NS_IN6ADDRSZ */
+
+	addra = ip_addr(ipa);
+	addrb = ip_addr(ipb);
+
+	delta = 0;
+	for (i = len-1; i >= 0; i--)
+	{
+		delta += (double)addrb[i] - (double)addra[i];
+		delta /= 256;
+	}
+
+	Assert((delta >= 0) && (delta <= 1));
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+
+/*
+ * Compute distance between two abstime values.
+ *
+ * abstime values are int32 values, so just subtract them
+ */
+Datum
+brin_minmax_multi_distance_abstime(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	AbsoluteTime a = PG_GETARG_ABSOLUTETIME(0);
+	AbsoluteTime b = PG_GETARG_ABSOLUTETIME(1);
+
+	delta = ((double)b - (double)a);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+
+/*
+ * Compute distance between two reltime values.
+ *
+ * reltime values are int32 values, so just subtract them
+ */
+Datum
+brin_minmax_multi_distance_reltime(PG_FUNCTION_ARGS)
+{
+	double	delta;
+
+	RelativeTime a = PG_GETARG_ABSOLUTETIME(0);
+	RelativeTime b = PG_GETARG_ABSOLUTETIME(1);
+
+	delta = ((double)b - (double)a);
+
+	Assert(delta >= 0);
+
+	PG_RETURN_FLOAT8(delta);
+}
+
+
+/*
+ * 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		modified = false;
+	Form_pg_attribute attr;
+	AttrNumber	attno;
+	Ranges *ranges;
+	SerializedRanges *serialized = NULL;
+
+	/*
+	 * 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();
+		column->bv_allnulls = false;
+		modified = true;
+	}
+	else
+	{
+		serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+		ranges = range_deserialize(serialized, attno, attr);
+	}
+
+	/*
+	 * Try to add the new value to the range. We need to update the modified
+	 * flag, so that we serialize the correct value.
+	 */
+	modified |= range_add_value(bdesc, colloid, attno, attr, ranges, newval);
+
+	if (modified)
+	{
+		SerializedRanges *s = range_serialize(ranges, attno, attr);
+		column->bv_values[0] = PointerGetDatum(s);
+
+		/*
+		 * XXX pfree must happen after range_serialize, because the Ranges value
+		 * may reference the original serialized value.
+		 */
+		if (serialized)
+			pfree(serialized);
+	}
+
+	pfree(ranges);
+
+	PG_RETURN_BOOL(modified);
+}
+
+/*
+ * 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;
+	SerializedRanges *serialized;
+	Ranges		*ranges;
+	int			keyno;
+	int			rangeno;
+	int			i;
+	Form_pg_attribute attr;
+
+	attno = column->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]);
+	ranges = range_deserialize(serialized, attno, attr);
+
+	/* 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];
+
+			/* NULL keys are handled and filtered-out in bringetbitmap */
+			Assert(!(key->sk_flags & SK_ISNULL));
+
+			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, val, 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));
+	}
+
+	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)
+{
+	BrinDesc   *bdesc = (BrinDesc *) PG_GETARG_POINTER(0);
+	BrinValues *col_a = (BrinValues *) PG_GETARG_POINTER(1);
+	BrinValues *col_b = (BrinValues *) PG_GETARG_POINTER(2);
+	Oid			colloid = PG_GET_COLLATION();
+	SerializedRanges   *serialized_a;
+	SerializedRanges   *serialized_b;
+	Ranges	   *ranges_a;
+	Ranges	   *ranges_b;
+	AttrNumber	attno;
+	Form_pg_attribute attr;
+	CombineRange *cranges;
+	int			ncranges;
+	FmgrInfo   *cmpFn,
+			   *distanceFn;
+	DistanceValue  *distances;
+	MemoryContext	ctx;
+	MemoryContext	oldctx;
+
+	Assert(col_a->bv_attno == col_b->bv_attno);
+
+	/* Adjust "hasnulls" */
+	if (!col_a->bv_hasnulls && col_b->bv_hasnulls)
+		col_a->bv_hasnulls = true;
+
+	/* If there are no values in B, there's nothing left to do */
+	if (col_b->bv_allnulls)
+		PG_RETURN_VOID();
+
+	attno = col_a->bv_attno;
+	attr = TupleDescAttr(bdesc->bd_tupdesc, attno - 1);
+
+	/*
+	 * Adjust "allnulls".  If A doesn't have values, just copy the values from
+	 * B into A, and we're done.  We cannot run the operators in this case,
+	 * because values in A might contain garbage.  Note we already established
+	 * that B contains values.
+	 */
+	if (col_a->bv_allnulls)
+	{
+		col_a->bv_allnulls = false;
+		col_a->bv_values[0] = datumCopy(col_b->bv_values[0], false, -1);
+		PG_RETURN_VOID();
+	}
+
+	serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]);
+	serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]);
+
+	ranges_a = range_deserialize(serialized_a, attno, attr);
+	ranges_b = range_deserialize(serialized_b, attno, attr);
+
+	/* make sure neither of the ranges is NULL */
+	Assert(ranges_a && ranges_b);
+
+	ncranges = (ranges_a->nranges + ranges_a->nvalues) +
+			   (ranges_b->nranges + ranges_b->nvalues);
+
+	/*
+	 * The distanceFn calls (which may internally call e.g. numeric_le) may
+	 * allocate quite a bit of memory, and we must not leak it. Otherwise
+	 * we'd have problems e.g. when building indexes. So we create a local
+	 * memory context and make sure we free the memory before leaving this
+	 * function (not after every call).
+	 */
+	ctx = AllocSetContextCreate(CurrentMemoryContext,
+								"minmax-multi context",
+								ALLOCSET_DEFAULT_SIZES);
+
+	oldctx = MemoryContextSwitchTo(ctx);
+
+	/* allocate and fill */
+	cranges = (CombineRange *)palloc0(ncranges * sizeof(CombineRange));
+
+	/* fill the combine ranges with entries for the first range */
+	fill_combine_ranges(cranges, ranges_a->nranges + ranges_a->nvalues,
+						ranges_a);
+
+	/* and now add combine ranges for the second range */
+	fill_combine_ranges(&cranges[ranges_a->nranges + ranges_a->nvalues],
+						ranges_b->nranges + ranges_b->nvalues,
+						ranges_b);
+
+	cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid,
+											 BTLessStrategyNumber);
+
+	/* sort the combine ranges */
+	sort_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/*
+	 * We've merged two different lists of ranges, so some of them may be
+	 * overlapping. So walk through them and merge them.
+	 */
+	ncranges = merge_combine_ranges(cmpFn, colloid, cranges, ncranges);
+
+	/* check that the combine ranges are correct (no overlaps, ordering) */
+	AssertValidCombineRanges(bdesc, colloid, attno, attr, cranges, ncranges);
+
+	/* build array of gap distances and sort them in ascending order */
+	distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE);
+	distances = build_distances(distanceFn, colloid, cranges, ncranges);
+
+	/*
+	 * See how many values would be needed to store the current ranges, and if
+	 * needed combine as many off them to get below the MINMAX_MAX_VALUES
+	 * threshold. The collapsed ranges will be stored as a single value.
+	 */
+	ncranges = reduce_combine_ranges(cranges, ncranges, distances);
+
+	/* update the first range summary */
+	store_combine_ranges(ranges_a, cranges, ncranges);
+
+	MemoryContextSwitchTo(oldctx);
+	MemoryContextDelete(ctx);
+
+	/* cleanup and update the serialized value */
+	pfree(serialized_a);
+	col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a, attno, attr));
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Cache and return minmax multi opclass support procedure
+ *
+ * Return the procedure corresponding to the given function support number
+ * or null if it is not exists.
+ */
+static FmgrInfo *
+minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, uint16 procnum)
+{
+	MinmaxMultiOpaque *opaque;
+	uint16		basenum = procnum - PROCNUM_BASE;
+
+	/*
+	 * We cache these in the opaque struct, to avoid repetitive syscache
+	 * lookups.
+	 */
+	opaque = (MinmaxMultiOpaque *) bdesc->bd_info[attno - 1]->oi_opaque;
+
+	/*
+	 * If we already searched for this proc and didn't find it, don't bother
+	 * searching again.
+	 */
+	if (opaque->extra_proc_missing[basenum])
+		return NULL;
+
+	if (opaque->extra_procinfos[basenum].fn_oid == InvalidOid)
+	{
+		if (RegProcedureIsValid(index_getprocid(bdesc->bd_index, attno,
+												procnum)))
+		{
+			fmgr_info_copy(&opaque->extra_procinfos[basenum],
+						   index_getprocinfo(bdesc->bd_index, attno, procnum),
+						   bdesc->bd_context);
+		}
+		else
+		{
+			opaque->extra_proc_missing[basenum] = true;
+			return NULL;
+		}
+	}
+
+	return &opaque->extra_procinfos[basenum];
+}
+
+/*
+ * Cache and return the procedure for the given strategy.
+ *
+ * Note: this function mirrors minmax_multi_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 84dc6e3..be2eba8 100644
--- a/src/include/catalog/pg_amop.h
+++ b/src/include/catalog/pg_amop.h
@@ -974,6 +974,52 @@ DATA(insert (	4054	 23   20 2 s		80	  3580 0 ));
 DATA(insert (	4054	 23   20 3 s		15	  3580 0 ));
 DATA(insert (	4054	 23   20 4 s		82	  3580 0 ));
 DATA(insert (	4054	 23   20 5 s		76	  3580 0 ));
+/* minmax multi integer */
+DATA(insert (	4126	 20   20 1 s	   412	  3580 0 ));
+DATA(insert (	4126	 20   20 2 s	   414	  3580 0 ));
+DATA(insert (	4126	 20   20 3 s	   410	  3580 0 ));
+DATA(insert (	4126	 20   20 4 s	   415	  3580 0 ));
+DATA(insert (	4126	 20   20 5 s	   413	  3580 0 ));
+DATA(insert (	4126	 20   21 1 s	  1870	  3580 0 ));
+DATA(insert (	4126	 20   21 2 s	  1872	  3580 0 ));
+DATA(insert (	4126	 20   21 3 s	  1868	  3580 0 ));
+DATA(insert (	4126	 20   21 4 s	  1873	  3580 0 ));
+DATA(insert (	4126	 20   21 5 s	  1871	  3580 0 ));
+DATA(insert (	4126	 20   23 1 s	   418	  3580 0 ));
+DATA(insert (	4126	 20   23 2 s	   420	  3580 0 ));
+DATA(insert (	4126	 20   23 3 s	   416	  3580 0 ));
+DATA(insert (	4126	 20   23 4 s	   430	  3580 0 ));
+DATA(insert (	4126	 20   23 5 s	   419	  3580 0 ));
+DATA(insert (	4126	 21   21 1 s		95	  3580 0 ));
+DATA(insert (	4126	 21   21 2 s	   522	  3580 0 ));
+DATA(insert (	4126	 21   21 3 s		94	  3580 0 ));
+DATA(insert (	4126	 21   21 4 s	   524	  3580 0 ));
+DATA(insert (	4126	 21   21 5 s	   520	  3580 0 ));
+DATA(insert (	4126	 21   20 1 s	  1864	  3580 0 ));
+DATA(insert (	4126	 21   20 2 s	  1866	  3580 0 ));
+DATA(insert (	4126	 21   20 3 s	  1862	  3580 0 ));
+DATA(insert (	4126	 21   20 4 s	  1867	  3580 0 ));
+DATA(insert (	4126	 21   20 5 s	  1865	  3580 0 ));
+DATA(insert (	4126	 21   23 1 s	   534	  3580 0 ));
+DATA(insert (	4126	 21   23 2 s	   540	  3580 0 ));
+DATA(insert (	4126	 21   23 3 s	   532	  3580 0 ));
+DATA(insert (	4126	 21   23 4 s	   542	  3580 0 ));
+DATA(insert (	4126	 21   23 5 s	   536	  3580 0 ));
+DATA(insert (	4126	 23   23 1 s		97	  3580 0 ));
+DATA(insert (	4126	 23   23 2 s	   523	  3580 0 ));
+DATA(insert (	4126	 23   23 3 s		96	  3580 0 ));
+DATA(insert (	4126	 23   23 4 s	   525	  3580 0 ));
+DATA(insert (	4126	 23   23 5 s	   521	  3580 0 ));
+DATA(insert (	4126	 23   21 1 s	   535	  3580 0 ));
+DATA(insert (	4126	 23   21 2 s	   541	  3580 0 ));
+DATA(insert (	4126	 23   21 3 s	   533	  3580 0 ));
+DATA(insert (	4126	 23   21 4 s	   543	  3580 0 ));
+DATA(insert (	4126	 23   21 5 s	   537	  3580 0 ));
+DATA(insert (	4126	 23   20 1 s		37	  3580 0 ));
+DATA(insert (	4126	 23   20 2 s		80	  3580 0 ));
+DATA(insert (	4126	 23   20 3 s		15	  3580 0 ));
+DATA(insert (	4126	 23   20 4 s		82	  3580 0 ));
+DATA(insert (	4126	 23   20 5 s		76	  3580 0 ));
 /* bloom integer */
 DATA(insert (	5024	 20   20 1 s	   410	  3580 0 ));
 DATA(insert (	5024	 20   21 1 s	  1868	  3580 0 ));
@@ -999,6 +1045,12 @@ DATA(insert (	4068	 26   26 2 s	   611	  3580 0 ));
 DATA(insert (	4068	 26   26 3 s	   607	  3580 0 ));
 DATA(insert (	4068	 26   26 4 s	   612	  3580 0 ));
 DATA(insert (	4068	 26   26 5 s	   610	  3580 0 ));
+/* minmax oid */
+DATA(insert (	4127	 26   26 1 s	   609	  3580 0 ));
+DATA(insert (	4127	 26   26 2 s	   611	  3580 0 ));
+DATA(insert (	4127	 26   26 3 s	   607	  3580 0 ));
+DATA(insert (	4127	 26   26 4 s	   612	  3580 0 ));
+DATA(insert (	4127	 26   26 5 s	   610	  3580 0 ));
 /* bloom oid */
 DATA(insert (	5029	 26   26 1 s	   607	  3580 0 ));
 /* minmax tid */
@@ -1007,6 +1059,12 @@ DATA(insert (	4069	 27   27 2 s	  2801	  3580 0 ));
 DATA(insert (	4069	 27   27 3 s	   387	  3580 0 ));
 DATA(insert (	4069	 27   27 4 s	  2802	  3580 0 ));
 DATA(insert (	4069	 27   27 5 s	  2800	  3580 0 ));
+/* minmax multi tid */
+DATA(insert (	4128	 27   27 1 s	  2799	  3580 0 ));
+DATA(insert (	4128	 27   27 2 s	  2801	  3580 0 ));
+DATA(insert (	4128	 27   27 3 s	   387	  3580 0 ));
+DATA(insert (	4128	 27   27 4 s	  2802	  3580 0 ));
+DATA(insert (	4128	 27   27 5 s	  2800	  3580 0 ));
 /* minmax float (float4, float8) */
 DATA(insert (	4070	700  700 1 s	   622	  3580 0 ));
 DATA(insert (	4070	700  700 2 s	   624	  3580 0 ));
@@ -1028,6 +1086,27 @@ DATA(insert (	4070	701  701 2 s	   673	  3580 0 ));
 DATA(insert (	4070	701  701 3 s	   670	  3580 0 ));
 DATA(insert (	4070	701  701 4 s	   675	  3580 0 ));
 DATA(insert (	4070	701  701 5 s	   674	  3580 0 ));
+/* minmax multi (float8) */
+DATA(insert (	4005	700  700 1 s	   622	  3580 0 ));
+DATA(insert (	4005	700  700 2 s	   624	  3580 0 ));
+DATA(insert (	4005	700  700 3 s	   620	  3580 0 ));
+DATA(insert (	4005	700  700 4 s	   625	  3580 0 ));
+DATA(insert (	4005	700  700 5 s	   623	  3580 0 ));
+DATA(insert (	4005	700  701 1 s	  1122	  3580 0 ));
+DATA(insert (	4005	700  701 2 s	  1124	  3580 0 ));
+DATA(insert (	4005	700  701 3 s	  1120	  3580 0 ));
+DATA(insert (	4005	700  701 4 s	  1125	  3580 0 ));
+DATA(insert (	4005	700  701 5 s	  1123	  3580 0 ));
+DATA(insert (	4005	701  700 1 s	  1132	  3580 0 ));
+DATA(insert (	4005	701  700 2 s	  1134	  3580 0 ));
+DATA(insert (	4005	701  700 3 s	  1130	  3580 0 ));
+DATA(insert (	4005	701  700 4 s	  1135	  3580 0 ));
+DATA(insert (	4005	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 ));
 /* bloom float (float4, float8) */
 DATA(insert (	5030	700  700 1 s	   620	  3580 0 ));
 DATA(insert (	5030	700  701 1 s	  1120	  3580 0 ));
@@ -1040,6 +1119,12 @@ DATA(insert (	4072	702  702 2 s	   564	  3580 0 ));
 DATA(insert (	4072	702  702 3 s	   560	  3580 0 ));
 DATA(insert (	4072	702  702 4 s	   565	  3580 0 ));
 DATA(insert (	4072	702  702 5 s	   563	  3580 0 ));
+/* multi minmax abstime */
+DATA(insert (	4146	702  702 1 s	   562	  3580 0 ));
+DATA(insert (	4146	702  702 2 s	   564	  3580 0 ));
+DATA(insert (	4146	702  702 3 s	   560	  3580 0 ));
+DATA(insert (	4146	702  702 4 s	   565	  3580 0 ));
+DATA(insert (	4146	702  702 5 s	   563	  3580 0 ));
 /* bloom abstime */
 DATA(insert (	5031	702  702 1 s	   560	  3580 0 ));
 /* minmax reltime */
@@ -1048,6 +1133,12 @@ DATA(insert (	4073	703  703 2 s	   570	  3580 0 ));
 DATA(insert (	4073	703  703 3 s	   566	  3580 0 ));
 DATA(insert (	4073	703  703 4 s	   571	  3580 0 ));
 DATA(insert (	4073	703  703 5 s	   569	  3580 0 ));
+/* multi minmax reltime */
+DATA(insert (	4147	703  703 1 s	   568	  3580 0 ));
+DATA(insert (	4147	703  703 2 s	   570	  3580 0 ));
+DATA(insert (	4147	703  703 3 s	   566	  3580 0 ));
+DATA(insert (	4147	703  703 4 s	   571	  3580 0 ));
+DATA(insert (	4147	703  703 5 s	   569	  3580 0 ));
 /* bloom reltime */
 DATA(insert (	5032	703  703 1 s	   566	  3580 0 ));
 /* minmax macaddr */
@@ -1056,6 +1147,12 @@ DATA(insert (	4074	829  829 2 s	  1223	  3580 0 ));
 DATA(insert (	4074	829  829 3 s	  1220	  3580 0 ));
 DATA(insert (	4074	829  829 4 s	  1225	  3580 0 ));
 DATA(insert (	4074	829  829 5 s	  1224	  3580 0 ));
+/* multi minmax macaddr */
+DATA(insert (	4143	829  829 1 s	  1222	  3580 0 ));
+DATA(insert (	4143	829  829 2 s	  1223	  3580 0 ));
+DATA(insert (	4143	829  829 3 s	  1220	  3580 0 ));
+DATA(insert (	4143	829  829 4 s	  1225	  3580 0 ));
+DATA(insert (	4143	829  829 5 s	  1224	  3580 0 ));
 /* bloom macaddr */
 DATA(insert (	5033	829  829 1 s	  1220	  3580 0 ));
 /* minmax macaddr8 */
@@ -1064,6 +1161,12 @@ DATA(insert (	4109	774  774 2 s	  3365	  3580 0 ));
 DATA(insert (	4109	774  774 3 s	  3362	  3580 0 ));
 DATA(insert (	4109	774  774 4 s	  3367	  3580 0 ));
 DATA(insert (	4109	774  774 5 s	  3366	  3580 0 ));
+/* multi minmax macaddr8 */
+DATA(insert (	4144	774  774 1 s	  3364	  3580 0 ));
+DATA(insert (	4144	774  774 2 s	  3365	  3580 0 ));
+DATA(insert (	4144	774  774 3 s	  3362	  3580 0 ));
+DATA(insert (	4144	774  774 4 s	  3367	  3580 0 ));
+DATA(insert (	4144	774  774 5 s	  3366	  3580 0 ));
 /* bloom macaddr8 */
 DATA(insert (	5034	774  774 1 s	  3362	  3580 0 ));
 /* minmax inet */
@@ -1072,6 +1175,12 @@ DATA(insert (	4075	869  869 2 s	  1204	  3580 0 ));
 DATA(insert (	4075	869  869 3 s	  1201	  3580 0 ));
 DATA(insert (	4075	869  869 4 s	  1206	  3580 0 ));
 DATA(insert (	4075	869  869 5 s	  1205	  3580 0 ));
+/* multi minmax inet */
+DATA(insert (	4145	869  869 1 s	  1203	  3580 0 ));
+DATA(insert (	4145	869  869 2 s	  1204	  3580 0 ));
+DATA(insert (	4145	869  869 3 s	  1201	  3580 0 ));
+DATA(insert (	4145	869  869 4 s	  1206	  3580 0 ));
+DATA(insert (	4145	869  869 5 s	  1205	  3580 0 ));
 /* inclusion inet */
 DATA(insert (	4102	869  869 3 s	  3552	  3580 0 ));
 DATA(insert (	4102	869  869 7 s	   934	  3580 0 ));
@@ -1095,6 +1204,12 @@ DATA(insert (	4077   1083 1083 2 s	  1111	  3580 0 ));
 DATA(insert (	4077   1083 1083 3 s	  1108	  3580 0 ));
 DATA(insert (	4077   1083 1083 4 s	  1113	  3580 0 ));
 DATA(insert (	4077   1083 1083 5 s	  1112	  3580 0 ));
+/* multi minmax time without time zone */
+DATA(insert (	4135   1083 1083 1 s	  1110	  3580 0 ));
+DATA(insert (	4135   1083 1083 2 s	  1111	  3580 0 ));
+DATA(insert (	4135   1083 1083 3 s	  1108	  3580 0 ));
+DATA(insert (	4135   1083 1083 4 s	  1113	  3580 0 ));
+DATA(insert (	4135   1083 1083 5 s	  1112	  3580 0 ));
 /* bloom time without time zone */
 DATA(insert (	5037   1083 1083 1 s	  1108	  3580 0 ));
 /* minmax datetime (date, timestamp, timestamptz) */
@@ -1143,6 +1258,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 ));
@@ -1160,6 +1321,12 @@ DATA(insert (	4078   1186 1186 2 s	  1333	  3580 0 ));
 DATA(insert (	4078   1186 1186 3 s	  1330	  3580 0 ));
 DATA(insert (	4078   1186 1186 4 s	  1335	  3580 0 ));
 DATA(insert (	4078   1186 1186 5 s	  1334	  3580 0 ));
+/* multi minmax interval */
+DATA(insert (	4134   1186 1186 1 s	  1332	  3580 0 ));
+DATA(insert (	4134   1186 1186 2 s	  1333	  3580 0 ));
+DATA(insert (	4134   1186 1186 3 s	  1330	  3580 0 ));
+DATA(insert (	4134   1186 1186 4 s	  1335	  3580 0 ));
+DATA(insert (	4134   1186 1186 5 s	  1334	  3580 0 ));
 /* bloom interval */
 DATA(insert (	5041   1186 1186 1 s	  1330	  3580 0 ));
 /* minmax time with time zone */
@@ -1168,6 +1335,12 @@ DATA(insert (	4058   1266 1266 2 s	  1553	  3580 0 ));
 DATA(insert (	4058   1266 1266 3 s	  1550	  3580 0 ));
 DATA(insert (	4058   1266 1266 4 s	  1555	  3580 0 ));
 DATA(insert (	4058   1266 1266 5 s	  1554	  3580 0 ));
+/* multi minmax time with time zone */
+DATA(insert (	4133   1266 1266 1 s	  1552	  3580 0 ));
+DATA(insert (	4133   1266 1266 2 s	  1553	  3580 0 ));
+DATA(insert (	4133   1266 1266 3 s	  1550	  3580 0 ));
+DATA(insert (	4133   1266 1266 4 s	  1555	  3580 0 ));
+DATA(insert (	4133   1266 1266 5 s	  1554	  3580 0 ));
 /* bloom time with time zone */
 DATA(insert (	5042   1266 1266 1 s	  1550	  3580 0 ));
 /* minmax bit */
@@ -1188,6 +1361,12 @@ DATA(insert (	4055   1700 1700 2 s	  1755	  3580 0 ));
 DATA(insert (	4055   1700 1700 3 s	  1752	  3580 0 ));
 DATA(insert (	4055   1700 1700 4 s	  1757	  3580 0 ));
 DATA(insert (	4055   1700 1700 5 s	  1756	  3580 0 ));
+/* minmax multi numeric */
+DATA(insert (	4008   1700 1700 1 s	  1754	  3580 0 ));
+DATA(insert (	4008   1700 1700 2 s	  1755	  3580 0 ));
+DATA(insert (	4008   1700 1700 3 s	  1752	  3580 0 ));
+DATA(insert (	4008   1700 1700 4 s	  1757	  3580 0 ));
+DATA(insert (	4008   1700 1700 5 s	  1756	  3580 0 ));
 /* bloom numeric */
 DATA(insert (	5045   1700 1700 1 s	  1752	  3580 0 ));
 /* minmax uuid */
@@ -1196,6 +1375,12 @@ DATA(insert (	4081   2950 2950 2 s	  2976	  3580 0 ));
 DATA(insert (	4081   2950 2950 3 s	  2972	  3580 0 ));
 DATA(insert (	4081   2950 2950 4 s	  2977	  3580 0 ));
 DATA(insert (	4081   2950 2950 5 s	  2975	  3580 0 ));
+/* minmax multi uuid */
+DATA(insert (	4131   2950 2950 1 s	  2974	  3580 0 ));
+DATA(insert (	4131   2950 2950 2 s	  2976	  3580 0 ));
+DATA(insert (	4131   2950 2950 3 s	  2972	  3580 0 ));
+DATA(insert (	4131   2950 2950 4 s	  2977	  3580 0 ));
+DATA(insert (	4131   2950 2950 5 s	  2975	  3580 0 ));
 /* bloom uuid */
 DATA(insert (	5046   2950 2950 1 s	  2972	  3580 0 ));
 /* inclusion range types */
@@ -1219,6 +1404,12 @@ DATA(insert (	4082   3220 3220 2 s	  3226	  3580 0 ));
 DATA(insert (	4082   3220 3220 3 s	  3222	  3580 0 ));
 DATA(insert (	4082   3220 3220 4 s	  3227	  3580 0 ));
 DATA(insert (	4082   3220 3220 5 s	  3225	  3580 0 ));
+/* multi minmax pg_lsn */
+DATA(insert (	4132   3220 3220 1 s	  3224	  3580 0 ));
+DATA(insert (	4132   3220 3220 2 s	  3226	  3580 0 ));
+DATA(insert (	4132   3220 3220 3 s	  3222	  3580 0 ));
+DATA(insert (	4132   3220 3220 4 s	  3227	  3580 0 ));
+DATA(insert (	4132   3220 3220 5 s	  3225	  3580 0 ));
 /* bloom pg_lsn */
 DATA(insert (	5047   3220 3220 1 s	  3222	  3580 0 ));
 /* inclusion box */
diff --git a/src/include/catalog/pg_amproc.h b/src/include/catalog/pg_amproc.h
index 4871836..8fb3405 100644
--- a/src/include/catalog/pg_amproc.h
+++ b/src/include/catalog/pg_amproc.h
@@ -415,6 +415,55 @@ DATA(insert (	4054	23	  21  2  3384 ));
 DATA(insert (	4054	23	  21  3  3385 ));
 DATA(insert (	4054	23	  21  4  3386 ));
 
+/* minmax multi integer: int2, int4, int8 */
+DATA(insert (	4126	20	  20  1  4001 ));
+DATA(insert (	4126	20	  20  2  4002 ));
+DATA(insert (	4126	20	  20  3  4003 ));
+DATA(insert (	4126	20	  20  4  4004 ));
+DATA(insert (	4126	20	  20  11 4013 ));
+DATA(insert (	4126	20	  21  1  4001 ));
+DATA(insert (	4126	20	  21  2  4002 ));
+DATA(insert (	4126	20	  21  3  4003 ));
+DATA(insert (	4126	20	  21  4  4004 ));
+DATA(insert (	4126	20	  21  11 4013 ));
+DATA(insert (	4126	20	  23  1  4001 ));
+DATA(insert (	4126	20	  23  2  4002 ));
+DATA(insert (	4126	20	  23  3  4003 ));
+DATA(insert (	4126	20	  23  4  4004 ));
+DATA(insert (	4126	20	  23  11 4013 ));
+
+DATA(insert (	4126	21	  21  1  4001 ));
+DATA(insert (	4126	21	  21  2  4002 ));
+DATA(insert (	4126	21	  21  3  4003 ));
+DATA(insert (	4126	21	  21  4  4004 ));
+DATA(insert (	4126	21	  21  11 4011 ));
+DATA(insert (	4126	21	  20  1  4001 ));
+DATA(insert (	4126	21	  20  2  4002 ));
+DATA(insert (	4126	21	  20  3  4003 ));
+DATA(insert (	4126	21	  20  4  4004 ));
+DATA(insert (	4126	21	  20  11 4011 ));
+DATA(insert (	4126	21	  23  1  4001 ));
+DATA(insert (	4126	21	  23  2  4002 ));
+DATA(insert (	4126	21	  23  3  4003 ));
+DATA(insert (	4126	21	  23  4  4004 ));
+DATA(insert (	4126	21	  23  11 4011 ));
+
+DATA(insert (	4126	23	  23  1  4001 ));
+DATA(insert (	4126	23	  23  2  4002 ));
+DATA(insert (	4126	23	  23  3  4003 ));
+DATA(insert (	4126	23	  23  4  4004 ));
+DATA(insert (	4126	23	  23  11 4012 ));
+DATA(insert (	4126	23	  20  1  4001 ));
+DATA(insert (	4126	23	  20  2  4002 ));
+DATA(insert (	4126	23	  20  3  4003 ));
+DATA(insert (	4126	23	  20  4  4004 ));
+DATA(insert (	4126	23	  20  11 4012 ));
+DATA(insert (	4126	23	  21  1  4001 ));
+DATA(insert (	4126	23	  21  2  4002 ));
+DATA(insert (	4126	23	  21  3  4003 ));
+DATA(insert (	4126	23	  21  4  4004 ));
+DATA(insert (	4126	23	  21  11 4012 ));
+
 /* bloom integer: int2, int4, int8 */
 DATA(insert (	5024	20	  20  1  5017 ));
 DATA(insert (	5024	20	  20  2  5018 ));
@@ -450,6 +499,12 @@ DATA(insert (	4068	26	  26  1  3383 ));
 DATA(insert (	4068	26	  26  2  3384 ));
 DATA(insert (	4068	26	  26  3  3385 ));
 DATA(insert (	4068	26	  26  4  3386 ));
+/* minmax multi oid */
+DATA(insert (	4127	26	  26  1  4001 ));
+DATA(insert (	4127	26	  26  2  4002 ));
+DATA(insert (	4127	26	  26  3  4003 ));
+DATA(insert (	4127	26	  26  4  4004 ));
+DATA(insert (	4127	26	  26 11  4012 ));
 /* bloom oid */
 DATA(insert (	5029	26	  26  1  5017 ));
 DATA(insert (	5029	26	  26  2  5018 ));
@@ -461,6 +516,12 @@ DATA(insert (	4069	27	  27  1  3383 ));
 DATA(insert (	4069	27	  27  2  3384 ));
 DATA(insert (	4069	27	  27  3  3385 ));
 DATA(insert (	4069	27	  27  4  3386 ));
+/* minmax multi tid */
+DATA(insert (	4128	27	  27  1  4001 ));
+DATA(insert (	4128	27	  27  2  4002 ));
+DATA(insert (	4128	27	  27  3  4003 ));
+DATA(insert (	4128	27	  27  4  4004 ));
+DATA(insert (	4128	27	  27  11 4129 ));
 /* minmax float */
 DATA(insert (	4070   700	 700  1  3383 ));
 DATA(insert (	4070   700	 700  2  3384 ));
@@ -482,6 +543,31 @@ 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   700	 700  1  4001 ));
+DATA(insert (	4005   700	 700  2  4002 ));
+DATA(insert (	4005   700	 700  3  4003 ));
+DATA(insert (	4005   700	 700  4  4004 ));
+DATA(insert (	4005   700	 700  11 4010 ));
+
+DATA(insert (	4005   700	 701  1  4001 ));
+DATA(insert (	4005   700	 701  2  4002 ));
+DATA(insert (	4005   700	 701  3  4003 ));
+DATA(insert (	4005   700	 701  4  4004 ));
+DATA(insert (	4005   700	 701  11 4010 ));
+
+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 ));
+DATA(insert (	4005   701	 701  11 4007 ));
+
+DATA(insert (	4005   701	 700  1  4001 ));
+DATA(insert (	4005   701	 700  2  4002 ));
+DATA(insert (	4005   701	 700  3  4003 ));
+DATA(insert (	4005   701	 700  4  4004 ));
+DATA(insert (	4005   701	 700  11 4007 ));
+
 /* bloom float */
 DATA(insert (	5030   700	 700  1  5017 ));
 DATA(insert (	5030   700	 700  2  5018 ));
@@ -500,6 +586,12 @@ DATA(insert (	4072   702	 702  1  3383 ));
 DATA(insert (	4072   702	 702  2  3384 ));
 DATA(insert (	4072   702	 702  3  3385 ));
 DATA(insert (	4072   702	 702  4  3386 ));
+/* multi minmax abstime */
+DATA(insert (	4146   702	 702  1  4001 ));
+DATA(insert (	4146   702	 702  2  4002 ));
+DATA(insert (	4146   702	 702  3  4003 ));
+DATA(insert (	4146   702	 702  4  4004 ));
+DATA(insert (	4146   702	 702 11  4148 ));
 /* bloom abstime */
 DATA(insert (	5031   702	 702  1  5017 ));
 DATA(insert (	5031   702	 702  2  5018 ));
@@ -511,6 +603,12 @@ DATA(insert (	4073   703	 703  1  3383 ));
 DATA(insert (	4073   703	 703  2  3384 ));
 DATA(insert (	4073   703	 703  3  3385 ));
 DATA(insert (	4073   703	 703  4  3386 ));
+/* multi minmax reltime */
+DATA(insert (	4147   703	 703  1  4001 ));
+DATA(insert (	4147   703	 703  2  4002 ));
+DATA(insert (	4147   703	 703  3  4003 ));
+DATA(insert (	4147   703	 703  4  4004 ));
+DATA(insert (	4147   703	 703 11  4149 ));
 /* bloom reltime */
 DATA(insert (	5032   703	 703  1  5017 ));
 DATA(insert (	5032   703	 703  2  5018 ));
@@ -522,6 +620,12 @@ DATA(insert (	4074   829	 829  1  3383 ));
 DATA(insert (	4074   829	 829  2  3384 ));
 DATA(insert (	4074   829	 829  3  3385 ));
 DATA(insert (	4074   829	 829  4  3386 ));
+/* multi minmax macaddr */
+DATA(insert (	4143   829	 829  1  4001 ));
+DATA(insert (	4143   829	 829  2  4002 ));
+DATA(insert (	4143   829	 829  3  4003 ));
+DATA(insert (	4143   829	 829  4  4004 ));
+DATA(insert (	4143   829	 829 11  4140 ));
 /* bloom macaddr */
 DATA(insert (	5033   829	 829  1  5017 ));
 DATA(insert (	5033   829	 829  2  5018 ));
@@ -533,6 +637,12 @@ DATA(insert (	4109   774	 774  1  3383 ));
 DATA(insert (	4109   774	 774  2  3384 ));
 DATA(insert (	4109   774	 774  3  3385 ));
 DATA(insert (	4109   774	 774  4  3386 ));
+/* minmax macaddr8 */
+DATA(insert (	4144   774	 774  1  4001 ));
+DATA(insert (	4144   774	 774  2  4002 ));
+DATA(insert (	4144   774	 774  3  4003 ));
+DATA(insert (	4144   774	 774  4  4004 ));
+DATA(insert (	4144   774	 774 11  4141 ));
 /* bloom macaddr8 */
 DATA(insert (	5034   774	 774  1  5017 ));
 DATA(insert (	5034   774	 774  2  5018 ));
@@ -544,6 +654,12 @@ DATA(insert (	4075   869	 869  1  3383 ));
 DATA(insert (	4075   869	 869  2  3384 ));
 DATA(insert (	4075   869	 869  3  3385 ));
 DATA(insert (	4075   869	 869  4  3386 ));
+/* multi minmax inet */
+DATA(insert (	4145   869	 869  1  4001 ));
+DATA(insert (	4145   869	 869  2  4002 ));
+DATA(insert (	4145   869	 869  3  4003 ));
+DATA(insert (	4145   869	 869  4  4004 ));
+DATA(insert (	4145   869	 869 11  4142 ));
 /* inclusion inet */
 DATA(insert (	4102   869	 869  1  4105 ));
 DATA(insert (	4102   869	 869  2  4106 ));
@@ -574,6 +690,12 @@ DATA(insert (	4077  1083	1083  1  3383 ));
 DATA(insert (	4077  1083	1083  2  3384 ));
 DATA(insert (	4077  1083	1083  3  3385 ));
 DATA(insert (	4077  1083	1083  4  3386 ));
+/* multi minmax time without time zone */
+DATA(insert (	4135  1083	1083  1  4001 ));
+DATA(insert (	4135  1083	1083  2  4002 ));
+DATA(insert (	4135  1083	1083  3  4003 ));
+DATA(insert (	4135  1083	1083  4  4004 ));
+DATA(insert (	4135  1083	1083 11  4136 ));
 /* bloom time without time zone */
 DATA(insert (	5037  1083	1083  1  5017 ));
 DATA(insert (	5037  1083	1083  2  5018 ));
@@ -620,6 +742,55 @@ DATA(insert (	4059  1082	1184  2  3384 ));
 DATA(insert (	4059  1082	1184  3  3385 ));
 DATA(insert (	4059  1082	1184  4  3386 ));
 
+/* minmax multi (date, 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	1114  11 4007 ));
+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	1184  11 4007 ));
+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  1114	1082  11 4007 ));
+
+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	1184  11 4007 ));
+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	1114  11 4007 ));
+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  1184	1082  11 4007 ));
+
+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	1082  11 4007 ));
+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	1114  11 4007 ));
+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 ));
+DATA(insert (	4006  1082	1184  11 4007 ));
+
 /* bloom datetime (date, timestamp, timestamptz) */
 DATA(insert (	5038  1114	1114  1  5017 ));
 DATA(insert (	5038  1114	1114  2  5018 ));
@@ -644,6 +815,12 @@ DATA(insert (	4078  1186	1186  1  3383 ));
 DATA(insert (	4078  1186	1186  2  3384 ));
 DATA(insert (	4078  1186	1186  3  3385 ));
 DATA(insert (	4078  1186	1186  4  3386 ));
+/* multi minmax interval */
+DATA(insert (	4134  1186	1186  1  4001 ));
+DATA(insert (	4134  1186	1186  2  4002 ));
+DATA(insert (	4134  1186	1186  3  4003 ));
+DATA(insert (	4134  1186	1186  4  4004 ));
+DATA(insert (	4134  1186	1186 11  4137 ));
 /* bloom interval */
 DATA(insert (	5041  1186	1186  1  5017 ));
 DATA(insert (	5041  1186	1186  2  5018 ));
@@ -655,6 +832,12 @@ DATA(insert (	4058  1266	1266  1  3383 ));
 DATA(insert (	4058  1266	1266  2  3384 ));
 DATA(insert (	4058  1266	1266  3  3385 ));
 DATA(insert (	4058  1266	1266  4  3386 ));
+/* multi minmax time with time zone */
+DATA(insert (	4133  1266	1266  1  4001 ));
+DATA(insert (	4133  1266	1266  2  4002 ));
+DATA(insert (	4133  1266	1266  3  4003 ));
+DATA(insert (	4133  1266	1266  4  4004 ));
+DATA(insert (	4133  1266	1266 11  4138 ));
 /* bloom time with time zone */
 DATA(insert (	5042  1266	1266  1  5017 ));
 DATA(insert (	5042  1266	1266  2  5018 ));
@@ -676,6 +859,12 @@ DATA(insert (	4055  1700	1700  1  3383 ));
 DATA(insert (	4055  1700	1700  2  3384 ));
 DATA(insert (	4055  1700	1700  3  3385 ));
 DATA(insert (	4055  1700	1700  4  3386 ));
+/* minmax multi numeric */
+DATA(insert (	4008  1700	1700  1  4001 ));
+DATA(insert (	4008  1700	1700  2  4002 ));
+DATA(insert (	4008  1700	1700  3  4003 ));
+DATA(insert (	4008  1700	1700  4  4004 ));
+DATA(insert (	4008  1700	1700 11  4009 ));
 /* bloom numeric */
 DATA(insert (	5045  1700	1700  1  5017 ));
 DATA(insert (	5045  1700	1700  2  5018 ));
@@ -687,6 +876,12 @@ DATA(insert (	4081  2950	2950  1  3383 ));
 DATA(insert (	4081  2950	2950  2  3384 ));
 DATA(insert (	4081  2950	2950  3  3385 ));
 DATA(insert (	4081  2950	2950  4  3386 ));
+/* minmax multi uuid */
+DATA(insert (	4131  2950	2950  1  4001 ));
+DATA(insert (	4131  2950	2950  2  4002 ));
+DATA(insert (	4131  2950	2950  3  4003 ));
+DATA(insert (	4131  2950	2950  4  4004 ));
+DATA(insert (	4131  2950	2950  11 4130 ));
 /* bloom uuid */
 DATA(insert (	5046  2950	2950  1  5017 ));
 DATA(insert (	5046  2950	2950  2  5018 ));
@@ -706,6 +901,12 @@ DATA(insert (	4082  3220	3220  1  3383 ));
 DATA(insert (	4082  3220	3220  2  3384 ));
 DATA(insert (	4082  3220	3220  3  3385 ));
 DATA(insert (	4082  3220	3220  4  3386 ));
+/* multi minmax pg_lsn */
+DATA(insert (	4132  3220	3220  1  4001 ));
+DATA(insert (	4132  3220	3220  2  4002 ));
+DATA(insert (	4132  3220	3220  3  4003 ));
+DATA(insert (	4132  3220	3220  4  4004 ));
+DATA(insert (	4132  3220	3220 11  4139 ));
 /* bloom pg_lsn */
 DATA(insert (	5047  3220	3220  1  5017 ));
 DATA(insert (	5047  3220	3220  2  5018 ));
diff --git a/src/include/catalog/pg_opclass.h b/src/include/catalog/pg_opclass.h
index 7dbd4ff..4e102e4 100644
--- a/src/include/catalog/pg_opclass.h
+++ b/src/include/catalog/pg_opclass.h
@@ -220,54 +220,75 @@ DATA(insert (	3580	char_bloom_ops			PGNSP PGUID 5022	18 f 18 ));
 DATA(insert (	3580	name_minmax_ops			PGNSP PGUID 4065	19 t 19 ));
 DATA(insert (	3580	name_bloom_ops			PGNSP PGUID 5023	19 f 19 ));
 DATA(insert (	3580	int8_minmax_ops			PGNSP PGUID 4054	20 t 20 ));
+DATA(insert (	3580	int8_minmax_multi_ops	PGNSP PGUID 4126	20 f 20 ));
 DATA(insert (	3580	int8_bloom_ops			PGNSP PGUID 5024	20 f 20 ));
 DATA(insert (	3580	int2_minmax_ops			PGNSP PGUID 4054	21 t 21 ));
+DATA(insert (	3580	int2_minmax_multi_ops	PGNSP PGUID 4126	21 f 21 ));
 DATA(insert (	3580	int2_bloom_ops			PGNSP PGUID 5024	21 f 21 ));
 DATA(insert (	3580	int4_minmax_ops			PGNSP PGUID 4054	23 t 23 ));
+DATA(insert (	3580	int4_minmax_multi_ops	PGNSP PGUID 4126	23 f 23 ));
 DATA(insert (	3580	int4_bloom_ops			PGNSP PGUID 5024	23 f 23 ));
 DATA(insert (	3580	text_minmax_ops			PGNSP PGUID 4056	25 t 25 ));
 DATA(insert (	3580	text_bloom_ops			PGNSP PGUID 5027	25 f 25 ));
 DATA(insert (	3580	oid_minmax_ops			PGNSP PGUID 4068	26 t 26 ));
+DATA(insert (	3580	oid_minmax_multi_ops	PGNSP PGUID 4127	26 f 26 ));
 DATA(insert (	3580	oid_bloom_ops			PGNSP PGUID 5029	26 f 26 ));
 DATA(insert (	3580	tid_minmax_ops			PGNSP PGUID 4069	27 t 27 ));
+DATA(insert (	3580	tid_minmax_multi_ops	PGNSP PGUID 4128	27 f 27 ));
 DATA(insert (	3580	float4_minmax_ops		PGNSP PGUID 4070   700 t 700 ));
+DATA(insert (	3580	float4_minmax_multi_ops	PGNSP PGUID 4005   700 f 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_minmax_multi_ops	PGNSP PGUID 4146   702 f 702 ));
 DATA(insert (	3580	abstime_bloom_ops		PGNSP PGUID 5031   702 f 702 ));
 DATA(insert (	3580	reltime_minmax_ops		PGNSP PGUID 4073   703 t 703 ));
+DATA(insert (	3580	reltime_minmax_multi_ops	PGNSP PGUID 4147   703 f 703 ));
 DATA(insert (	3580	reltime_bloom_ops		PGNSP PGUID 5032   703 f 703 ));
 DATA(insert (	3580	macaddr_minmax_ops		PGNSP PGUID 4074   829 t 829 ));
+DATA(insert (	3580	macaddr_minmax_multi_ops	PGNSP PGUID 4143   829 f 829 ));
 DATA(insert (	3580	macaddr_bloom_ops		PGNSP PGUID 5033   829 f 829 ));
 DATA(insert (	3580	macaddr8_minmax_ops		PGNSP PGUID 4109   774 t 774 ));
+DATA(insert (	3580	macaddr8_minmax_multi_ops	PGNSP PGUID 4144   774 f 774 ));
 DATA(insert (	3580	macaddr8_bloom_ops		PGNSP PGUID 5034   774 f 774 ));
 DATA(insert (	3580	inet_minmax_ops			PGNSP PGUID 4075   869 f 869 ));
+DATA(insert (	3580	inet_minmax_multi_ops	PGNSP PGUID 4145   869 f 869 ));
 DATA(insert (	3580	inet_inclusion_ops		PGNSP PGUID 4102   869 t 869 ));
 DATA(insert (	3580	inet_bloom_ops			PGNSP PGUID 5035   869 f 869 ));
 DATA(insert (	3580	bpchar_minmax_ops		PGNSP PGUID 4076  1042 t 1042 ));
 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_minmax_multi_ops	PGNSP PGUID 4135  1083 f 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_minmax_multi_ops	PGNSP PGUID 4134  1186 f 1186 ));
 DATA(insert (	3580	interval_bloom_ops		PGNSP PGUID 5041  1186 f 1186 ));
 DATA(insert (	3580	timetz_minmax_ops		PGNSP PGUID 4058  1266 t 1266 ));
+DATA(insert (	3580	timetz_minmax_multi_ops	PGNSP PGUID 4133  1266 f 1266 ));
 DATA(insert (	3580	timetz_bloom_ops		PGNSP PGUID 5042  1266 f 1266 ));
 DATA(insert (	3580	bit_minmax_ops			PGNSP PGUID 4079  1560 t 1560 ));
 DATA(insert (	3580	varbit_minmax_ops		PGNSP PGUID 4080  1562 t 1562 ));
 DATA(insert (	3580	numeric_minmax_ops		PGNSP PGUID 4055  1700 t 1700 ));
+DATA(insert (	3580	numeric_minmax_multi_ops	PGNSP PGUID 4008  1700 f 1700 ));
 DATA(insert (	3580	numeric_bloom_ops		PGNSP PGUID 5045  1700 f 1700 ));
 /* no brin opclass for record, anyarray */
 DATA(insert (	3580	uuid_minmax_ops			PGNSP PGUID 4081  2950 t 2950 ));
+DATA(insert (	3580	uuid_minmax_multi_ops	PGNSP PGUID 4131  2950 f 2950 ));
 DATA(insert (	3580	uuid_bloom_ops			PGNSP PGUID 5046  2950 f 2950 ));
 DATA(insert (	3580	range_inclusion_ops		PGNSP PGUID 4103  3831 t 3831 ));
 DATA(insert (	3580	pg_lsn_minmax_ops		PGNSP PGUID 4082  3220 t 3220 ));
+DATA(insert (	3580	pg_lsn_minmax_multi_ops	PGNSP PGUID 4132  3220 f 3220 ));
 DATA(insert (	3580	pg_lsn_bloom_ops		PGNSP PGUID 5047  3220 f 3220 ));
 /* no brin opclass for enum, tsvector, tsquery, jsonb */
 DATA(insert (	3580	box_inclusion_ops		PGNSP PGUID 4104   603 t 603 ));
diff --git a/src/include/catalog/pg_opfamily.h b/src/include/catalog/pg_opfamily.h
index bc22ce3..f0ce10b 100644
--- a/src/include/catalog/pg_opfamily.h
+++ b/src/include/catalog/pg_opfamily.h
@@ -160,14 +160,18 @@ DATA(insert OID = 4036 (	2742	jsonb_ops		PGNSP PGUID ));
 DATA(insert OID = 4037 (	2742	jsonb_path_ops	PGNSP PGUID ));
 
 DATA(insert OID = 4054 (	3580	integer_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4126 (	3580	integer_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5024 (	3580	integer_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4055 (	3580	numeric_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4008 (	3580	numeric_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5045 (	3580	numeric_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4056 (	3580	text_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 5027 (	3580	text_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4058 (	3580	timetz_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4133 (	3580	timetz_minmax_multi_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 ));
@@ -176,33 +180,45 @@ DATA(insert OID = 5021 (	3580	bytea_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4065 (	3580	name_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 5023 (	3580	name_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4068 (	3580	oid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 4127 (	3580	oid_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5029 (	3580	oid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4069 (	3580	tid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 4128 (	3580	tid_minmax_multi_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 = 4146 (	3580	abstime_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5031 (	3580	abstime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4073 (	3580	reltime_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4147 (	3580	reltime_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5032 (	3580	reltime_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4074 (	3580	macaddr_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4143 (	3580	macaddr_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5033 (	3580	macaddr_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4109 (	3580	macaddr8_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4144 (	3580	macaddr8_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5034 (	3580	macaddr8_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4075 (	3580	network_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4145 (	3580	network_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 4102 (	3580	network_inclusion_ops	PGNSP PGUID ));
 DATA(insert OID = 5035 (	3580	network_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4076 (	3580	bpchar_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 5036 (	3580	bpchar_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4077 (	3580	time_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 4135 (	3580	time_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5037 (	3580	time_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4078 (	3580	interval_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4134 (	3580	interval_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5041 (	3580	interval_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4079 (	3580	bit_minmax_ops			PGNSP PGUID ));
 DATA(insert OID = 4080 (	3580	varbit_minmax_ops		PGNSP PGUID ));
 DATA(insert OID = 4081 (	3580	uuid_minmax_ops			PGNSP PGUID ));
+DATA(insert OID = 4131 (	3580	uuid_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5046 (	3580	uuid_bloom_ops			PGNSP PGUID ));
 DATA(insert OID = 4103 (	3580	range_inclusion_ops		PGNSP PGUID ));
 DATA(insert OID = 4082 (	3580	pg_lsn_minmax_ops		PGNSP PGUID ));
+DATA(insert OID = 4132 (	3580	pg_lsn_minmax_multi_ops	PGNSP PGUID ));
 DATA(insert OID = 5047 (	3580	pg_lsn_bloom_ops		PGNSP PGUID ));
 DATA(insert OID = 4104 (	3580	box_inclusion_ops		PGNSP PGUID ));
 DATA(insert OID = 5000 (	4000	box_ops		PGNSP PGUID ));
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 7154e5e..c78c49f 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -4364,6 +4364,50 @@ 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");
+DATA(insert OID = 4011 ( brin_minmax_multi_distance_int2		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_int2 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between int2 values");
+DATA(insert OID = 4012 ( brin_minmax_multi_distance_int4		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_int4 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between int4 values");
+DATA(insert OID = 4013 ( brin_minmax_multi_distance_int8		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_int8 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between int8 values");
+DATA(insert OID = 4010 ( brin_minmax_multi_distance_float4	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_float4 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between float4 values");
+DATA(insert OID = 4007 ( brin_minmax_multi_distance_float8	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_float8 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between float8 values");
+DATA(insert OID = 4009 ( brin_minmax_multi_distance_numeric	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_numeric _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between numeric values");
+DATA(insert OID = 4129 ( brin_minmax_multi_distance_tid		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_tid _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between tid values");
+DATA(insert OID = 4130 ( brin_minmax_multi_distance_uuid		PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_uuid _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between uuid values");
+DATA(insert OID = 4136 ( brin_minmax_multi_distance_time	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_time _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between time values");
+DATA(insert OID = 4137 ( brin_minmax_multi_distance_interval	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_interval _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between interval values");
+DATA(insert OID = 4138 ( brin_minmax_multi_distance_timetz	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_timetz _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between timetz values");
+DATA(insert OID = 4139 ( brin_minmax_multi_distance_pg_lsn	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_pg_lsn _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between pg_lsn values");
+DATA(insert OID = 4140 ( brin_minmax_multi_distance_macaddr	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_macaddr _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between macaddr values");
+DATA(insert OID = 4141 ( brin_minmax_multi_distance_macaddr8	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_macaddr8 _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between macaddr8 values");
+DATA(insert OID = 4142 ( brin_minmax_multi_distance_inet	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_inet _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between inet values");
+DATA(insert OID = 4148 ( brin_minmax_multi_distance_abstime	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_abstime _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between abstime values");
+DATA(insert OID = 4149 ( brin_minmax_multi_distance_reltime	PGNSP PGUID 12 1 0 0 0 f f f f t f i s 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ _null_ brin_minmax_multi_distance_reltime _null_ _null_ _null_ ));
+DESCR("BRIN minmax compute distance between reltime values");
+
 /* 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");
diff --git a/src/test/regress/expected/brin_multi.out b/src/test/regress/expected/brin_multi.out
new file mode 100644
index 0000000..a337006
--- /dev/null
+++ b/src/test/regress/expected/brin_multi.out
@@ -0,0 +1,408 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+UPDATE brintest_multi SET int8col = int8col * int4col;
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+ERROR:  "brintest_multi" is not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+ERROR:  "tenk1_unique1" is not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+ brin_summarize_new_values 
+---------------------------
+                         0
+(1 row)
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+ERROR:  block number out of range: -1
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+ brin_desummarize_range 
+------------------------
+ 
+(1 row)
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+ brin_summarize_range 
+----------------------
+                    1
+(1 row)
+
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+ brin_summarize_range 
+----------------------
+                    0
+(1 row)
+
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+ERROR:  block number out of range: -1
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+ERROR:  block number out of range: 4294967296
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Bitmap Heap Scan on brin_test_multi
+   Recheck Cond: (a = 1)
+   ->  Bitmap Index Scan on brin_test_multi_a_idx
+         Index Cond: (a = 1)
+(4 rows)
+
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on brin_test_multi
+   Filter: (b = 1)
+(2 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 149999d..2c635d5 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -84,7 +84,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: brin brin_bloom gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password
+test: brin brin_bloom brin_multi gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/brin_multi.sql b/src/test/regress/sql/brin_multi.sql
new file mode 100644
index 0000000..263396f
--- /dev/null
+++ b/src/test/regress/sql/brin_multi.sql
@@ -0,0 +1,361 @@
+CREATE TABLE brintest_multi (
+	int8col bigint,
+	int2col smallint,
+	int4col integer,
+	oidcol oid,
+	tidcol tid,
+	float4col real,
+	float8col double precision,
+	macaddrcol macaddr,
+	inetcol inet,
+	cidrcol cidr,
+	datecol date,
+	timecol time without time zone,
+	timestampcol timestamp without time zone,
+	timestamptzcol timestamp with time zone,
+	intervalcol interval,
+	timetzcol time with time zone,
+	numericcol numeric,
+	uuidcol uuid,
+	lsncol pg_lsn
+) WITH (fillfactor=10);
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4/24' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20+02' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 100;
+
+-- throw in some NULL's and different values
+INSERT INTO brintest_multi (inetcol, cidrcol) SELECT
+	inet 'fe80::6e40:8ff:fea9:8c46' + tenthous,
+	cidr 'fe80::6e40:8ff:fea9:8c46' + tenthous
+FROM tenk1 ORDER BY thousand, tenthous LIMIT 25;
+
+CREATE INDEX brinidx_multi ON brintest_multi USING brin (
+	int8col int8_minmax_multi_ops,
+	int2col int2_minmax_multi_ops,
+	int4col int4_minmax_multi_ops,
+	oidcol oid_minmax_multi_ops,
+	tidcol tid_minmax_multi_ops,
+	float4col float4_minmax_multi_ops,
+	float8col float8_minmax_multi_ops,
+	macaddrcol macaddr_minmax_multi_ops,
+	inetcol inet_minmax_multi_ops,
+	cidrcol inet_minmax_multi_ops,
+	datecol date_minmax_multi_ops,
+	timecol time_minmax_multi_ops,
+	timestampcol timestamp_minmax_multi_ops,
+	timestamptzcol timestamptz_minmax_multi_ops,
+	intervalcol interval_minmax_multi_ops,
+	timetzcol timetz_minmax_multi_ops,
+	numericcol numeric_minmax_multi_ops,
+	uuidcol uuid_minmax_multi_ops,
+	lsncol pg_lsn_minmax_multi_ops
+) with (pages_per_range = 1);
+
+CREATE TABLE brinopers_multi (colname name, typ text,
+	op text[], value text[], matches int[],
+	check (cardinality(op) = cardinality(value)),
+	check (cardinality(op) = cardinality(matches)));
+
+INSERT INTO brinopers_multi VALUES
+	('int2col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int2col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int2',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1999}',
+	 '{100, 100, 1, 100, 100}'),
+	('int4col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 800, 1999, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('int8col', 'int2',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int4',
+	 '{>, >=}',
+	 '{0, 0}',
+	 '{100, 100}'),
+	('int8col', 'int8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 1257141600, 1428427143, 1428427143}',
+	 '{100, 100, 1, 100, 100}'),
+	('oidcol', 'oid',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 8800, 9999, 9999}',
+	 '{100, 100, 1, 100, 100}'),
+	('tidcol', 'tid',
+	 '{>, >=, =, <=, <}',
+	 '{"(0,0)", "(0,0)", "(8800,0)", "(9999,19)", "(9999,19)"}',
+	 '{100, 100, 1, 100, 100}'),
+	('float4col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float4col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0.0103093, 0.0103093, 1, 1, 1}',
+	 '{100, 100, 4, 100, 96}'),
+	('float8col', 'float4',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('float8col', 'float8',
+	 '{>, >=, =, <=, <}',
+	 '{0, 0, 0, 1.98, 1.98}',
+	 '{99, 100, 1, 100, 100}'),
+	('macaddrcol', 'macaddr',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:01:00:00:00, 00:00:01:00:00:00, 2c:00:2d:00:16:00, ff:fe:00:00:00:00, ff:fe:00:00:00:00}',
+	 '{99, 100, 2, 100, 100}'),
+	('inetcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14.231/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{1, 100, 100, 125, 125}'),
+	('inetcol', 'cidr',
+	 '{<, <=, >, >=}',
+	 '{255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{100, 100, 125, 125}'),
+	('cidrcol', 'inet',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('cidrcol', 'cidr',
+	 '{=, <, <=, >, >=}',
+	 '{10.2.14/24, 255.255.255.255, 255.255.255.255, 0.0.0.0, 0.0.0.0}',
+	 '{2, 100, 100, 125, 125}'),
+	('datecol', 'date',
+	 '{>, >=, =, <=, <}',
+	 '{1995-08-15, 1995-08-15, 2009-12-01, 2022-12-30, 2022-12-30}',
+	 '{100, 100, 1, 100, 100}'),
+	('timecol', 'time',
+	 '{>, >=, =, <=, <}',
+	 '{01:20:30, 01:20:30, 02:28:57, 06:28:31.5, 06:28:31.5}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamp',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestampcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1942-07-23 03:05:09, 1942-07-23 03:05:09, 1964-03-24 19:26:45, 1984-01-20 22:42:21, 1984-01-20 22:42:21}',
+	 '{100, 100, 1, 100, 100}'),
+	('timestamptzcol', 'timestamptz',
+	 '{>, >=, =, <=, <}',
+	 '{1972-10-10 03:00:00-04, 1972-10-10 03:00:00-04, 1972-10-19 09:00:00-07, 1972-11-20 19:00:00-03, 1972-11-20 19:00:00-03}',
+	 '{100, 100, 1, 100, 100}'),
+	('intervalcol', 'interval',
+	 '{>, >=, =, <=, <}',
+	 '{00:00:00, 00:00:00, 1 mons 13 days 12:24, 2 mons 23 days 07:48:00, 1 year}',
+	 '{100, 100, 1, 100, 100}'),
+	('timetzcol', 'timetz',
+	 '{>, >=, =, <=, <}',
+	 '{01:30:20+02, 01:30:20+02, 01:35:50+02, 23:55:05+02, 23:55:05+02}',
+	 '{99, 100, 2, 100, 100}'),
+	('numericcol', 'numeric',
+	 '{>, >=, =, <=, <}',
+	 '{0.00, 0.01, 2268164.347826086956521739130434782609, 99470151.9, 99470151.9}',
+	 '{100, 100, 1, 100, 100}'),
+	('uuidcol', 'uuid',
+	 '{>, >=, =, <=, <}',
+	 '{00040004-0004-0004-0004-000400040004, 00040004-0004-0004-0004-000400040004, 52225222-5222-5222-5222-522252225222, 99989998-9998-9998-9998-999899989998, 99989998-9998-9998-9998-999899989998}',
+	 '{100, 100, 1, 100, 100}'),
+	('lsncol', 'pg_lsn',
+	 '{>, >=, =, <=, <, IS, IS NOT}',
+	 '{0/1200, 0/1200, 44/455222, 198/1999799, 198/1999799, NULL, NULL}',
+	 '{100, 100, 1, 100, 100, 25, 100}');
+
+DO $x$
+DECLARE
+	r record;
+	r2 record;
+	cond text;
+	idx_ctids tid[];
+	ss_ctids tid[];
+	count int;
+	plan_ok bool;
+	plan_line text;
+BEGIN
+	FOR r IN SELECT colname, oper, typ, value[ordinality], matches[ordinality] FROM brinopers_multi, unnest(op) WITH ORDINALITY AS oper LOOP
+
+		-- prepare the condition
+		IF r.value IS NULL THEN
+			cond := format('%I %s %L', r.colname, r.oper, r.value);
+		ELSE
+			cond := format('%I %s %L::%s', r.colname, r.oper, r.value, r.typ);
+		END IF;
+
+		-- run the query using the brin index
+		SET enable_seqscan = 0;
+		SET enable_bitmapscan = 1;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Bitmap Heap Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get bitmap indexscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO idx_ctids;
+
+		-- run the query using a seqscan
+		SET enable_seqscan = 1;
+		SET enable_bitmapscan = 0;
+
+		plan_ok := false;
+		FOR plan_line IN EXECUTE format($y$EXPLAIN SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond) LOOP
+			IF plan_line LIKE '%Seq Scan on brintest_multi%' THEN
+				plan_ok := true;
+			END IF;
+		END LOOP;
+		IF NOT plan_ok THEN
+			RAISE WARNING 'did not get seqscan plan for %', r;
+		END IF;
+
+		EXECUTE format($y$SELECT array_agg(ctid) FROM brintest_multi WHERE %s $y$, cond)
+			INTO ss_ctids;
+
+		-- make sure both return the same results
+		count := array_length(idx_ctids, 1);
+
+		IF NOT (count = array_length(ss_ctids, 1) AND
+				idx_ctids @> ss_ctids AND
+				idx_ctids <@ ss_ctids) THEN
+			-- report the results of each scan to make the differences obvious
+			RAISE WARNING 'something not right in %: count %', r, count;
+			SET enable_seqscan = 1;
+			SET enable_bitmapscan = 0;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'seqscan: %', r2;
+			END LOOP;
+
+			SET enable_seqscan = 0;
+			SET enable_bitmapscan = 1;
+			FOR r2 IN EXECUTE 'SELECT ' || r.colname || ' FROM brintest_multi WHERE ' || cond LOOP
+				RAISE NOTICE 'bitmapscan: %', r2;
+			END LOOP;
+		END IF;
+
+		-- make sure we found expected number of matches
+		IF count != r.matches THEN RAISE WARNING 'unexpected number of results % for %', count, r; END IF;
+	END LOOP;
+END;
+$x$;
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+INSERT INTO brintest_multi SELECT
+	142857 * tenthous,
+	thousand,
+	twothousand,
+	unique1::oid,
+	format('(%s,%s)', tenthous, twenty)::tid,
+	(four + 1.0)/(hundred+1),
+	odd::float8 / (tenthous + 1),
+	format('%s:00:%s:00:%s:00', to_hex(odd), to_hex(even), to_hex(hundred))::macaddr,
+	inet '10.2.3.4' + tenthous,
+	cidr '10.2.3/24' + tenthous,
+	date '1995-08-15' + tenthous,
+	time '01:20:30' + thousand * interval '18.5 second',
+	timestamp '1942-07-23 03:05:09' + tenthous * interval '36.38 hours',
+	timestamptz '1972-10-10 03:00' + thousand * interval '1 hour',
+	justify_days(justify_hours(tenthous * interval '12 minutes')),
+	timetz '01:30:20' + hundred * interval '15 seconds',
+	tenthous::numeric(36,30) * fivethous * even / (hundred + 1),
+	format('%s%s-%s-%s-%s-%s%s%s', to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'), to_char(tenthous, 'FM0000'))::uuid,
+	format('%s/%s%s', odd, even, tenthous)::pg_lsn
+FROM tenk1 ORDER BY unique2 LIMIT 5 OFFSET 5;
+
+SELECT brin_desummarize_range('brinidx_multi', 0);
+VACUUM brintest_multi;  -- force a summarization cycle in brinidx
+
+UPDATE brintest_multi SET int8col = int8col * int4col;
+
+-- Tests for brin_summarize_new_values
+SELECT brin_summarize_new_values('brintest_multi'); -- error, not an index
+SELECT brin_summarize_new_values('tenk1_unique1'); -- error, not a BRIN index
+SELECT brin_summarize_new_values('brinidx_multi'); -- ok, no change expected
+
+-- Tests for brin_desummarize_range
+SELECT brin_desummarize_range('brinidx_multi', -1); -- error, invalid range
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 0);
+SELECT brin_desummarize_range('brinidx_multi', 100000000);
+
+-- Test brin_summarize_range
+CREATE TABLE brin_summarize_multi (
+    value int
+) WITH (fillfactor=10, autovacuum_enabled=false);
+CREATE INDEX brin_summarize_multi_idx ON brin_summarize_multi USING brin (value) WITH (pages_per_range=2);
+-- Fill a few pages
+DO $$
+DECLARE curtid tid;
+BEGIN
+  LOOP
+    INSERT INTO brin_summarize_multi VALUES (1) RETURNING ctid INTO curtid;
+    EXIT WHEN curtid > tid '(2, 0)';
+  END LOOP;
+END;
+$$;
+
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 0);
+-- nothing: already summarized
+SELECT brin_summarize_range('brin_summarize_multi_idx', 1);
+-- summarize one range
+SELECT brin_summarize_range('brin_summarize_multi_idx', 2);
+-- nothing: page doesn't exist in table
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967295);
+-- invalid block number values
+SELECT brin_summarize_range('brin_summarize_multi_idx', -1);
+SELECT brin_summarize_range('brin_summarize_multi_idx', 4294967296);
+
+
+-- test brin cost estimates behave sanely based on correlation of values
+CREATE TABLE brin_test_multi (a INT, b INT);
+INSERT INTO brin_test_multi SELECT x/100,x%100 FROM generate_series(1,10000) x(x);
+CREATE INDEX brin_test_multi_a_idx ON brin_test_multi USING brin (a) WITH (pages_per_range = 2);
+CREATE INDEX brin_test_multi_b_idx ON brin_test_multi USING brin (b) WITH (pages_per_range = 2);
+VACUUM ANALYZE brin_test_multi;
+
+-- Ensure brin index is used when columns are perfectly correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE a = 1;
+-- Ensure brin index is not used when values are not correlated
+EXPLAIN (COSTS OFF) SELECT * FROM brin_test_multi WHERE b = 1;
-- 
2.9.5

view thread (177+ messages)  latest in thread

Message-ID: <c32f9f46-e0ca-0c02-45ec-718dd381f4be@2ndquadrant.com>
Permalink:  ../c32f9f46-e0ca-0c02-45ec-718dd381f4be@2ndquadrant.com/
Also on:    postgresql.org/message-id/c32f9f46-e0ca-0c02-45ec-718dd381f4be@2ndquadrant.com

 ·  · 

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: pgsql-hackers@postgresql.org
  Cc: tomas.vondra@2ndquadrant.com, alvherre@alvh.no-ip.org, hornschnorter@gmail.com
  Subject: Re: WIP: BRIN multi-range indexes
  In-Reply-To: <c32f9f46-e0ca-0c02-45ec-718dd381f4be@2ndquadrant.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox