mirror of
https://github.com/postgres/postgres.git
synced 2025-04-22 23:02:54 +03:00
Teach btree to handle ScalarArrayOpExpr quals natively.
This allows "indexedcol op ANY(ARRAY[...])" conditions to be used in plain indexscans, and particularly in index-only scans.
This commit is contained in:
parent
0898d71f66
commit
9e8da0f757
@ -491,6 +491,13 @@
|
|||||||
for the first index column?</entry>
|
for the first index column?</entry>
|
||||||
</row>
|
</row>
|
||||||
|
|
||||||
|
<row>
|
||||||
|
<entry><structfield>amsearcharray</structfield></entry>
|
||||||
|
<entry><type>bool</type></entry>
|
||||||
|
<entry></entry>
|
||||||
|
<entry>Does the access method support <literal>ScalarArrayOpExpr</> searches?</entry>
|
||||||
|
</row>
|
||||||
|
|
||||||
<row>
|
<row>
|
||||||
<entry><structfield>amsearchnulls</structfield></entry>
|
<entry><structfield>amsearchnulls</structfield></entry>
|
||||||
<entry><type>bool</type></entry>
|
<entry><type>bool</type></entry>
|
||||||
|
@ -276,39 +276,63 @@ btgettuple(PG_FUNCTION_ARGS)
|
|||||||
scan->xs_recheck = false;
|
scan->xs_recheck = false;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If we've already initialized this scan, we can just advance it in the
|
* If we have any array keys, initialize them during first call for a
|
||||||
* appropriate direction. If we haven't done so yet, we call a routine to
|
* scan. We can't do this in btrescan because we don't know the scan
|
||||||
* get the first item in the scan.
|
* direction at that time.
|
||||||
*/
|
*/
|
||||||
if (BTScanPosIsValid(so->currPos))
|
if (so->numArrayKeys && !BTScanPosIsValid(so->currPos))
|
||||||
|
{
|
||||||
|
/* punt if we have any unsatisfiable array keys */
|
||||||
|
if (so->numArrayKeys < 0)
|
||||||
|
PG_RETURN_BOOL(false);
|
||||||
|
|
||||||
|
_bt_start_array_keys(scan, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This loop handles advancing to the next array elements, if any */
|
||||||
|
do
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* Check to see if we should kill the previously-fetched tuple.
|
* If we've already initialized this scan, we can just advance it in
|
||||||
|
* the appropriate direction. If we haven't done so yet, we call
|
||||||
|
* _bt_first() to get the first item in the scan.
|
||||||
*/
|
*/
|
||||||
if (scan->kill_prior_tuple)
|
if (!BTScanPosIsValid(so->currPos))
|
||||||
|
res = _bt_first(scan, dir);
|
||||||
|
else
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* Yes, remember it for later. (We'll deal with all such tuples
|
* Check to see if we should kill the previously-fetched tuple.
|
||||||
* at once right before leaving the index page.) The test for
|
|
||||||
* numKilled overrun is not just paranoia: if the caller reverses
|
|
||||||
* direction in the indexscan then the same item might get entered
|
|
||||||
* multiple times. It's not worth trying to optimize that, so we
|
|
||||||
* don't detect it, but instead just forget any excess entries.
|
|
||||||
*/
|
*/
|
||||||
if (so->killedItems == NULL)
|
if (scan->kill_prior_tuple)
|
||||||
so->killedItems = (int *)
|
{
|
||||||
palloc(MaxIndexTuplesPerPage * sizeof(int));
|
/*
|
||||||
if (so->numKilled < MaxIndexTuplesPerPage)
|
* Yes, remember it for later. (We'll deal with all such
|
||||||
so->killedItems[so->numKilled++] = so->currPos.itemIndex;
|
* tuples at once right before leaving the index page.) The
|
||||||
|
* test for numKilled overrun is not just paranoia: if the
|
||||||
|
* caller reverses direction in the indexscan then the same
|
||||||
|
* item might get entered multiple times. It's not worth
|
||||||
|
* trying to optimize that, so we don't detect it, but instead
|
||||||
|
* just forget any excess entries.
|
||||||
|
*/
|
||||||
|
if (so->killedItems == NULL)
|
||||||
|
so->killedItems = (int *)
|
||||||
|
palloc(MaxIndexTuplesPerPage * sizeof(int));
|
||||||
|
if (so->numKilled < MaxIndexTuplesPerPage)
|
||||||
|
so->killedItems[so->numKilled++] = so->currPos.itemIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Now continue the scan.
|
||||||
|
*/
|
||||||
|
res = _bt_next(scan, dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* If we have a tuple, return it ... */
|
||||||
* Now continue the scan.
|
if (res)
|
||||||
*/
|
break;
|
||||||
res = _bt_next(scan, dir);
|
/* ... otherwise see if we have more array keys to deal with */
|
||||||
}
|
} while (so->numArrayKeys && _bt_advance_array_keys(scan, dir));
|
||||||
else
|
|
||||||
res = _bt_first(scan, dir);
|
|
||||||
|
|
||||||
PG_RETURN_BOOL(res);
|
PG_RETURN_BOOL(res);
|
||||||
}
|
}
|
||||||
@ -325,35 +349,50 @@ btgetbitmap(PG_FUNCTION_ARGS)
|
|||||||
int64 ntids = 0;
|
int64 ntids = 0;
|
||||||
ItemPointer heapTid;
|
ItemPointer heapTid;
|
||||||
|
|
||||||
/* Fetch the first page & tuple. */
|
/*
|
||||||
if (!_bt_first(scan, ForwardScanDirection))
|
* If we have any array keys, initialize them.
|
||||||
|
*/
|
||||||
|
if (so->numArrayKeys)
|
||||||
{
|
{
|
||||||
/* empty scan */
|
/* punt if we have any unsatisfiable array keys */
|
||||||
PG_RETURN_INT64(0);
|
if (so->numArrayKeys < 0)
|
||||||
}
|
PG_RETURN_INT64(ntids);
|
||||||
/* Save tuple ID, and continue scanning */
|
|
||||||
heapTid = &scan->xs_ctup.t_self;
|
|
||||||
tbm_add_tuples(tbm, heapTid, 1, false);
|
|
||||||
ntids++;
|
|
||||||
|
|
||||||
for (;;)
|
_bt_start_array_keys(scan, ForwardScanDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This loop handles advancing to the next array elements, if any */
|
||||||
|
do
|
||||||
{
|
{
|
||||||
/*
|
/* Fetch the first page & tuple */
|
||||||
* Advance to next tuple within page. This is the same as the easy
|
if (_bt_first(scan, ForwardScanDirection))
|
||||||
* case in _bt_next().
|
|
||||||
*/
|
|
||||||
if (++so->currPos.itemIndex > so->currPos.lastItem)
|
|
||||||
{
|
{
|
||||||
/* let _bt_next do the heavy lifting */
|
/* Save tuple ID, and continue scanning */
|
||||||
if (!_bt_next(scan, ForwardScanDirection))
|
heapTid = &scan->xs_ctup.t_self;
|
||||||
break;
|
tbm_add_tuples(tbm, heapTid, 1, false);
|
||||||
}
|
ntids++;
|
||||||
|
|
||||||
/* Save tuple ID, and continue scanning */
|
for (;;)
|
||||||
heapTid = &so->currPos.items[so->currPos.itemIndex].heapTid;
|
{
|
||||||
tbm_add_tuples(tbm, heapTid, 1, false);
|
/*
|
||||||
ntids++;
|
* Advance to next tuple within page. This is the same as the
|
||||||
}
|
* easy case in _bt_next().
|
||||||
|
*/
|
||||||
|
if (++so->currPos.itemIndex > so->currPos.lastItem)
|
||||||
|
{
|
||||||
|
/* let _bt_next do the heavy lifting */
|
||||||
|
if (!_bt_next(scan, ForwardScanDirection))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Save tuple ID, and continue scanning */
|
||||||
|
heapTid = &so->currPos.items[so->currPos.itemIndex].heapTid;
|
||||||
|
tbm_add_tuples(tbm, heapTid, 1, false);
|
||||||
|
ntids++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Now see if we have more array keys to deal with */
|
||||||
|
} while (so->numArrayKeys && _bt_advance_array_keys(scan, ForwardScanDirection));
|
||||||
|
|
||||||
PG_RETURN_INT64(ntids);
|
PG_RETURN_INT64(ntids);
|
||||||
}
|
}
|
||||||
@ -383,6 +422,12 @@ btbeginscan(PG_FUNCTION_ARGS)
|
|||||||
so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
|
so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
|
||||||
else
|
else
|
||||||
so->keyData = NULL;
|
so->keyData = NULL;
|
||||||
|
|
||||||
|
so->arrayKeyData = NULL; /* assume no array keys for now */
|
||||||
|
so->numArrayKeys = 0;
|
||||||
|
so->arrayKeys = NULL;
|
||||||
|
so->arrayContext = NULL;
|
||||||
|
|
||||||
so->killedItems = NULL; /* until needed */
|
so->killedItems = NULL; /* until needed */
|
||||||
so->numKilled = 0;
|
so->numKilled = 0;
|
||||||
|
|
||||||
@ -460,6 +505,9 @@ btrescan(PG_FUNCTION_ARGS)
|
|||||||
scan->numberOfKeys * sizeof(ScanKeyData));
|
scan->numberOfKeys * sizeof(ScanKeyData));
|
||||||
so->numberOfKeys = 0; /* until _bt_preprocess_keys sets it */
|
so->numberOfKeys = 0; /* until _bt_preprocess_keys sets it */
|
||||||
|
|
||||||
|
/* If any keys are SK_SEARCHARRAY type, set up array-key info */
|
||||||
|
_bt_preprocess_array_keys(scan);
|
||||||
|
|
||||||
PG_RETURN_VOID();
|
PG_RETURN_VOID();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,10 +538,13 @@ btendscan(PG_FUNCTION_ARGS)
|
|||||||
so->markItemIndex = -1;
|
so->markItemIndex = -1;
|
||||||
|
|
||||||
/* Release storage */
|
/* Release storage */
|
||||||
if (so->killedItems != NULL)
|
|
||||||
pfree(so->killedItems);
|
|
||||||
if (so->keyData != NULL)
|
if (so->keyData != NULL)
|
||||||
pfree(so->keyData);
|
pfree(so->keyData);
|
||||||
|
/* so->arrayKeyData and so->arrayKeys are in arrayContext */
|
||||||
|
if (so->arrayContext != NULL)
|
||||||
|
MemoryContextDelete(so->arrayContext);
|
||||||
|
if (so->killedItems != NULL)
|
||||||
|
pfree(so->killedItems);
|
||||||
if (so->currTuples != NULL)
|
if (so->currTuples != NULL)
|
||||||
pfree(so->currTuples);
|
pfree(so->currTuples);
|
||||||
/* so->markTuples should not be pfree'd, see btrescan */
|
/* so->markTuples should not be pfree'd, see btrescan */
|
||||||
|
@ -21,10 +21,26 @@
|
|||||||
#include "access/reloptions.h"
|
#include "access/reloptions.h"
|
||||||
#include "access/relscan.h"
|
#include "access/relscan.h"
|
||||||
#include "miscadmin.h"
|
#include "miscadmin.h"
|
||||||
|
#include "utils/array.h"
|
||||||
#include "utils/lsyscache.h"
|
#include "utils/lsyscache.h"
|
||||||
|
#include "utils/memutils.h"
|
||||||
#include "utils/rel.h"
|
#include "utils/rel.h"
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct BTSortArrayContext
|
||||||
|
{
|
||||||
|
FmgrInfo flinfo;
|
||||||
|
Oid collation;
|
||||||
|
bool reverse;
|
||||||
|
} BTSortArrayContext;
|
||||||
|
|
||||||
|
static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
|
||||||
|
StrategyNumber strat,
|
||||||
|
Datum *elems, int nelems);
|
||||||
|
static int _bt_sort_array_elements(IndexScanDesc scan, ScanKey skey,
|
||||||
|
bool reverse,
|
||||||
|
Datum *elems, int nelems);
|
||||||
|
static int _bt_compare_array_elements(const void *a, const void *b, void *arg);
|
||||||
static bool _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
|
static bool _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op,
|
||||||
ScanKey leftarg, ScanKey rightarg,
|
ScanKey leftarg, ScanKey rightarg,
|
||||||
bool *result);
|
bool *result);
|
||||||
@ -158,13 +174,433 @@ _bt_freestack(BTStack stack)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys
|
||||||
|
*
|
||||||
|
* If there are any SK_SEARCHARRAY scan keys, deconstruct the array(s) and
|
||||||
|
* set up BTArrayKeyInfo info for each one that is an equality-type key.
|
||||||
|
* Prepare modified scan keys in so->arrayKeyData, which will hold the current
|
||||||
|
* array elements during each primitive indexscan operation. For inequality
|
||||||
|
* array keys, it's sufficient to find the extreme element value and replace
|
||||||
|
* the whole array with that scalar value.
|
||||||
|
*
|
||||||
|
* Note: the reason we need so->arrayKeyData, rather than just scribbling
|
||||||
|
* on scan->keyData, is that callers are permitted to call btrescan without
|
||||||
|
* supplying a new set of scankey data.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
_bt_preprocess_array_keys(IndexScanDesc scan)
|
||||||
|
{
|
||||||
|
BTScanOpaque so = (BTScanOpaque) scan->opaque;
|
||||||
|
int numberOfKeys = scan->numberOfKeys;
|
||||||
|
int16 *indoption = scan->indexRelation->rd_indoption;
|
||||||
|
int numArrayKeys;
|
||||||
|
ScanKey cur;
|
||||||
|
int i;
|
||||||
|
MemoryContext oldContext;
|
||||||
|
|
||||||
|
/* Quick check to see if there are any array keys */
|
||||||
|
numArrayKeys = 0;
|
||||||
|
for (i = 0; i < numberOfKeys; i++)
|
||||||
|
{
|
||||||
|
cur = &scan->keyData[i];
|
||||||
|
if (cur->sk_flags & SK_SEARCHARRAY)
|
||||||
|
{
|
||||||
|
numArrayKeys++;
|
||||||
|
Assert(!(cur->sk_flags & (SK_ROW_HEADER | SK_SEARCHNULL | SK_SEARCHNOTNULL)));
|
||||||
|
/* If any arrays are null as a whole, we can quit right now. */
|
||||||
|
if (cur->sk_flags & SK_ISNULL)
|
||||||
|
{
|
||||||
|
so->numArrayKeys = -1;
|
||||||
|
so->arrayKeyData = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quit if nothing to do. */
|
||||||
|
if (numArrayKeys == 0)
|
||||||
|
{
|
||||||
|
so->numArrayKeys = 0;
|
||||||
|
so->arrayKeyData = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Make a scan-lifespan context to hold array-associated data, or reset
|
||||||
|
* it if we already have one from a previous rescan cycle.
|
||||||
|
*/
|
||||||
|
if (so->arrayContext == NULL)
|
||||||
|
so->arrayContext = AllocSetContextCreate(CurrentMemoryContext,
|
||||||
|
"BTree Array Context",
|
||||||
|
ALLOCSET_SMALL_MINSIZE,
|
||||||
|
ALLOCSET_SMALL_INITSIZE,
|
||||||
|
ALLOCSET_SMALL_MAXSIZE);
|
||||||
|
else
|
||||||
|
MemoryContextReset(so->arrayContext);
|
||||||
|
|
||||||
|
oldContext = MemoryContextSwitchTo(so->arrayContext);
|
||||||
|
|
||||||
|
/* Create modifiable copy of scan->keyData in the workspace context */
|
||||||
|
so->arrayKeyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
|
||||||
|
memcpy(so->arrayKeyData,
|
||||||
|
scan->keyData,
|
||||||
|
scan->numberOfKeys * sizeof(ScanKeyData));
|
||||||
|
|
||||||
|
/* Allocate space for per-array data in the workspace context */
|
||||||
|
so->arrayKeys = (BTArrayKeyInfo *) palloc0(numArrayKeys * sizeof(BTArrayKeyInfo));
|
||||||
|
|
||||||
|
/* Now process each array key */
|
||||||
|
numArrayKeys = 0;
|
||||||
|
for (i = 0; i < numberOfKeys; i++)
|
||||||
|
{
|
||||||
|
ArrayType *arrayval;
|
||||||
|
int16 elmlen;
|
||||||
|
bool elmbyval;
|
||||||
|
char elmalign;
|
||||||
|
int num_elems;
|
||||||
|
Datum *elem_values;
|
||||||
|
bool *elem_nulls;
|
||||||
|
int num_nonnulls;
|
||||||
|
int j;
|
||||||
|
|
||||||
|
cur = &so->arrayKeyData[i];
|
||||||
|
if (!(cur->sk_flags & SK_SEARCHARRAY))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* First, deconstruct the array into elements. Anything allocated
|
||||||
|
* here (including a possibly detoasted array value) is in the
|
||||||
|
* workspace context.
|
||||||
|
*/
|
||||||
|
arrayval = DatumGetArrayTypeP(cur->sk_argument);
|
||||||
|
/* We could cache this data, but not clear it's worth it */
|
||||||
|
get_typlenbyvalalign(ARR_ELEMTYPE(arrayval),
|
||||||
|
&elmlen, &elmbyval, &elmalign);
|
||||||
|
deconstruct_array(arrayval,
|
||||||
|
ARR_ELEMTYPE(arrayval),
|
||||||
|
elmlen, elmbyval, elmalign,
|
||||||
|
&elem_values, &elem_nulls, &num_elems);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compress out any null elements. We can ignore them since we assume
|
||||||
|
* all btree operators are strict.
|
||||||
|
*/
|
||||||
|
num_nonnulls = 0;
|
||||||
|
for (j = 0; j < num_elems; j++)
|
||||||
|
{
|
||||||
|
if (!elem_nulls[j])
|
||||||
|
elem_values[num_nonnulls++] = elem_values[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We could pfree(elem_nulls) now, but not worth the cycles */
|
||||||
|
|
||||||
|
/* If there's no non-nulls, the scan qual is unsatisfiable */
|
||||||
|
if (num_nonnulls == 0)
|
||||||
|
{
|
||||||
|
numArrayKeys = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the comparison operator is not equality, then the array qual
|
||||||
|
* degenerates to a simple comparison against the smallest or largest
|
||||||
|
* non-null array element, as appropriate.
|
||||||
|
*/
|
||||||
|
switch (cur->sk_strategy)
|
||||||
|
{
|
||||||
|
case BTLessStrategyNumber:
|
||||||
|
case BTLessEqualStrategyNumber:
|
||||||
|
cur->sk_argument =
|
||||||
|
_bt_find_extreme_element(scan, cur,
|
||||||
|
BTGreaterStrategyNumber,
|
||||||
|
elem_values, num_nonnulls);
|
||||||
|
continue;
|
||||||
|
case BTEqualStrategyNumber:
|
||||||
|
/* proceed with rest of loop */
|
||||||
|
break;
|
||||||
|
case BTGreaterEqualStrategyNumber:
|
||||||
|
case BTGreaterStrategyNumber:
|
||||||
|
cur->sk_argument =
|
||||||
|
_bt_find_extreme_element(scan, cur,
|
||||||
|
BTLessStrategyNumber,
|
||||||
|
elem_values, num_nonnulls);
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
elog(ERROR, "unrecognized StrategyNumber: %d",
|
||||||
|
(int) cur->sk_strategy);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sort the non-null elements and eliminate any duplicates. We must
|
||||||
|
* sort in the same ordering used by the index column, so that the
|
||||||
|
* successive primitive indexscans produce data in index order.
|
||||||
|
*/
|
||||||
|
num_elems = _bt_sort_array_elements(scan, cur,
|
||||||
|
(indoption[cur->sk_attno - 1] & INDOPTION_DESC) != 0,
|
||||||
|
elem_values, num_nonnulls);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* And set up the BTArrayKeyInfo data.
|
||||||
|
*/
|
||||||
|
so->arrayKeys[numArrayKeys].scan_key = i;
|
||||||
|
so->arrayKeys[numArrayKeys].num_elems = num_elems;
|
||||||
|
so->arrayKeys[numArrayKeys].elem_values = elem_values;
|
||||||
|
numArrayKeys++;
|
||||||
|
}
|
||||||
|
|
||||||
|
so->numArrayKeys = numArrayKeys;
|
||||||
|
|
||||||
|
MemoryContextSwitchTo(oldContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _bt_find_extreme_element() -- get least or greatest array element
|
||||||
|
*
|
||||||
|
* scan and skey identify the index column, whose opfamily determines the
|
||||||
|
* comparison semantics. strat should be BTLessStrategyNumber to get the
|
||||||
|
* least element, or BTGreaterStrategyNumber to get the greatest.
|
||||||
|
*/
|
||||||
|
static Datum
|
||||||
|
_bt_find_extreme_element(IndexScanDesc scan, ScanKey skey,
|
||||||
|
StrategyNumber strat,
|
||||||
|
Datum *elems, int nelems)
|
||||||
|
{
|
||||||
|
Relation rel = scan->indexRelation;
|
||||||
|
Oid elemtype,
|
||||||
|
cmp_op;
|
||||||
|
RegProcedure cmp_proc;
|
||||||
|
FmgrInfo flinfo;
|
||||||
|
Datum result;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Determine the nominal datatype of the array elements. We have to
|
||||||
|
* support the convention that sk_subtype == InvalidOid means the opclass
|
||||||
|
* input type; this is a hack to simplify life for ScanKeyInit().
|
||||||
|
*/
|
||||||
|
elemtype = skey->sk_subtype;
|
||||||
|
if (elemtype == InvalidOid)
|
||||||
|
elemtype = rel->rd_opcintype[skey->sk_attno - 1];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Look up the appropriate comparison operator in the opfamily.
|
||||||
|
*
|
||||||
|
* Note: it's possible that this would fail, if the opfamily is incomplete,
|
||||||
|
* but it seems quite unlikely that an opfamily would omit non-cross-type
|
||||||
|
* comparison operators for any datatype that it supports at all.
|
||||||
|
*/
|
||||||
|
cmp_op = get_opfamily_member(rel->rd_opfamily[skey->sk_attno - 1],
|
||||||
|
elemtype,
|
||||||
|
elemtype,
|
||||||
|
strat);
|
||||||
|
if (!OidIsValid(cmp_op))
|
||||||
|
elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
|
||||||
|
strat, elemtype, elemtype,
|
||||||
|
rel->rd_opfamily[skey->sk_attno - 1]);
|
||||||
|
cmp_proc = get_opcode(cmp_op);
|
||||||
|
if (!RegProcedureIsValid(cmp_proc))
|
||||||
|
elog(ERROR, "missing oprcode for operator %u", cmp_op);
|
||||||
|
|
||||||
|
fmgr_info(cmp_proc, &flinfo);
|
||||||
|
|
||||||
|
Assert(nelems > 0);
|
||||||
|
result = elems[0];
|
||||||
|
for (i = 1; i < nelems; i++)
|
||||||
|
{
|
||||||
|
if (DatumGetBool(FunctionCall2Coll(&flinfo,
|
||||||
|
skey->sk_collation,
|
||||||
|
elems[i],
|
||||||
|
result)))
|
||||||
|
result = elems[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _bt_sort_array_elements() -- sort and de-dup array elements
|
||||||
|
*
|
||||||
|
* The array elements are sorted in-place, and the new number of elements
|
||||||
|
* after duplicate removal is returned.
|
||||||
|
*
|
||||||
|
* scan and skey identify the index column, whose opfamily determines the
|
||||||
|
* comparison semantics. If reverse is true, we sort in descending order.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
_bt_sort_array_elements(IndexScanDesc scan, ScanKey skey,
|
||||||
|
bool reverse,
|
||||||
|
Datum *elems, int nelems)
|
||||||
|
{
|
||||||
|
Relation rel = scan->indexRelation;
|
||||||
|
Oid elemtype;
|
||||||
|
RegProcedure cmp_proc;
|
||||||
|
BTSortArrayContext cxt;
|
||||||
|
int last_non_dup;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (nelems <= 1)
|
||||||
|
return nelems; /* no work to do */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Determine the nominal datatype of the array elements. We have to
|
||||||
|
* support the convention that sk_subtype == InvalidOid means the opclass
|
||||||
|
* input type; this is a hack to simplify life for ScanKeyInit().
|
||||||
|
*/
|
||||||
|
elemtype = skey->sk_subtype;
|
||||||
|
if (elemtype == InvalidOid)
|
||||||
|
elemtype = rel->rd_opcintype[skey->sk_attno - 1];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Look up the appropriate comparison function in the opfamily.
|
||||||
|
*
|
||||||
|
* Note: it's possible that this would fail, if the opfamily is incomplete,
|
||||||
|
* but it seems quite unlikely that an opfamily would omit non-cross-type
|
||||||
|
* support functions for any datatype that it supports at all.
|
||||||
|
*/
|
||||||
|
cmp_proc = get_opfamily_proc(rel->rd_opfamily[skey->sk_attno - 1],
|
||||||
|
elemtype,
|
||||||
|
elemtype,
|
||||||
|
BTORDER_PROC);
|
||||||
|
if (!RegProcedureIsValid(cmp_proc))
|
||||||
|
elog(ERROR, "missing support function %d(%u,%u) in opfamily %u",
|
||||||
|
BTORDER_PROC, elemtype, elemtype,
|
||||||
|
rel->rd_opfamily[skey->sk_attno - 1]);
|
||||||
|
|
||||||
|
/* Sort the array elements */
|
||||||
|
fmgr_info(cmp_proc, &cxt.flinfo);
|
||||||
|
cxt.collation = skey->sk_collation;
|
||||||
|
cxt.reverse = reverse;
|
||||||
|
qsort_arg((void *) elems, nelems, sizeof(Datum),
|
||||||
|
_bt_compare_array_elements, (void *) &cxt);
|
||||||
|
|
||||||
|
/* Now scan the sorted elements and remove duplicates */
|
||||||
|
last_non_dup = 0;
|
||||||
|
for (i = 1; i < nelems; i++)
|
||||||
|
{
|
||||||
|
int32 compare;
|
||||||
|
|
||||||
|
compare = DatumGetInt32(FunctionCall2Coll(&cxt.flinfo,
|
||||||
|
cxt.collation,
|
||||||
|
elems[last_non_dup],
|
||||||
|
elems[i]));
|
||||||
|
if (compare != 0)
|
||||||
|
elems[++last_non_dup] = elems[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return last_non_dup + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* qsort_arg comparator for sorting array elements
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
_bt_compare_array_elements(const void *a, const void *b, void *arg)
|
||||||
|
{
|
||||||
|
Datum da = *((const Datum *) a);
|
||||||
|
Datum db = *((const Datum *) b);
|
||||||
|
BTSortArrayContext *cxt = (BTSortArrayContext *) arg;
|
||||||
|
int32 compare;
|
||||||
|
|
||||||
|
compare = DatumGetInt32(FunctionCall2Coll(&cxt->flinfo,
|
||||||
|
cxt->collation,
|
||||||
|
da, db));
|
||||||
|
if (cxt->reverse)
|
||||||
|
compare = -compare;
|
||||||
|
return compare;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _bt_start_array_keys() -- Initialize array keys at start of a scan
|
||||||
|
*
|
||||||
|
* Set up the cur_elem counters and fill in the first sk_argument value for
|
||||||
|
* each array scankey. We can't do this until we know the scan direction.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
_bt_start_array_keys(IndexScanDesc scan, ScanDirection dir)
|
||||||
|
{
|
||||||
|
BTScanOpaque so = (BTScanOpaque) scan->opaque;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < so->numArrayKeys; i++)
|
||||||
|
{
|
||||||
|
BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
|
||||||
|
ScanKey skey = &so->arrayKeyData[curArrayKey->scan_key];
|
||||||
|
|
||||||
|
Assert(curArrayKey->num_elems > 0);
|
||||||
|
if (ScanDirectionIsBackward(dir))
|
||||||
|
curArrayKey->cur_elem = curArrayKey->num_elems - 1;
|
||||||
|
else
|
||||||
|
curArrayKey->cur_elem = 0;
|
||||||
|
skey->sk_argument = curArrayKey->elem_values[curArrayKey->cur_elem];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _bt_advance_array_keys() -- Advance to next set of array elements
|
||||||
|
*
|
||||||
|
* Returns TRUE if there is another set of values to consider, FALSE if not.
|
||||||
|
* On TRUE result, the scankeys are initialized with the next set of values.
|
||||||
|
*/
|
||||||
|
bool
|
||||||
|
_bt_advance_array_keys(IndexScanDesc scan, ScanDirection dir)
|
||||||
|
{
|
||||||
|
BTScanOpaque so = (BTScanOpaque) scan->opaque;
|
||||||
|
bool found = false;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We must advance the last array key most quickly, since it will
|
||||||
|
* correspond to the lowest-order index column among the available
|
||||||
|
* qualifications. This is necessary to ensure correct ordering of output
|
||||||
|
* when there are multiple array keys.
|
||||||
|
*/
|
||||||
|
for (i = so->numArrayKeys - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
BTArrayKeyInfo *curArrayKey = &so->arrayKeys[i];
|
||||||
|
ScanKey skey = &so->arrayKeyData[curArrayKey->scan_key];
|
||||||
|
int cur_elem = curArrayKey->cur_elem;
|
||||||
|
int num_elems = curArrayKey->num_elems;
|
||||||
|
|
||||||
|
if (ScanDirectionIsBackward(dir))
|
||||||
|
{
|
||||||
|
if (--cur_elem < 0)
|
||||||
|
{
|
||||||
|
cur_elem = num_elems - 1;
|
||||||
|
found = false; /* need to advance next array key */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (++cur_elem >= num_elems)
|
||||||
|
{
|
||||||
|
cur_elem = 0;
|
||||||
|
found = false; /* need to advance next array key */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
curArrayKey->cur_elem = cur_elem;
|
||||||
|
skey->sk_argument = curArrayKey->elem_values[cur_elem];
|
||||||
|
if (found)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* _bt_preprocess_keys() -- Preprocess scan keys
|
* _bt_preprocess_keys() -- Preprocess scan keys
|
||||||
*
|
*
|
||||||
* The caller-supplied search-type keys (in scan->keyData[]) are copied to
|
* The given search-type keys (in scan->keyData[] or so->arrayKeyData[])
|
||||||
* so->keyData[] with possible transformation. scan->numberOfKeys is
|
* are copied to so->keyData[] with possible transformation.
|
||||||
* the number of input keys, so->numberOfKeys gets the number of output
|
* scan->numberOfKeys is the number of input keys, so->numberOfKeys gets
|
||||||
* keys (possibly less, never greater).
|
* the number of output keys (possibly less, never greater).
|
||||||
*
|
*
|
||||||
* The output keys are marked with additional sk_flag bits beyond the
|
* The output keys are marked with additional sk_flag bits beyond the
|
||||||
* system-standard bits supplied by the caller. The DESC and NULLS_FIRST
|
* system-standard bits supplied by the caller. The DESC and NULLS_FIRST
|
||||||
@ -226,8 +662,8 @@ _bt_freestack(BTStack stack)
|
|||||||
*
|
*
|
||||||
* Note: the reason we have to copy the preprocessed scan keys into private
|
* Note: the reason we have to copy the preprocessed scan keys into private
|
||||||
* storage is that we are modifying the array based on comparisons of the
|
* storage is that we are modifying the array based on comparisons of the
|
||||||
* key argument values, which could change on a rescan. Therefore we can't
|
* key argument values, which could change on a rescan or after moving to
|
||||||
* overwrite the caller's data structure.
|
* new elements of array keys. Therefore we can't overwrite the source data.
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
_bt_preprocess_keys(IndexScanDesc scan)
|
_bt_preprocess_keys(IndexScanDesc scan)
|
||||||
@ -253,7 +689,14 @@ _bt_preprocess_keys(IndexScanDesc scan)
|
|||||||
if (numberOfKeys < 1)
|
if (numberOfKeys < 1)
|
||||||
return; /* done if qual-less scan */
|
return; /* done if qual-less scan */
|
||||||
|
|
||||||
inkeys = scan->keyData;
|
/*
|
||||||
|
* Read so->arrayKeyData if array keys are present, else scan->keyData
|
||||||
|
*/
|
||||||
|
if (so->arrayKeyData != NULL)
|
||||||
|
inkeys = so->arrayKeyData;
|
||||||
|
else
|
||||||
|
inkeys = scan->keyData;
|
||||||
|
|
||||||
outkeys = so->keyData;
|
outkeys = so->keyData;
|
||||||
cur = &inkeys[0];
|
cur = &inkeys[0];
|
||||||
/* we check that input keys are correctly ordered */
|
/* we check that input keys are correctly ordered */
|
||||||
|
@ -647,11 +647,13 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags)
|
|||||||
* as specified in access/skey.h. The elements of the row comparison
|
* as specified in access/skey.h. The elements of the row comparison
|
||||||
* can have either constant or non-constant comparison values.
|
* can have either constant or non-constant comparison values.
|
||||||
*
|
*
|
||||||
* 4. ScalarArrayOpExpr ("indexkey op ANY (array-expression)"). For these,
|
* 4. ScalarArrayOpExpr ("indexkey op ANY (array-expression)"). If the index
|
||||||
|
* has rd_am->amsearcharray, we handle these the same as simple operators,
|
||||||
|
* setting the SK_SEARCHARRAY flag to tell the AM to handle them. Otherwise,
|
||||||
* we create a ScanKey with everything filled in except the comparison value,
|
* we create a ScanKey with everything filled in except the comparison value,
|
||||||
* and set up an IndexArrayKeyInfo struct to drive processing of the qual.
|
* and set up an IndexArrayKeyInfo struct to drive processing of the qual.
|
||||||
* (Note that we treat all array-expressions as requiring runtime evaluation,
|
* (Note that if we use an IndexArrayKeyInfo struct, the array expression is
|
||||||
* even if they happen to be constants.)
|
* always treated as requiring runtime evaluation, even if it's a constant.)
|
||||||
*
|
*
|
||||||
* 5. NullTest ("indexkey IS NULL/IS NOT NULL"). We just fill in the
|
* 5. NullTest ("indexkey IS NULL/IS NOT NULL"). We just fill in the
|
||||||
* ScanKey properly.
|
* ScanKey properly.
|
||||||
@ -680,7 +682,7 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags)
|
|||||||
* *numArrayKeys: receives number of array keys
|
* *numArrayKeys: receives number of array keys
|
||||||
*
|
*
|
||||||
* Caller may pass NULL for arrayKeys and numArrayKeys to indicate that
|
* Caller may pass NULL for arrayKeys and numArrayKeys to indicate that
|
||||||
* ScalarArrayOpExpr quals are not supported.
|
* IndexArrayKeyInfos are not supported.
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
ExecIndexBuildScanKeys(PlanState *planstate, Relation index,
|
ExecIndexBuildScanKeys(PlanState *planstate, Relation index,
|
||||||
@ -981,6 +983,8 @@ ExecIndexBuildScanKeys(PlanState *planstate, Relation index,
|
|||||||
{
|
{
|
||||||
/* indexkey op ANY (array-expression) */
|
/* indexkey op ANY (array-expression) */
|
||||||
ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
|
ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
|
||||||
|
int flags = 0;
|
||||||
|
Datum scanvalue;
|
||||||
|
|
||||||
Assert(!isorderby);
|
Assert(!isorderby);
|
||||||
|
|
||||||
@ -1027,23 +1031,72 @@ ExecIndexBuildScanKeys(PlanState *planstate, Relation index,
|
|||||||
|
|
||||||
Assert(rightop != NULL);
|
Assert(rightop != NULL);
|
||||||
|
|
||||||
array_keys[n_array_keys].scan_key = this_scan_key;
|
if (index->rd_am->amsearcharray)
|
||||||
array_keys[n_array_keys].array_expr =
|
{
|
||||||
ExecInitExpr(rightop, planstate);
|
/* Index AM will handle this like a simple operator */
|
||||||
/* the remaining fields were zeroed by palloc0 */
|
flags |= SK_SEARCHARRAY;
|
||||||
n_array_keys++;
|
if (IsA(rightop, Const))
|
||||||
|
{
|
||||||
|
/* OK, simple constant comparison value */
|
||||||
|
scanvalue = ((Const *) rightop)->constvalue;
|
||||||
|
if (((Const *) rightop)->constisnull)
|
||||||
|
flags |= SK_ISNULL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Need to treat this one as a runtime key */
|
||||||
|
if (n_runtime_keys >= max_runtime_keys)
|
||||||
|
{
|
||||||
|
if (max_runtime_keys == 0)
|
||||||
|
{
|
||||||
|
max_runtime_keys = 8;
|
||||||
|
runtime_keys = (IndexRuntimeKeyInfo *)
|
||||||
|
palloc(max_runtime_keys * sizeof(IndexRuntimeKeyInfo));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
max_runtime_keys *= 2;
|
||||||
|
runtime_keys = (IndexRuntimeKeyInfo *)
|
||||||
|
repalloc(runtime_keys, max_runtime_keys * sizeof(IndexRuntimeKeyInfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runtime_keys[n_runtime_keys].scan_key = this_scan_key;
|
||||||
|
runtime_keys[n_runtime_keys].key_expr =
|
||||||
|
ExecInitExpr(rightop, planstate);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Careful here: the runtime expression is not of
|
||||||
|
* op_righttype, but rather is an array of same; so
|
||||||
|
* TypeIsToastable() isn't helpful. However, we can
|
||||||
|
* assume that all array types are toastable.
|
||||||
|
*/
|
||||||
|
runtime_keys[n_runtime_keys].key_toastable = true;
|
||||||
|
n_runtime_keys++;
|
||||||
|
scanvalue = (Datum) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Executor has to expand the array value */
|
||||||
|
array_keys[n_array_keys].scan_key = this_scan_key;
|
||||||
|
array_keys[n_array_keys].array_expr =
|
||||||
|
ExecInitExpr(rightop, planstate);
|
||||||
|
/* the remaining fields were zeroed by palloc0 */
|
||||||
|
n_array_keys++;
|
||||||
|
scanvalue = (Datum) 0;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* initialize the scan key's fields appropriately
|
* initialize the scan key's fields appropriately
|
||||||
*/
|
*/
|
||||||
ScanKeyEntryInitialize(this_scan_key,
|
ScanKeyEntryInitialize(this_scan_key,
|
||||||
0, /* flags */
|
flags,
|
||||||
varattno, /* attribute number to scan */
|
varattno, /* attribute number to scan */
|
||||||
op_strategy, /* op's strategy */
|
op_strategy, /* op's strategy */
|
||||||
op_righttype, /* strategy subtype */
|
op_righttype, /* strategy subtype */
|
||||||
saop->inputcollid, /* collation */
|
saop->inputcollid, /* collation */
|
||||||
opfuncid, /* reg proc to use */
|
opfuncid, /* reg proc to use */
|
||||||
(Datum) 0); /* constant */
|
scanvalue); /* constant */
|
||||||
}
|
}
|
||||||
else if (IsA(clause, NullTest))
|
else if (IsA(clause, NullTest))
|
||||||
{
|
{
|
||||||
|
@ -394,9 +394,14 @@ cost_index(IndexPath *path, PlannerInfo *root,
|
|||||||
if (indexonly)
|
if (indexonly)
|
||||||
pages_fetched = ceil(pages_fetched * (1.0 - baserel->allvisfrac));
|
pages_fetched = ceil(pages_fetched * (1.0 - baserel->allvisfrac));
|
||||||
|
|
||||||
min_IO_cost = spc_random_page_cost;
|
if (pages_fetched > 0)
|
||||||
if (pages_fetched > 1)
|
{
|
||||||
min_IO_cost += (pages_fetched - 1) * spc_seq_page_cost;
|
min_IO_cost = spc_random_page_cost;
|
||||||
|
if (pages_fetched > 1)
|
||||||
|
min_IO_cost += (pages_fetched - 1) * spc_seq_page_cost;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
min_IO_cost = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -48,9 +48,9 @@
|
|||||||
/* Whether to use ScalarArrayOpExpr to build index qualifications */
|
/* Whether to use ScalarArrayOpExpr to build index qualifications */
|
||||||
typedef enum
|
typedef enum
|
||||||
{
|
{
|
||||||
SAOP_FORBID, /* Do not use ScalarArrayOpExpr */
|
SAOP_PER_AM, /* Use ScalarArrayOpExpr if amsearcharray */
|
||||||
SAOP_ALLOW, /* OK to use ScalarArrayOpExpr */
|
SAOP_ALLOW, /* Use ScalarArrayOpExpr for all indexes */
|
||||||
SAOP_REQUIRE /* Require ScalarArrayOpExpr */
|
SAOP_REQUIRE /* Require ScalarArrayOpExpr to be used */
|
||||||
} SaOpControl;
|
} SaOpControl;
|
||||||
|
|
||||||
/* Whether we are looking for plain indexscan, bitmap scan, or either */
|
/* Whether we are looking for plain indexscan, bitmap scan, or either */
|
||||||
@ -196,7 +196,7 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel)
|
|||||||
*/
|
*/
|
||||||
indexpaths = find_usable_indexes(root, rel,
|
indexpaths = find_usable_indexes(root, rel,
|
||||||
rel->baserestrictinfo, NIL,
|
rel->baserestrictinfo, NIL,
|
||||||
true, NULL, SAOP_FORBID, ST_ANYSCAN);
|
true, NULL, SAOP_PER_AM, ST_ANYSCAN);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Submit all the ones that can form plain IndexScan plans to add_path.
|
* Submit all the ones that can form plain IndexScan plans to add_path.
|
||||||
@ -233,8 +233,9 @@ create_index_paths(PlannerInfo *root, RelOptInfo *rel)
|
|||||||
bitindexpaths = list_concat(bitindexpaths, indexpaths);
|
bitindexpaths = list_concat(bitindexpaths, indexpaths);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Likewise, generate paths using ScalarArrayOpExpr clauses; these can't
|
* Likewise, generate paths using executor-managed ScalarArrayOpExpr
|
||||||
* be simple indexscans but they can be used in bitmap scans.
|
* clauses; these can't be simple indexscans but they can be used in
|
||||||
|
* bitmap scans.
|
||||||
*/
|
*/
|
||||||
indexpaths = find_saop_paths(root, rel,
|
indexpaths = find_saop_paths(root, rel,
|
||||||
rel->baserestrictinfo, NIL,
|
rel->baserestrictinfo, NIL,
|
||||||
@ -337,6 +338,14 @@ find_usable_indexes(PlannerInfo *root, RelOptInfo *rel,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If we're doing find_saop_paths(), we can skip indexes that support
|
||||||
|
* ScalarArrayOpExpr natively. We already generated all the potential
|
||||||
|
* indexpaths for them, so no need to do anything more.
|
||||||
|
*/
|
||||||
|
if (saop_control == SAOP_REQUIRE && index->amsearcharray)
|
||||||
|
continue;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Ignore partial indexes that do not match the query. If a partial
|
* Ignore partial indexes that do not match the query. If a partial
|
||||||
* index is marked predOK then we know it's OK; otherwise, if we are
|
* index is marked predOK then we know it's OK; otherwise, if we are
|
||||||
@ -492,10 +501,10 @@ find_usable_indexes(PlannerInfo *root, RelOptInfo *rel,
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* find_saop_paths
|
* find_saop_paths
|
||||||
* Find all the potential indexpaths that make use of ScalarArrayOpExpr
|
* Find all the potential indexpaths that make use of executor-managed
|
||||||
* clauses. The executor only supports these in bitmap scans, not
|
* ScalarArrayOpExpr clauses. The executor only supports these in bitmap
|
||||||
* plain indexscans, so we need to segregate them from the normal case.
|
* scans, not plain indexscans, so we need to segregate them from the
|
||||||
* Otherwise, same API as find_usable_indexes().
|
* normal case. Otherwise, same API as find_usable_indexes().
|
||||||
* Returns a list of IndexPaths.
|
* Returns a list of IndexPaths.
|
||||||
*/
|
*/
|
||||||
static List *
|
static List *
|
||||||
@ -1287,9 +1296,10 @@ group_clauses_by_indexkey(IndexOptInfo *index,
|
|||||||
* expand_indexqual_rowcompare().
|
* expand_indexqual_rowcompare().
|
||||||
*
|
*
|
||||||
* It is also possible to match ScalarArrayOpExpr clauses to indexes, when
|
* It is also possible to match ScalarArrayOpExpr clauses to indexes, when
|
||||||
* the clause is of the form "indexkey op ANY (arrayconst)". Since the
|
* the clause is of the form "indexkey op ANY (arrayconst)". Since not
|
||||||
* executor can only handle these in the context of bitmap index scans,
|
* all indexes handle these natively, and the executor implements them
|
||||||
* our caller specifies whether to allow these or not.
|
* only in the context of bitmap index scans, our caller specifies whether
|
||||||
|
* to allow these or not.
|
||||||
*
|
*
|
||||||
* For boolean indexes, it is also possible to match the clause directly
|
* For boolean indexes, it is also possible to match the clause directly
|
||||||
* to the indexkey; or perhaps the clause is (NOT indexkey).
|
* to the indexkey; or perhaps the clause is (NOT indexkey).
|
||||||
@ -1357,8 +1367,8 @@ match_clause_to_indexcol(IndexOptInfo *index,
|
|||||||
expr_coll = ((OpExpr *) clause)->inputcollid;
|
expr_coll = ((OpExpr *) clause)->inputcollid;
|
||||||
plain_op = true;
|
plain_op = true;
|
||||||
}
|
}
|
||||||
else if (saop_control != SAOP_FORBID &&
|
else if (clause && IsA(clause, ScalarArrayOpExpr) &&
|
||||||
clause && IsA(clause, ScalarArrayOpExpr))
|
(index->amsearcharray || saop_control != SAOP_PER_AM))
|
||||||
{
|
{
|
||||||
ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
|
ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
|
||||||
|
|
||||||
@ -2089,12 +2099,12 @@ best_inner_indexscan(PlannerInfo *root, RelOptInfo *rel,
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Find all the index paths that are usable for this join, except for
|
* Find all the index paths that are usable for this join, except for
|
||||||
* stuff involving OR and ScalarArrayOpExpr clauses.
|
* stuff involving OR and executor-managed ScalarArrayOpExpr clauses.
|
||||||
*/
|
*/
|
||||||
allindexpaths = find_usable_indexes(root, rel,
|
allindexpaths = find_usable_indexes(root, rel,
|
||||||
clause_list, NIL,
|
clause_list, NIL,
|
||||||
false, outer_rel,
|
false, outer_rel,
|
||||||
SAOP_FORBID,
|
SAOP_PER_AM,
|
||||||
ST_ANYSCAN);
|
ST_ANYSCAN);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -2123,8 +2133,9 @@ best_inner_indexscan(PlannerInfo *root, RelOptInfo *rel,
|
|||||||
outer_rel));
|
outer_rel));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Likewise, generate paths using ScalarArrayOpExpr clauses; these can't
|
* Likewise, generate paths using executor-managed ScalarArrayOpExpr
|
||||||
* be simple indexscans but they can be used in bitmap scans.
|
* clauses; these can't be simple indexscans but they can be used in
|
||||||
|
* bitmap scans.
|
||||||
*/
|
*/
|
||||||
bitindexpaths = list_concat(bitindexpaths,
|
bitindexpaths = list_concat(bitindexpaths,
|
||||||
find_saop_paths(root, rel,
|
find_saop_paths(root, rel,
|
||||||
|
@ -215,6 +215,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
|
|||||||
info->amcanorderbyop = indexRelation->rd_am->amcanorderbyop;
|
info->amcanorderbyop = indexRelation->rd_am->amcanorderbyop;
|
||||||
info->amcanreturn = indexRelation->rd_am->amcanreturn;
|
info->amcanreturn = indexRelation->rd_am->amcanreturn;
|
||||||
info->amoptionalkey = indexRelation->rd_am->amoptionalkey;
|
info->amoptionalkey = indexRelation->rd_am->amoptionalkey;
|
||||||
|
info->amsearcharray = indexRelation->rd_am->amsearcharray;
|
||||||
info->amsearchnulls = indexRelation->rd_am->amsearchnulls;
|
info->amsearchnulls = indexRelation->rd_am->amsearchnulls;
|
||||||
info->amhasgettuple = OidIsValid(indexRelation->rd_am->amgettuple);
|
info->amhasgettuple = OidIsValid(indexRelation->rd_am->amgettuple);
|
||||||
info->amhasgetbitmap = OidIsValid(indexRelation->rd_am->amgetbitmap);
|
info->amhasgetbitmap = OidIsValid(indexRelation->rd_am->amgetbitmap);
|
||||||
|
@ -6385,14 +6385,7 @@ btcostestimate(PG_FUNCTION_ARGS)
|
|||||||
* is that multiple columns dilute the importance of the first column's
|
* is that multiple columns dilute the importance of the first column's
|
||||||
* ordering, but don't negate it entirely. Before 8.0 we divided the
|
* ordering, but don't negate it entirely. Before 8.0 we divided the
|
||||||
* correlation by the number of columns, but that seems too strong.)
|
* correlation by the number of columns, but that seems too strong.)
|
||||||
*
|
|
||||||
* We can skip all this if we found a ScalarArrayOpExpr, because then the
|
|
||||||
* call must be for a bitmap index scan, and the caller isn't going to
|
|
||||||
* care what the index correlation is.
|
|
||||||
*/
|
*/
|
||||||
if (found_saop)
|
|
||||||
PG_RETURN_VOID();
|
|
||||||
|
|
||||||
MemSet(&vardata, 0, sizeof(vardata));
|
MemSet(&vardata, 0, sizeof(vardata));
|
||||||
|
|
||||||
if (index->indexkeys[0] != 0)
|
if (index->indexkeys[0] != 0)
|
||||||
|
@ -525,6 +525,15 @@ typedef BTScanPosData *BTScanPos;
|
|||||||
|
|
||||||
#define BTScanPosIsValid(scanpos) BufferIsValid((scanpos).buf)
|
#define BTScanPosIsValid(scanpos) BufferIsValid((scanpos).buf)
|
||||||
|
|
||||||
|
/* We need one of these for each equality-type SK_SEARCHARRAY scan key */
|
||||||
|
typedef struct BTArrayKeyInfo
|
||||||
|
{
|
||||||
|
int scan_key; /* index of associated key in arrayKeyData */
|
||||||
|
int cur_elem; /* index of current element in elem_values */
|
||||||
|
int num_elems; /* number of elems in current array value */
|
||||||
|
Datum *elem_values; /* array of num_elems Datums */
|
||||||
|
} BTArrayKeyInfo;
|
||||||
|
|
||||||
typedef struct BTScanOpaqueData
|
typedef struct BTScanOpaqueData
|
||||||
{
|
{
|
||||||
/* these fields are set by _bt_preprocess_keys(): */
|
/* these fields are set by _bt_preprocess_keys(): */
|
||||||
@ -532,6 +541,13 @@ typedef struct BTScanOpaqueData
|
|||||||
int numberOfKeys; /* number of preprocessed scan keys */
|
int numberOfKeys; /* number of preprocessed scan keys */
|
||||||
ScanKey keyData; /* array of preprocessed scan keys */
|
ScanKey keyData; /* array of preprocessed scan keys */
|
||||||
|
|
||||||
|
/* workspace for SK_SEARCHARRAY support */
|
||||||
|
ScanKey arrayKeyData; /* modified copy of scan->keyData */
|
||||||
|
int numArrayKeys; /* number of equality-type array keys (-1 if
|
||||||
|
* there are any unsatisfiable array keys) */
|
||||||
|
BTArrayKeyInfo *arrayKeys; /* info about each equality-type array key */
|
||||||
|
MemoryContext arrayContext; /* scan-lifespan context for array data */
|
||||||
|
|
||||||
/* info about killed items if any (killedItems is NULL if never used) */
|
/* info about killed items if any (killedItems is NULL if never used) */
|
||||||
int *killedItems; /* currPos.items indexes of killed items */
|
int *killedItems; /* currPos.items indexes of killed items */
|
||||||
int numKilled; /* number of currently stored items */
|
int numKilled; /* number of currently stored items */
|
||||||
@ -639,6 +655,9 @@ extern ScanKey _bt_mkscankey(Relation rel, IndexTuple itup);
|
|||||||
extern ScanKey _bt_mkscankey_nodata(Relation rel);
|
extern ScanKey _bt_mkscankey_nodata(Relation rel);
|
||||||
extern void _bt_freeskey(ScanKey skey);
|
extern void _bt_freeskey(ScanKey skey);
|
||||||
extern void _bt_freestack(BTStack stack);
|
extern void _bt_freestack(BTStack stack);
|
||||||
|
extern void _bt_preprocess_array_keys(IndexScanDesc scan);
|
||||||
|
extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir);
|
||||||
|
extern bool _bt_advance_array_keys(IndexScanDesc scan, ScanDirection dir);
|
||||||
extern void _bt_preprocess_keys(IndexScanDesc scan);
|
extern void _bt_preprocess_keys(IndexScanDesc scan);
|
||||||
extern IndexTuple _bt_checkkeys(IndexScanDesc scan,
|
extern IndexTuple _bt_checkkeys(IndexScanDesc scan,
|
||||||
Page page, OffsetNumber offnum,
|
Page page, OffsetNumber offnum,
|
||||||
|
@ -55,18 +55,27 @@ typedef uint16 StrategyNumber;
|
|||||||
* If the operator is collation-sensitive, sk_collation must be set
|
* If the operator is collation-sensitive, sk_collation must be set
|
||||||
* correctly as well.
|
* correctly as well.
|
||||||
*
|
*
|
||||||
|
* A ScanKey can also represent a ScalarArrayOpExpr, that is a condition
|
||||||
|
* "column op ANY(ARRAY[...])". This is signaled by the SK_SEARCHARRAY
|
||||||
|
* flag bit. The sk_argument is not a value of the operator's right-hand
|
||||||
|
* argument type, but rather an array of such values, and the per-element
|
||||||
|
* comparisons are to be ORed together.
|
||||||
|
*
|
||||||
* A ScanKey can also represent a condition "column IS NULL" or "column
|
* A ScanKey can also represent a condition "column IS NULL" or "column
|
||||||
* IS NOT NULL"; these cases are signaled by the SK_SEARCHNULL and
|
* IS NOT NULL"; these cases are signaled by the SK_SEARCHNULL and
|
||||||
* SK_SEARCHNOTNULL flag bits respectively. The argument is always NULL,
|
* SK_SEARCHNOTNULL flag bits respectively. The argument is always NULL,
|
||||||
* and the sk_strategy, sk_subtype, sk_collation, and sk_func fields are
|
* and the sk_strategy, sk_subtype, sk_collation, and sk_func fields are
|
||||||
* not used (unless set by the index AM). Currently, SK_SEARCHNULL and
|
* not used (unless set by the index AM).
|
||||||
* SK_SEARCHNOTNULL are supported only for index scans, not heap scans;
|
*
|
||||||
* and not all index AMs support them.
|
* SK_SEARCHARRAY, SK_SEARCHNULL and SK_SEARCHNOTNULL are supported only
|
||||||
|
* for index scans, not heap scans; and not all index AMs support them,
|
||||||
|
* only those that set amsearcharray or amsearchnulls respectively.
|
||||||
*
|
*
|
||||||
* A ScanKey can also represent an ordering operator invocation, that is
|
* A ScanKey can also represent an ordering operator invocation, that is
|
||||||
* an ordering requirement "ORDER BY indexedcol op constant". This looks
|
* an ordering requirement "ORDER BY indexedcol op constant". This looks
|
||||||
* the same as a comparison operator, except that the operator doesn't
|
* the same as a comparison operator, except that the operator doesn't
|
||||||
* (usually) yield boolean. We mark such ScanKeys with SK_ORDER_BY.
|
* (usually) yield boolean. We mark such ScanKeys with SK_ORDER_BY.
|
||||||
|
* SK_SEARCHARRAY, SK_SEARCHNULL, SK_SEARCHNOTNULL cannot be used here.
|
||||||
*
|
*
|
||||||
* Note: in some places, ScanKeys are used as a convenient representation
|
* Note: in some places, ScanKeys are used as a convenient representation
|
||||||
* for the invocation of an access method support procedure. In this case
|
* for the invocation of an access method support procedure. In this case
|
||||||
@ -114,6 +123,7 @@ typedef ScanKeyData *ScanKey;
|
|||||||
* opclass, NOT the operator's implementation function.
|
* opclass, NOT the operator's implementation function.
|
||||||
* sk_strategy must be the same in all elements of the subsidiary array,
|
* sk_strategy must be the same in all elements of the subsidiary array,
|
||||||
* that is, the same as in the header entry.
|
* that is, the same as in the header entry.
|
||||||
|
* SK_SEARCHARRAY, SK_SEARCHNULL, SK_SEARCHNOTNULL cannot be used here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -128,10 +138,11 @@ typedef ScanKeyData *ScanKey;
|
|||||||
#define SK_ROW_HEADER 0x0004 /* row comparison header (see above) */
|
#define SK_ROW_HEADER 0x0004 /* row comparison header (see above) */
|
||||||
#define SK_ROW_MEMBER 0x0008 /* row comparison member (see above) */
|
#define SK_ROW_MEMBER 0x0008 /* row comparison member (see above) */
|
||||||
#define SK_ROW_END 0x0010 /* last row comparison member */
|
#define SK_ROW_END 0x0010 /* last row comparison member */
|
||||||
#define SK_SEARCHNULL 0x0020 /* scankey represents "col IS NULL" */
|
#define SK_SEARCHARRAY 0x0020 /* scankey represents ScalarArrayOp */
|
||||||
#define SK_SEARCHNOTNULL 0x0040 /* scankey represents "col IS NOT
|
#define SK_SEARCHNULL 0x0040 /* scankey represents "col IS NULL" */
|
||||||
|
#define SK_SEARCHNOTNULL 0x0080 /* scankey represents "col IS NOT
|
||||||
* NULL" */
|
* NULL" */
|
||||||
#define SK_ORDER_BY 0x0080 /* scankey is for ORDER BY op */
|
#define SK_ORDER_BY 0x0100 /* scankey is for ORDER BY op */
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -53,6 +53,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* yyyymmddN */
|
/* yyyymmddN */
|
||||||
#define CATALOG_VERSION_NO 201110141
|
#define CATALOG_VERSION_NO 201110161
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -47,6 +47,7 @@ CATALOG(pg_am,2601)
|
|||||||
bool amcanmulticol; /* does AM support multi-column indexes? */
|
bool amcanmulticol; /* does AM support multi-column indexes? */
|
||||||
bool amcanreturn; /* can AM return IndexTuples? */
|
bool amcanreturn; /* can AM return IndexTuples? */
|
||||||
bool amoptionalkey; /* can query omit key for the first column? */
|
bool amoptionalkey; /* can query omit key for the first column? */
|
||||||
|
bool amsearcharray; /* can AM handle ScalarArrayOpExpr quals? */
|
||||||
bool amsearchnulls; /* can AM search for NULL/NOT NULL entries? */
|
bool amsearchnulls; /* can AM search for NULL/NOT NULL entries? */
|
||||||
bool amstorage; /* can storage type differ from column type? */
|
bool amstorage; /* can storage type differ from column type? */
|
||||||
bool amclusterable; /* does AM support cluster command? */
|
bool amclusterable; /* does AM support cluster command? */
|
||||||
@ -79,7 +80,7 @@ typedef FormData_pg_am *Form_pg_am;
|
|||||||
* compiler constants for pg_am
|
* compiler constants for pg_am
|
||||||
* ----------------
|
* ----------------
|
||||||
*/
|
*/
|
||||||
#define Natts_pg_am 29
|
#define Natts_pg_am 30
|
||||||
#define Anum_pg_am_amname 1
|
#define Anum_pg_am_amname 1
|
||||||
#define Anum_pg_am_amstrategies 2
|
#define Anum_pg_am_amstrategies 2
|
||||||
#define Anum_pg_am_amsupport 3
|
#define Anum_pg_am_amsupport 3
|
||||||
@ -90,41 +91,42 @@ typedef FormData_pg_am *Form_pg_am;
|
|||||||
#define Anum_pg_am_amcanmulticol 8
|
#define Anum_pg_am_amcanmulticol 8
|
||||||
#define Anum_pg_am_amcanreturn 9
|
#define Anum_pg_am_amcanreturn 9
|
||||||
#define Anum_pg_am_amoptionalkey 10
|
#define Anum_pg_am_amoptionalkey 10
|
||||||
#define Anum_pg_am_amsearchnulls 11
|
#define Anum_pg_am_amsearcharray 11
|
||||||
#define Anum_pg_am_amstorage 12
|
#define Anum_pg_am_amsearchnulls 12
|
||||||
#define Anum_pg_am_amclusterable 13
|
#define Anum_pg_am_amstorage 13
|
||||||
#define Anum_pg_am_ampredlocks 14
|
#define Anum_pg_am_amclusterable 14
|
||||||
#define Anum_pg_am_amkeytype 15
|
#define Anum_pg_am_ampredlocks 15
|
||||||
#define Anum_pg_am_aminsert 16
|
#define Anum_pg_am_amkeytype 16
|
||||||
#define Anum_pg_am_ambeginscan 17
|
#define Anum_pg_am_aminsert 17
|
||||||
#define Anum_pg_am_amgettuple 18
|
#define Anum_pg_am_ambeginscan 18
|
||||||
#define Anum_pg_am_amgetbitmap 19
|
#define Anum_pg_am_amgettuple 19
|
||||||
#define Anum_pg_am_amrescan 20
|
#define Anum_pg_am_amgetbitmap 20
|
||||||
#define Anum_pg_am_amendscan 21
|
#define Anum_pg_am_amrescan 21
|
||||||
#define Anum_pg_am_ammarkpos 22
|
#define Anum_pg_am_amendscan 22
|
||||||
#define Anum_pg_am_amrestrpos 23
|
#define Anum_pg_am_ammarkpos 23
|
||||||
#define Anum_pg_am_ambuild 24
|
#define Anum_pg_am_amrestrpos 24
|
||||||
#define Anum_pg_am_ambuildempty 25
|
#define Anum_pg_am_ambuild 25
|
||||||
#define Anum_pg_am_ambulkdelete 26
|
#define Anum_pg_am_ambuildempty 26
|
||||||
#define Anum_pg_am_amvacuumcleanup 27
|
#define Anum_pg_am_ambulkdelete 27
|
||||||
#define Anum_pg_am_amcostestimate 28
|
#define Anum_pg_am_amvacuumcleanup 28
|
||||||
#define Anum_pg_am_amoptions 29
|
#define Anum_pg_am_amcostestimate 29
|
||||||
|
#define Anum_pg_am_amoptions 30
|
||||||
|
|
||||||
/* ----------------
|
/* ----------------
|
||||||
* initial contents of pg_am
|
* initial contents of pg_am
|
||||||
* ----------------
|
* ----------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
DATA(insert OID = 403 ( btree 5 1 t f t t t t t t f t t 0 btinsert btbeginscan btgettuple btgetbitmap btrescan btendscan btmarkpos btrestrpos btbuild btbuildempty btbulkdelete btvacuumcleanup btcostestimate btoptions ));
|
DATA(insert OID = 403 ( btree 5 1 t f t t t t t t t f t t 0 btinsert btbeginscan btgettuple btgetbitmap btrescan btendscan btmarkpos btrestrpos btbuild btbuildempty btbulkdelete btvacuumcleanup btcostestimate btoptions ));
|
||||||
DESCR("b-tree index access method");
|
DESCR("b-tree index access method");
|
||||||
#define BTREE_AM_OID 403
|
#define BTREE_AM_OID 403
|
||||||
DATA(insert OID = 405 ( hash 1 1 f f t f f f f f f f f 23 hashinsert hashbeginscan hashgettuple hashgetbitmap hashrescan hashendscan hashmarkpos hashrestrpos hashbuild hashbuildempty hashbulkdelete hashvacuumcleanup hashcostestimate hashoptions ));
|
DATA(insert OID = 405 ( hash 1 1 f f t f f f f f f f f f 23 hashinsert hashbeginscan hashgettuple hashgetbitmap hashrescan hashendscan hashmarkpos hashrestrpos hashbuild hashbuildempty hashbulkdelete hashvacuumcleanup hashcostestimate hashoptions ));
|
||||||
DESCR("hash index access method");
|
DESCR("hash index access method");
|
||||||
#define HASH_AM_OID 405
|
#define HASH_AM_OID 405
|
||||||
DATA(insert OID = 783 ( gist 0 8 f t f f t f t t t t f 0 gistinsert gistbeginscan gistgettuple gistgetbitmap gistrescan gistendscan gistmarkpos gistrestrpos gistbuild gistbuildempty gistbulkdelete gistvacuumcleanup gistcostestimate gistoptions ));
|
DATA(insert OID = 783 ( gist 0 8 f t f f t f t f t t t f 0 gistinsert gistbeginscan gistgettuple gistgetbitmap gistrescan gistendscan gistmarkpos gistrestrpos gistbuild gistbuildempty gistbulkdelete gistvacuumcleanup gistcostestimate gistoptions ));
|
||||||
DESCR("GiST index access method");
|
DESCR("GiST index access method");
|
||||||
#define GIST_AM_OID 783
|
#define GIST_AM_OID 783
|
||||||
DATA(insert OID = 2742 ( gin 0 5 f f f f t f t f t f f 0 gininsert ginbeginscan - gingetbitmap ginrescan ginendscan ginmarkpos ginrestrpos ginbuild ginbuildempty ginbulkdelete ginvacuumcleanup gincostestimate ginoptions ));
|
DATA(insert OID = 2742 ( gin 0 5 f f f f t f t f f t f f 0 gininsert ginbeginscan - gingetbitmap ginrescan ginendscan ginmarkpos ginrestrpos ginbuild ginbuildempty ginbulkdelete ginvacuumcleanup gincostestimate ginoptions ));
|
||||||
DESCR("GIN index access method");
|
DESCR("GIN index access method");
|
||||||
#define GIN_AM_OID 2742
|
#define GIN_AM_OID 2742
|
||||||
|
|
||||||
|
@ -490,8 +490,9 @@ typedef struct IndexOptInfo
|
|||||||
bool unique; /* true if a unique index */
|
bool unique; /* true if a unique index */
|
||||||
bool hypothetical; /* true if index doesn't really exist */
|
bool hypothetical; /* true if index doesn't really exist */
|
||||||
bool amcanorderbyop; /* does AM support order by operator result? */
|
bool amcanorderbyop; /* does AM support order by operator result? */
|
||||||
bool amcanreturn; /* does AM know how to return tuples? */
|
bool amcanreturn; /* can AM return IndexTuples? */
|
||||||
bool amoptionalkey; /* can query omit key for the first column? */
|
bool amoptionalkey; /* can query omit key for the first column? */
|
||||||
|
bool amsearcharray; /* can AM handle ScalarArrayOpExpr quals? */
|
||||||
bool amsearchnulls; /* can AM search for NULL/NOT NULL entries? */
|
bool amsearchnulls; /* can AM search for NULL/NOT NULL entries? */
|
||||||
bool amhasgettuple; /* does AM have amgettuple interface? */
|
bool amhasgettuple; /* does AM have amgettuple interface? */
|
||||||
bool amhasgetbitmap; /* does AM have amgetbitmap interface? */
|
bool amhasgetbitmap; /* does AM have amgetbitmap interface? */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user