/*-------------------------------------------------------------------------
 *
 * amutils.c
 *	  SQL-level APIs related to index access methods.
 *
 * Copyright (c) 2016-2024, PostgreSQL Global Development Group
 *
 *
 * IDENTIFICATION
 *	  src/backend/utils/adt/amutils.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/amapi.h"
#include "access/htup_details.h"
#include "catalog/pg_class.h"
#include "catalog/pg_index.h"
#include "utils/builtins.h"
#include "utils/syscache.h"


/* Convert string property name to enum, for efficiency */
struct am_propname
{
	const char *name;
	IndexAMProperty prop;
};

static const struct am_propname am_propnames[] =
{
	{
		"asc", AMPROP_ASC
	},
	{
		"desc", AMPROP_DESC
	},
	{
		"nulls_first", AMPROP_NULLS_FIRST
	},
	{
		"nulls_last", AMPROP_NULLS_LAST
	},
	{
		"orderable", AMPROP_ORDERABLE
	},
	{
		"distance_orderable", AMPROP_DISTANCE_ORDERABLE
	},
	{
		"returnable", AMPROP_RETURNABLE
	},
	{
		"search_array", AMPROP_SEARCH_ARRAY
	},
	{
		"search_nulls", AMPROP_SEARCH_NULLS
	},
	{
		"clusterable", AMPROP_CLUSTERABLE
	},
	{
		"index_scan", AMPROP_INDEX_SCAN
	},
	{
		"bitmap_scan", AMPROP_BITMAP_SCAN
	},
	{
		"backward_scan", AMPROP_BACKWARD_SCAN
	},
	{
		"can_order", AMPROP_CAN_ORDER
	},
	{
		"can_unique", AMPROP_CAN_UNIQUE
	},
	{
		"can_multi_col", AMPROP_CAN_MULTI_COL
	},
	{
		"can_exclude", AMPROP_CAN_EXCLUDE
	},
	{
		"can_include", AMPROP_CAN_INCLUDE
	},
};

static IndexAMProperty
lookup_prop_name(const char *name)
{
	int			i;

	for (i = 0; i < lengthof(am_propnames); i++)
	{
		if (pg_strcasecmp(am_propnames[i].name, name) == 0)
			return am_propnames[i].prop;
	}

	/* We do not throw an error, so that AMs can define their own properties */
	return AMPROP_UNKNOWN;
}

/*
 * Common code for properties that are just bit tests of indoptions.
 *
 * tuple: the pg_index heaptuple
 * attno: identify the index column to test the indoptions of.
 * guard: if false, a boolean false result is forced (saves code in caller).
 * iopt_mask: mask for interesting indoption bit.
 * iopt_expect: value for a "true" result (should be 0 or iopt_mask).
 *
 * Returns false to indicate a NULL result (for "unknown/inapplicable"),
 * otherwise sets *res to the boolean value to return.
 */
static bool
test_indoption(HeapTuple tuple, int attno, bool guard,
			   int16 iopt_mask, int16 iopt_expect,
			   bool *res)
{
	Datum		datum;
	int2vector *indoption;
	int16		indoption_val;

	if (!guard)
	{
		*res = false;
		return true;
	}

	datum = SysCacheGetAttrNotNull(INDEXRELID, tuple, Anum_pg_index_indoption);

	indoption = ((int2vector *) DatumGetPointer(datum));
	indoption_val = indoption->values[attno - 1];

	*res = (indoption_val & iopt_mask) == iopt_expect;

	return true;
}


/*
 * Test property of an index AM, index, or index column.
 *
 * This is common code for different SQL-level funcs, so the amoid and
 * index_oid parameters are mutually exclusive; we look up the amoid from the
 * index_oid if needed, or if no index oid is given, we're looking at AM-wide
 * properties.
 */
