From: Manuel Reyes Bravo <manuelreyesbravo@gmail.com>
Subject: [PATCH v2] Reject index-only scans when the AM can return nothing

Variant of Andrey Rachitskiy's patch for BUG #19638.

His guard keys off bms_is_empty(index_canreturn_attrs).  That bitmapset is
also empty for an index whose columns are all expressions, because the loop
that fills it skips them (attno == 0), so the guard rejects index-only scans
over expression indexes as well -- for example count(*) over a table whose
only index is on (a + b), which is a legitimate and useful plan.

Test index->canreturn[] directly instead.  plancat.c fills it per column
from index_can_return(), expression columns included, so an expression btree
has a true entry while an AM with amcanreturn == NULL has none.

Measured on 18.6: the #19638 reproducer returns the correct answer, count(*)
over an expression-only index keeps its Index Only Scan, and make check
passes 231/231.

Bug: 19638
Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Co-authored-by: Manuel Reyes Bravo <manuelreyesbravo@gmail.com>
Reported-by: Manuel Reyes Bravo <manuelreyesbravo@gmail.com>
Discussion: https://www.postgresql.org/message-id/19638-277d0f73dfaeaec8@postgresql.org
---
 src/backend/optimizer/path/indxpath.c | 25 ++++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -2291,6 +2291,31 @@
 	/* Do we have all the necessary attributes? */
 	result = bms_is_subset(attrs_used, index_canreturn_attrs);
 
+	/*
+	 * bms_is_subset() is true when attrs_used is empty, even if the index
+	 * returns nothing.  That would allow a broken index-only scan for AMs
+	 * with amcanreturn == NULL.
+	 *
+	 * Test the AM's capability directly rather than the bitmapset, which is
+	 * empty for expression-only indexes too (attno == 0 is skipped above)
+	 * even though such an index can perfectly well feed an index-only scan.
+	 */
+	if (result)
+	{
+		bool		any_canreturn = false;
+
+		for (i = 0; i < index->ncolumns; i++)
+		{
+			if (index->canreturn[i])
+			{
+				any_canreturn = true;
+				break;
+			}
+		}
+		if (!any_canreturn)
+			result = false;
+	}
+
 	bms_free(attrs_used);
 	bms_free(index_canreturn_attrs);
 