static Datum
indexam_property(FunctionCallInfo fcinfo,
				 const char *propname,
				 Oid amoid, Oid index_oid, int attno)
{
	bool		res = false;
	bool		isnull = false;
	int			natts = 0;
	IndexAMProperty prop;
	IndexAmRoutine *routine;

	/* Try to convert property name to enum (no error if not known) */
	prop = lookup_prop_name(propname);

	/* If we have an index OID, look up the AM, and get # of columns too */
	if (OidIsValid(index_oid))
	{
		HeapTuple	tuple;
		Form_pg_class rd_rel;

		Assert(!OidIsValid(amoid));
		tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(index_oid));
		if (!HeapTupleIsValid(tuple))
			PG_RETURN_NULL();
		rd_rel = (Form_pg_class) GETSTRUCT(tuple);
		if (rd_rel->relkind != RELKIND_INDEX &&
			rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
		{
			ReleaseSysCache(tuple);
			PG_RETURN_NULL();
		}
		amoid = rd_rel->relam;
		natts = rd_rel->relnatts;
		ReleaseSysCache(tuple);
	}

	/*
	 * At this point, either index_oid == InvalidOid or it's a valid index
	 * OID. Also, after this test and the one below, either attno == 0 for
	 * index-wide or AM-wide tests, or it's a valid column number in a valid
	 * index.
	 */
	if (attno < 0 || attno > natts)
		PG_RETURN_NULL();

	/*
	 * Get AM information.  If we don't have a valid AM OID, return NULL.
	 */
	routine = GetIndexAmRoutineByAmId(amoid, true);
	if (routine == NULL)
		PG_RETURN_NULL();

	/*
	 * If there's an AM property routine, give it a chance to override the
	 * generic logic.  Proceed if it returns false.
	 */
	if (routine->amproperty &&
		routine->amproperty(index_oid, attno, prop, propname,
							&res, &isnull))
	{
		if (isnull)
			PG_RETURN_NULL();
		PG_RETURN_BOOL(res);
	}

	if (attno > 0)
	{
		HeapTuple	tuple;
		Form_pg_index rd_index;
		bool		iskey = true;

		/*
		 * Handle column-level properties. Many of these need the pg_index row
		 * (which we also need to use to check for nonkey atts) so we fetch
		 * that first.
		 */
		tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(index_oid));
		if (!HeapTupleIsValid(tuple))
			PG_RETURN_NULL();
		rd_index = (Form_pg_index) GETSTRUCT(tuple);

		Assert(index_oid == rd_index->indexrelid);
		Assert(attno > 0 && attno <= rd_index->indnatts);

		isnull = true;

		/*
		 * If amcaninclude, we might be looking at an attno for a nonkey
		 * column, for which we (generically) assume that most properties are
		 * null.
		 */
		if (routine->amcaninclude
			&& attno > rd_index->indnkeyatts)
			iskey = false;

		switch (prop)
		{
			case AMPROP_ASC:
				if (iskey &&
					test_indoption(tuple, attno, routine->amcanorder,
								   INDOPTION_DESC, 0, &res))
					isnull = false;
				break;

			case AMPROP_DESC:
				if (iskey &&
					test_indoption(tuple, attno, routine->amcanorder,
								   INDOPTION_DESC, INDOPTION_DESC, &res))
					isnull = false;
				break;

			case AMPROP_NULLS_FIRST:
				if (iskey &&
					test_indoption(tuple, attno, routine->amcanorder,
								   INDOPTION_NULLS_FIRST, INDOPTION_NULLS_FIRST, &res))
					isnull = false;
				break;

			case AMPROP_NULLS_LAST:
				if (iskey &&
					test_indoption(tuple, attno, routine->amcanorder,
								   INDOPTION_NULLS_FIRST, 0, &res))
					isnull = false;
				break;

			case AMPROP_ORDERABLE:

				/*
				 * generic assumption is that nonkey columns are not orderable
				 */
				res = iskey ? routine->amcanorder : false;
				isnull = false;
				break;

			case AMPROP_DISTANCE_ORDERABLE:

				/*
				 * The conditions for whether a column is distance-orderable
				 * are really up to the AM (at time of writing, only GiST
				 * supports it at all). The planner has its own idea based on
				 * whether it finds an operator with amoppurpose 'o', but
				 * getting there from just the index column type seems like a
				 * lot of work. So instead we expect the AM to handle this in
				 * its amproperty routine. The generic result is to return
				 * false if the AM says it never supports this, or if this is
				 * a nonkey column, and null otherwise (meaning we don't
				 * know).
				 */
				if (!iskey || !routine->amcanorderbyop)
				{
					res = false;
					isnull = false;
				}
				break;

			case AMPROP_RETURNABLE:

				/* note that we ignore iskey for this property */

				isnull = false;
				res = false;

				if (routine->amcanreturn)
				{
					/*
					 * If possible, the AM should handle this test in its
					 * amproperty function without opening the rel. But this
					 * is the generic fallback if it does not.
					 */
					Relation	indexrel = index_open(index_oid, AccessShareLock);

					res = index_can_return(indexrel, attno);
					index_close(indexrel, AccessShareLock);
				}
				break;

			case AMPROP_SEARCH_ARRAY:
				if (iskey)
				{
					res = routine->amsearcharray;
					isnull = false;
				}
				break;

			case AMPROP_SEARCH_NULLS:
				if (iskey)
				{
					res = routine->amsearchnulls;
					isnull = false;
				}
				break;

			default:
				break;
		}

		ReleaseSysCache(tuple);

		if (!isnull)
			PG_RETURN_BOOL(res);
		PG_RETURN_NULL();
	}

	if (OidIsValid(index_oid))
	{
		/*
		 * Handle index-level properties.  Currently, these only depend on the
		 * AM, but that might not be true forever, so we make users name an
		 * index not just an AM.
		 */
		switch (prop)
		{
			case AMPROP_CLUSTERABLE:
				PG_RETURN_BOOL(routine->amclusterable);

			case AMPROP_INDEX_SCAN:
				PG_RETURN_BOOL(routine->amgettuple ? true : false);

			case AMPROP_BITMAP_SCAN:
				PG_RETURN_BOOL(routine->amgetbitmap ? true : false);

			case AMPROP_BACKWARD_SCAN:
				PG_RETURN_BOOL(routine->amcanbackward);

			default:
				PG_RETURN_NULL();
		}
	}

	/*
	 * Handle AM-level properties (those that control what you can say in
	 * CREATE INDEX).
	 */
	switch (prop)
	{
		case AMPROP_CAN_ORDER:
			PG_RETURN_BOOL(routine->amcanorder);

		case AMPROP_CAN_UNIQUE:
			PG_RETURN_BOOL(routine->amcanunique);

		case AMPROP_CAN_MULTI_COL:
			PG_RETURN_BOOL(routine->amcanmulticol);

		case AMPROP_CAN_EXCLUDE:
			PG_RETURN_BOOL(routine->amgettuple ? true : false);

		case AMPROP_CAN_INCLUDE:
			PG_RETURN_BOOL(routine->amcaninclude);

		default:
			PG_RETURN_NULL();
	}
}

/*
 * Test property of an AM specified by AM OID
 */
Datum
pg_indexam_has_property(PG_FUNCTION_ARGS)
{
	Oid			amoid = PG_GETARG_OID(0);
	char	   *propname = text_to_cstring(PG_GETARG_TEXT_PP(1));

	return indexam_property(fcinfo, propname, amoid, InvalidOid, 0);
}

/*
 * Test property of an index specified by index OID
 */
Datum
pg_index_has_property(PG_FUNCTION_ARGS)
{
	Oid			relid = PG_GETARG_OID(0);
	char	   *propname = text_to_cstring(PG_GETARG_TEXT_PP(1));

	return indexam_property(fcinfo, propname, InvalidOid, relid, 0);
}

/*
 * Test property of an index column specified by index OID and column number
 */
Datum
pg_index_column_has_property(PG_FUNCTION_ARGS)
{
	Oid			relid = PG_GETARG_OID(0);
	int32		attno = PG_GETARG_INT32(1);
	char	   *propname = text_to_cstring(PG_GETARG_TEXT_PP(2));

	/* Reject attno 0 immediately, so that attno > 0 identifies this case */
	if (attno <= 0)
		PG_RETURN_NULL();

	return indexam_property(fcinfo, propname, InvalidOid, relid, attno);
}

/*
 * Return the name of the given phase, as used for progress reporting by the
 * given AM.
 */
Datum
pg_indexam_progress_phasename(PG_FUNCTION_ARGS)
{
	Oid			amoid = PG_GETARG_OID(0);
	int32		phasenum = PG_GETARG_INT32(1);
	IndexAmRoutine *routine;
	char	   *name;

	routine = GetIndexAmRoutineByAmId(amoid, true);
	if (routine == NULL || !routine->ambuildphasename)
		PG_RETURN_NULL();

	name = routine->ambuildphasename(phasenum);
	if (!name)
		PG_RETURN_NULL();

	PG_RETURN_DATUM(CStringGetTextDatum(name));
}
