Branch data Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * spgscan.c
4 : : * routines for scanning SP-GiST indexes
5 : : *
6 : : *
7 : : * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
8 : : * Portions Copyright (c) 1994, Regents of the University of California
9 : : *
10 : : * IDENTIFICATION
11 : : * src/backend/access/spgist/spgscan.c
12 : : *
13 : : *-------------------------------------------------------------------------
14 : : */
15 : :
16 : : #include "postgres.h"
17 : :
18 : : #include "access/genam.h"
19 : : #include "access/relscan.h"
20 : : #include "access/spgist_private.h"
21 : : #include "executor/instrument_node.h"
22 : : #include "miscadmin.h"
23 : : #include "pgstat.h"
24 : : #include "storage/bufmgr.h"
25 : : #include "utils/datum.h"
26 : : #include "utils/float.h"
27 : : #include "utils/lsyscache.h"
28 : : #include "utils/memutils.h"
29 : : #include "utils/rel.h"
30 : :
31 : : typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
32 : : Datum leafValue, bool isNull,
33 : : SpGistLeafTuple leafTuple, bool recheck,
34 : : bool recheckDistances, double *distances);
35 : :
36 : : /*
37 : : * Pairing heap comparison function for the SpGistSearchItem queue.
38 : : * KNN-searches currently only support NULLS LAST. So, preserve this logic
39 : : * here.
40 : : */
41 : : static int
42 : 2082729 : pairingheap_SpGistSearchItem_cmp(const pairingheap_node *a,
43 : : const pairingheap_node *b, void *arg)
44 : : {
45 : 2082729 : const SpGistSearchItem *sa = (const SpGistSearchItem *) a;
46 : 2082729 : const SpGistSearchItem *sb = (const SpGistSearchItem *) b;
47 : 2082729 : SpGistScanOpaque so = (SpGistScanOpaque) arg;
48 : 2082729 : int i;
49 : :
50 [ + + ]: 2082729 : if (sa->isNull)
51 : : {
52 [ + + ]: 97 : if (!sb->isNull)
53 : 90 : return -1;
54 : 7 : }
55 [ + + ]: 2082632 : else if (sb->isNull)
56 : : {
57 : 75 : return 1;
58 : : }
59 : : else
60 : : {
61 : : /* Order according to distance comparison */
62 [ + + ]: 2123224 : for (i = 0; i < so->numberOfNonNullOrderBys; i++)
63 : : {
64 [ + + + + : 2017719 : if (isnan(sa->distances[i]) && isnan(sb->distances[i]))
+ - # # #
# # # ]
65 : 0 : continue; /* NaN == NaN */
66 [ - + # # : 672573 : if (isnan(sa->distances[i]))
+ - ]
67 : 0 : return -1; /* NaN > number */
68 [ - + # # : 672573 : if (isnan(sb->distances[i]))
+ - ]
69 : 0 : return 1; /* number < NaN */
70 [ + + ]: 672573 : if (sa->distances[i] != sb->distances[i])
71 : 631906 : return (sa->distances[i] < sb->distances[i]) ? 1 : -1;
72 : 40667 : }
73 : : }
74 : :
75 : : /* Leaf items go before inner pages, to ensure a depth-first search */
76 [ + + + + ]: 105512 : if (sa->isLeaf && !sb->isLeaf)
77 : 730 : return 1;
78 [ + + + + ]: 104782 : if (!sa->isLeaf && sb->isLeaf)
79 : 824 : return -1;
80 : :
81 : 103958 : return 0;
82 : 737583 : }
83 : :
84 : : static void
85 : 77386 : spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
86 : : {
87 : : /* value is of type attType if isLeaf, else of type attLeafType */
88 : : /* (no, that is not backwards; yes, it's confusing) */
89 [ + + ]: 77386 : if (!(item->isLeaf ? so->state.attType.attbyval :
90 [ + + ]: 126441 : so->state.attLeafType.attbyval) &&
91 : 49055 : DatumGetPointer(item->value) != NULL)
92 : 49055 : pfree(DatumGetPointer(item->value));
93 : :
94 [ + + ]: 77386 : if (item->leafTuple)
95 : 10 : pfree(item->leafTuple);
96 : :
97 [ + + ]: 77386 : if (item->traversalValue)
98 : 7412 : pfree(item->traversalValue);
99 : :
100 : 77386 : pfree(item);
101 : 77386 : }
102 : :
103 : : /*
104 : : * Add SpGistSearchItem to queue
105 : : *
106 : : * Called in queue context
107 : : */
108 : : static void
109 : 77847 : spgAddSearchItemToQueue(SpGistScanOpaque so, SpGistSearchItem *item)
110 : : {
111 : 77847 : pairingheap_add(so->scanQueue, &item->phNode);
112 : 77847 : }
113 : :
114 : : static SpGistSearchItem *
115 : 77847 : spgAllocSearchItem(SpGistScanOpaque so, bool isnull, double *distances)
116 : : {
117 : : /* allocate distance array only for non-NULL items */
118 : 155694 : SpGistSearchItem *item =
119 [ + + ]: 77847 : palloc(SizeOfSpGistSearchItem(isnull ? 0 : so->numberOfNonNullOrderBys));
120 : :
121 : 77847 : item->isNull = isnull;
122 : :
123 [ + + + + ]: 77847 : if (!isnull && so->numberOfNonNullOrderBys > 0)
124 : 63006 : memcpy(item->distances, distances,
125 : : sizeof(item->distances[0]) * so->numberOfNonNullOrderBys);
126 : :
127 : 155694 : return item;
128 : 77847 : }
129 : :
130 : : static void
131 : 163 : spgAddStartItem(SpGistScanOpaque so, bool isnull)
132 : : {
133 : 326 : SpGistSearchItem *startEntry =
134 : 163 : spgAllocSearchItem(so, isnull, so->zeroDistances);
135 : :
136 : 326 : ItemPointerSet(&startEntry->heapPtr,
137 : 163 : isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO,
138 : : FirstOffsetNumber);
139 : 163 : startEntry->isLeaf = false;
140 : 163 : startEntry->level = 0;
141 : 163 : startEntry->value = (Datum) 0;
142 : 163 : startEntry->leafTuple = NULL;
143 : 163 : startEntry->traversalValue = NULL;
144 : 163 : startEntry->recheck = false;
145 : 163 : startEntry->recheckDistances = false;
146 : :
147 : 163 : spgAddSearchItemToQueue(so, startEntry);
148 : 163 : }
149 : :
150 : : /*
151 : : * Initialize queue to search the root page, resetting
152 : : * any previously active scan
153 : : */
154 : : static void
155 : 154 : resetSpGistScanOpaque(SpGistScanOpaque so)
156 : : {
157 : 154 : MemoryContext oldCtx;
158 : :
159 : 154 : MemoryContextReset(so->traversalCxt);
160 : :
161 : 154 : oldCtx = MemoryContextSwitchTo(so->traversalCxt);
162 : :
163 : : /* initialize queue only for distance-ordered scans */
164 : 154 : so->scanQueue = pairingheap_allocate(pairingheap_SpGistSearchItem_cmp, so);
165 : :
166 [ + + ]: 154 : if (so->searchNulls)
167 : : /* Add a work item to scan the null index entries */
168 : 11 : spgAddStartItem(so, true);
169 : :
170 [ + + ]: 154 : if (so->searchNonNulls)
171 : : /* Add a work item to scan the non-null index entries */
172 : 152 : spgAddStartItem(so, false);
173 : :
174 : 154 : MemoryContextSwitchTo(oldCtx);
175 : :
176 [ + + ]: 154 : if (so->numberOfOrderBys > 0)
177 : : {
178 : : /* Must pfree distances to avoid memory leak */
179 : 13 : int i;
180 : :
181 [ + + ]: 15 : for (i = 0; i < so->nPtrs; i++)
182 [ - + ]: 4 : if (so->distances[i])
183 : 2 : pfree(so->distances[i]);
184 : 13 : }
185 : :
186 [ + + ]: 154 : if (so->want_itup)
187 : : {
188 : : /* Must pfree reconstructed tuples to avoid memory leak */
189 : 4 : int i;
190 : :
191 [ + + ]: 15 : for (i = 0; i < so->nPtrs; i++)
192 : 11 : pfree(so->reconTups[i]);
193 : 4 : }
194 : 154 : so->iPtr = so->nPtrs = 0;
195 : 154 : }
196 : :
197 : : /*
198 : : * Prepare scan keys in SpGistScanOpaque from caller-given scan keys
199 : : *
200 : : * Sets searchNulls, searchNonNulls, numberOfKeys, keyData fields of *so.
201 : : *
202 : : * The point here is to eliminate null-related considerations from what the
203 : : * opclass consistent functions need to deal with. We assume all SPGiST-
204 : : * indexable operators are strict, so any null RHS value makes the scan
205 : : * condition unsatisfiable. We also pull out any IS NULL/IS NOT NULL
206 : : * conditions; their effect is reflected into searchNulls/searchNonNulls.
207 : : */
208 : : static void
209 : 154 : spgPrepareScanKeys(IndexScanDesc scan)
210 : : {
211 : 154 : SpGistScanOpaque so = (SpGistScanOpaque) scan->opaque;
212 : 154 : bool qual_ok;
213 : 154 : bool haveIsNull;
214 : 154 : bool haveNotNull;
215 : 154 : int nkeys;
216 : 154 : int i;
217 : :
218 : 154 : so->numberOfOrderBys = scan->numberOfOrderBys;
219 : 154 : so->orderByData = scan->orderByData;
220 : :
221 [ + + ]: 154 : if (so->numberOfOrderBys <= 0)
222 : 141 : so->numberOfNonNullOrderBys = 0;
223 : : else
224 : : {
225 : 13 : int j = 0;
226 : :
227 : : /*
228 : : * Remove all NULL keys, but remember their offsets in the original
229 : : * array.
230 : : */
231 [ + + ]: 29 : for (i = 0; i < scan->numberOfOrderBys; i++)
232 : : {
233 : 16 : ScanKey skey = &so->orderByData[i];
234 : :
235 [ + + ]: 16 : if (skey->sk_flags & SK_ISNULL)
236 : 1 : so->nonNullOrderByOffsets[i] = -1;
237 : : else
238 : : {
239 [ + + ]: 15 : if (i != j)
240 : 1 : so->orderByData[j] = *skey;
241 : :
242 : 15 : so->nonNullOrderByOffsets[i] = j++;
243 : : }
244 : 16 : }
245 : :
246 : 13 : so->numberOfNonNullOrderBys = j;
247 : 13 : }
248 : :
249 [ + + ]: 154 : if (scan->numberOfKeys <= 0)
250 : : {
251 : : /* If no quals, whole-index scan is required */
252 : 9 : so->searchNulls = true;
253 : 9 : so->searchNonNulls = true;
254 : 9 : so->numberOfKeys = 0;
255 : 9 : return;
256 : : }
257 : :
258 : : /* Examine the given quals */
259 : 145 : qual_ok = true;
260 : 145 : haveIsNull = haveNotNull = false;
261 : 145 : nkeys = 0;
262 [ + + ]: 290 : for (i = 0; i < scan->numberOfKeys; i++)
263 : : {
264 : 145 : ScanKey skey = &scan->keyData[i];
265 : :
266 [ + + ]: 145 : if (skey->sk_flags & SK_SEARCHNULL)
267 : 2 : haveIsNull = true;
268 [ + + ]: 143 : else if (skey->sk_flags & SK_SEARCHNOTNULL)
269 : 4 : haveNotNull = true;
270 [ - + ]: 139 : else if (skey->sk_flags & SK_ISNULL)
271 : : {
272 : : /* ordinary qual with null argument - unsatisfiable */
273 : 0 : qual_ok = false;
274 : 0 : break;
275 : : }
276 : : else
277 : : {
278 : : /* ordinary qual, propagate into so->keyData */
279 : 139 : so->keyData[nkeys++] = *skey;
280 : : /* this effectively creates a not-null requirement */
281 : 139 : haveNotNull = true;
282 : : }
283 [ - + ]: 145 : }
284 : :
285 : : /* IS NULL in combination with something else is unsatisfiable */
286 [ + + + - ]: 145 : if (haveIsNull && haveNotNull)
287 : 0 : qual_ok = false;
288 : :
289 : : /* Emit results */
290 [ + - ]: 145 : if (qual_ok)
291 : : {
292 : 145 : so->searchNulls = haveIsNull;
293 : 145 : so->searchNonNulls = haveNotNull;
294 : 145 : so->numberOfKeys = nkeys;
295 : 145 : }
296 : : else
297 : : {
298 : 0 : so->searchNulls = false;
299 : 0 : so->searchNonNulls = false;
300 : 0 : so->numberOfKeys = 0;
301 : : }
302 : 154 : }
303 : :
304 : : IndexScanDesc
305 : 150 : spgbeginscan(Relation rel, int keysz, int orderbysz)
306 : : {
307 : 150 : IndexScanDesc scan;
308 : 150 : SpGistScanOpaque so;
309 : 150 : int i;
310 : :
311 : 150 : scan = RelationGetIndexScan(rel, keysz, orderbysz);
312 : :
313 : 150 : so = palloc0_object(SpGistScanOpaqueData);
314 [ + + ]: 150 : if (keysz > 0)
315 : 143 : so->keyData = palloc_array(ScanKeyData, keysz);
316 : : else
317 : 7 : so->keyData = NULL;
318 : 150 : initSpGistState(&so->state, scan->indexRelation);
319 : :
320 : 150 : so->tempCxt = AllocSetContextCreate(CurrentMemoryContext,
321 : : "SP-GiST search temporary context",
322 : : ALLOCSET_DEFAULT_SIZES);
323 : 150 : so->traversalCxt = AllocSetContextCreate(CurrentMemoryContext,
324 : : "SP-GiST traversal-value context",
325 : : ALLOCSET_DEFAULT_SIZES);
326 : :
327 : : /*
328 : : * Set up reconTupDesc and xs_hitupdesc in case it's an index-only scan,
329 : : * making sure that the key column is shown as being of type attType.
330 : : * (It's rather annoying to do this work when it might be wasted, but for
331 : : * most opclasses we can re-use the index reldesc instead of making one.)
332 : : */
333 : 150 : so->reconTupDesc = scan->xs_hitupdesc =
334 : 150 : getSpGistTupleDesc(rel, &so->state.attType);
335 : :
336 : : /* Allocate various arrays needed for order-by scans */
337 [ + + ]: 150 : if (scan->numberOfOrderBys > 0)
338 : : {
339 : : /* This will be filled in spgrescan, but allocate the space here */
340 : 11 : so->orderByTypes = palloc_array(Oid, scan->numberOfOrderBys);
341 : 11 : so->nonNullOrderByOffsets = palloc_array(int, scan->numberOfOrderBys);
342 : :
343 : : /* These arrays have constant contents, so we can fill them now */
344 : 11 : so->zeroDistances = palloc_array(double, scan->numberOfOrderBys);
345 : 11 : so->infDistances = palloc_array(double, scan->numberOfOrderBys);
346 : :
347 [ + + ]: 23 : for (i = 0; i < scan->numberOfOrderBys; i++)
348 : : {
349 : 12 : so->zeroDistances[i] = 0.0;
350 : 12 : so->infDistances[i] = get_float8_infinity();
351 : 12 : }
352 : :
353 : 11 : scan->xs_orderbyvals = palloc0_array(Datum, scan->numberOfOrderBys);
354 : 11 : scan->xs_orderbynulls = palloc_array(bool, scan->numberOfOrderBys);
355 : 11 : memset(scan->xs_orderbynulls, true,
356 : : sizeof(bool) * scan->numberOfOrderBys);
357 : 11 : }
358 : :
359 : 300 : fmgr_info_copy(&so->innerConsistentFn,
360 : 150 : index_getprocinfo(rel, 1, SPGIST_INNER_CONSISTENT_PROC),
361 : 150 : CurrentMemoryContext);
362 : :
363 : 300 : fmgr_info_copy(&so->leafConsistentFn,
364 : 150 : index_getprocinfo(rel, 1, SPGIST_LEAF_CONSISTENT_PROC),
365 : 150 : CurrentMemoryContext);
366 : :
367 : 150 : so->indexCollation = rel->rd_indcollation[0];
368 : :
369 : 150 : scan->opaque = so;
370 : :
371 : 300 : return scan;
372 : 150 : }
373 : :
374 : : void
375 : 154 : spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys,
376 : : ScanKey orderbys, int norderbys)
377 : : {
378 : 154 : SpGistScanOpaque so = (SpGistScanOpaque) scan->opaque;
379 : :
380 : : /* copy scankeys into local storage */
381 [ + - + + ]: 154 : if (scankey && scan->numberOfKeys > 0)
382 : 145 : memcpy(scan->keyData, scankey, scan->numberOfKeys * sizeof(ScanKeyData));
383 : :
384 : : /* initialize order-by data if needed */
385 [ + + + + ]: 154 : if (orderbys && scan->numberOfOrderBys > 0)
386 : : {
387 : 13 : int i;
388 : :
389 : 13 : memcpy(scan->orderByData, orderbys, scan->numberOfOrderBys * sizeof(ScanKeyData));
390 : :
391 [ + + ]: 29 : for (i = 0; i < scan->numberOfOrderBys; i++)
392 : : {
393 : 16 : ScanKey skey = &scan->orderByData[i];
394 : :
395 : : /*
396 : : * Look up the datatype returned by the original ordering
397 : : * operator. SP-GiST always uses a float8 for the distance
398 : : * function, but the ordering operator could be anything else.
399 : : *
400 : : * XXX: The distance function is only allowed to be lossy if the
401 : : * ordering operator's result type is float4 or float8. Otherwise
402 : : * we don't know how to return the distance to the executor. But
403 : : * we cannot check that here, as we won't know if the distance
404 : : * function is lossy until it returns *recheck = true for the
405 : : * first time.
406 : : */
407 : 16 : so->orderByTypes[i] = get_func_rettype(skey->sk_func.fn_oid);
408 : 16 : }
409 : 13 : }
410 : :
411 : : /* preprocess scankeys, set up the representation in *so */
412 : 154 : spgPrepareScanKeys(scan);
413 : :
414 : : /* set up starting queue entries */
415 : 154 : resetSpGistScanOpaque(so);
416 : :
417 : : /* count an indexscan for stats */
418 [ + - + - : 154 : pgstat_count_index_scan(scan->indexRelation);
# # ]
419 [ + - ]: 154 : if (scan->instrument)
420 : 154 : scan->instrument->nsearches++;
421 : 154 : }
422 : :
423 : : void
424 : 150 : spgendscan(IndexScanDesc scan)
425 : : {
426 : 150 : SpGistScanOpaque so = (SpGistScanOpaque) scan->opaque;
427 : :
428 : 150 : MemoryContextDelete(so->tempCxt);
429 : 150 : MemoryContextDelete(so->traversalCxt);
430 : :
431 [ + + ]: 150 : if (so->keyData)
432 : 143 : pfree(so->keyData);
433 : :
434 [ + - + + ]: 150 : if (so->state.leafTupDesc &&
435 : 150 : so->state.leafTupDesc != RelationGetDescr(so->state.index))
436 : 1 : FreeTupleDesc(so->state.leafTupDesc);
437 : :
438 [ - + ]: 150 : if (so->state.deadTupleStorage)
439 : 150 : pfree(so->state.deadTupleStorage);
440 : :
441 [ + + ]: 150 : if (scan->numberOfOrderBys > 0)
442 : : {
443 : 11 : pfree(so->orderByTypes);
444 : 11 : pfree(so->nonNullOrderByOffsets);
445 : 11 : pfree(so->zeroDistances);
446 : 11 : pfree(so->infDistances);
447 : 11 : pfree(scan->xs_orderbyvals);
448 : 11 : pfree(scan->xs_orderbynulls);
449 : 11 : }
450 : :
451 : 150 : pfree(so);
452 : 150 : }
453 : :
454 : : /*
455 : : * Leaf SpGistSearchItem constructor, called in queue context
456 : : */
457 : : static SpGistSearchItem *
458 : 60985 : spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
459 : : Datum leafValue, bool recheck, bool recheckDistances,
460 : : bool isnull, double *distances)
461 : : {
462 : 60985 : SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
463 : :
464 : 60985 : item->level = level;
465 : 60985 : item->heapPtr = leafTuple->heapPtr;
466 : :
467 : : /*
468 : : * If we need the reconstructed value, copy it to queue cxt out of tmp
469 : : * cxt. Caution: the leaf_consistent method may not have supplied a value
470 : : * if we didn't ask it to, and mildly-broken methods might supply one of
471 : : * the wrong type. The correct leafValue type is attType not leafType.
472 : : */
473 [ + + ]: 60985 : if (so->want_itup)
474 : : {
475 [ + + ]: 46559 : item->value = isnull ? (Datum) 0 :
476 : 93106 : datumCopy(leafValue, so->state.attType.attbyval,
477 : 46553 : so->state.attType.attlen);
478 : :
479 : : /*
480 : : * If we're going to need to reconstruct INCLUDE attributes, store the
481 : : * whole leaf tuple so we can get the INCLUDE attributes out of it.
482 : : */
483 [ + + ]: 46559 : if (so->state.leafTupDesc->natts > 1)
484 : : {
485 : 34 : item->leafTuple = palloc(leafTuple->size);
486 : 34 : memcpy(item->leafTuple, leafTuple, leafTuple->size);
487 : 34 : }
488 : : else
489 : 46525 : item->leafTuple = NULL;
490 : 46559 : }
491 : : else
492 : : {
493 : 14426 : item->value = (Datum) 0;
494 : 14426 : item->leafTuple = NULL;
495 : : }
496 : 60985 : item->traversalValue = NULL;
497 : 60985 : item->isLeaf = true;
498 : 60985 : item->recheck = recheck;
499 : 60985 : item->recheckDistances = recheckDistances;
500 : :
501 : 121970 : return item;
502 : 60985 : }
503 : :
504 : : /*
505 : : * Test whether a leaf tuple satisfies all the scan keys
506 : : *
507 : : * *reportedSome is set to true if:
508 : : * the scan is not ordered AND the item satisfies the scankeys
509 : : */
510 : : static bool
511 : 440364 : spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
512 : : SpGistLeafTuple leafTuple, bool isnull,
513 : : bool *reportedSome, storeRes_func storeRes)
514 : : {
515 : 440364 : Datum leafValue;
516 : 440364 : double *distances;
517 : 440364 : bool result;
518 : 440364 : bool recheck;
519 : 440364 : bool recheckDistances;
520 : :
521 [ + + ]: 440364 : if (isnull)
522 : : {
523 : : /* Should not have arrived on a nulls page unless nulls are wanted */
524 [ + - ]: 20 : Assert(so->searchNulls);
525 : 20 : leafValue = (Datum) 0;
526 : 20 : distances = NULL;
527 : 20 : recheck = false;
528 : 20 : recheckDistances = false;
529 : 20 : result = true;
530 : 20 : }
531 : : else
532 : : {
533 : 440344 : spgLeafConsistentIn in;
534 : 440344 : spgLeafConsistentOut out;
535 : :
536 : : /* use temp context for calling leaf_consistent */
537 : 440344 : MemoryContext oldCxt = MemoryContextSwitchTo(so->tempCxt);
538 : :
539 : 440344 : in.scankeys = so->keyData;
540 : 440344 : in.nkeys = so->numberOfKeys;
541 : 440344 : in.orderbys = so->orderByData;
542 : 440344 : in.norderbys = so->numberOfNonNullOrderBys;
543 [ + - ]: 440344 : Assert(!item->isLeaf); /* else reconstructedValue would be wrong type */
544 : 440344 : in.reconstructedValue = item->value;
545 : 440344 : in.traversalValue = item->traversalValue;
546 : 440344 : in.level = item->level;
547 : 440344 : in.returnData = so->want_itup;
548 : 440344 : in.leafDatum = SGLTDATUM(leafTuple, &so->state);
549 : :
550 : 440344 : out.leafValue = (Datum) 0;
551 : 440344 : out.recheck = false;
552 : 440344 : out.distances = NULL;
553 : 440344 : out.recheckDistances = false;
554 : :
555 : 880688 : result = DatumGetBool(FunctionCall2Coll(&so->leafConsistentFn,
556 : 440344 : so->indexCollation,
557 : 440344 : PointerGetDatum(&in),
558 : 440344 : PointerGetDatum(&out)));
559 : 440344 : recheck = out.recheck;
560 : 440344 : recheckDistances = out.recheckDistances;
561 : 440344 : leafValue = out.leafValue;
562 : 440344 : distances = out.distances;
563 : :
564 : 440344 : MemoryContextSwitchTo(oldCxt);
565 : 440344 : }
566 : :
567 [ + + ]: 440364 : if (result)
568 : : {
569 : : /* item passes the scankeys */
570 [ + + ]: 343532 : if (so->numberOfNonNullOrderBys > 0)
571 : : {
572 : : /* the scan is ordered -> add the item to the queue */
573 : 60985 : MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
574 : 121970 : SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
575 : 60985 : leafTuple,
576 : 60985 : leafValue,
577 : 60985 : recheck,
578 : 60985 : recheckDistances,
579 : 60985 : isnull,
580 : 60985 : distances);
581 : :
582 : 60985 : spgAddSearchItemToQueue(so, heapItem);
583 : :
584 : 60985 : MemoryContextSwitchTo(oldCxt);
585 : 60985 : }
586 : : else
587 : : {
588 : : /* non-ordered scan, so report the item right away */
589 [ + - ]: 282547 : Assert(!recheckDistances);
590 : 565094 : storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
591 : 282547 : leafTuple, recheck, false, NULL);
592 : 282547 : *reportedSome = true;
593 : : }
594 : 343532 : }
595 : :
596 : 880728 : return result;
597 : 440364 : }
598 : :
599 : : /* A bundle initializer for inner_consistent methods */
600 : : static void
601 : 4203 : spgInitInnerConsistentIn(spgInnerConsistentIn *in,
602 : : SpGistScanOpaque so,
603 : : SpGistSearchItem *item,
604 : : SpGistInnerTuple innerTuple)
605 : : {
606 : 4203 : in->scankeys = so->keyData;
607 : 4203 : in->orderbys = so->orderByData;
608 : 4203 : in->nkeys = so->numberOfKeys;
609 : 4203 : in->norderbys = so->numberOfNonNullOrderBys;
610 [ + - ]: 4203 : Assert(!item->isLeaf); /* else reconstructedValue would be wrong type */
611 : 4203 : in->reconstructedValue = item->value;
612 : 4203 : in->traversalMemoryContext = so->traversalCxt;
613 : 4203 : in->traversalValue = item->traversalValue;
614 : 4203 : in->level = item->level;
615 : 4203 : in->returnData = so->want_itup;
616 : 4203 : in->allTheSame = innerTuple->allTheSame;
617 : 4203 : in->hasPrefix = (innerTuple->prefixSize > 0);
618 [ + + + + ]: 4203 : in->prefixDatum = SGITDATUM(innerTuple, &so->state);
619 : 4203 : in->nNodes = innerTuple->nNodes;
620 : 4203 : in->nodeLabels = spgExtractNodeLabels(&so->state, innerTuple);
621 : 4203 : }
622 : :
623 : : static SpGistSearchItem *
624 : 16699 : spgMakeInnerItem(SpGistScanOpaque so,
625 : : SpGistSearchItem *parentItem,
626 : : SpGistNodeTuple tuple,
627 : : spgInnerConsistentOut *out, int i, bool isnull,
628 : : double *distances)
629 : : {
630 : 16699 : SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
631 : :
632 : 16699 : item->heapPtr = tuple->t_tid;
633 [ + + ]: 16699 : item->level = out->levelAdds ? parentItem->level + out->levelAdds[i]
634 : 8833 : : parentItem->level;
635 : :
636 : : /* Must copy value out of temp context */
637 : : /* (recall that reconstructed values are of type leafType) */
638 [ + + ]: 16699 : item->value = out->reconstructedValues
639 : 5856 : ? datumCopy(out->reconstructedValues[i],
640 : 2928 : so->state.attLeafType.attbyval,
641 : 2928 : so->state.attLeafType.attlen)
642 : : : (Datum) 0;
643 : :
644 : 16699 : item->leafTuple = NULL;
645 : :
646 : : /*
647 : : * Elements of out.traversalValues should be allocated in
648 : : * in.traversalMemoryContext, which is actually a long lived context of
649 : : * index scan.
650 : : */
651 : 16699 : item->traversalValue =
652 [ + + ]: 16699 : out->traversalValues ? out->traversalValues[i] : NULL;
653 : :
654 : 16699 : item->isLeaf = false;
655 : 16699 : item->recheck = false;
656 : 16699 : item->recheckDistances = false;
657 : :
658 : 33398 : return item;
659 : 16699 : }
660 : :
661 : : static void
662 : 4203 : spgInnerTest(SpGistScanOpaque so, SpGistSearchItem *item,
663 : : SpGistInnerTuple innerTuple, bool isnull)
664 : : {
665 : 4203 : MemoryContext oldCxt = MemoryContextSwitchTo(so->tempCxt);
666 : 4203 : spgInnerConsistentOut out;
667 : 4203 : int nNodes = innerTuple->nNodes;
668 : 4203 : int i;
669 : :
670 : 4203 : memset(&out, 0, sizeof(out));
671 : :
672 [ - + ]: 4203 : if (!isnull)
673 : : {
674 : 4203 : spgInnerConsistentIn in;
675 : :
676 : 4203 : spgInitInnerConsistentIn(&in, so, item, innerTuple);
677 : :
678 : : /* use user-defined inner consistent method */
679 : 8406 : FunctionCall2Coll(&so->innerConsistentFn,
680 : 4203 : so->indexCollation,
681 : 4203 : PointerGetDatum(&in),
682 : 4203 : PointerGetDatum(&out));
683 : 4203 : }
684 : : else
685 : : {
686 : : /* force all children to be visited */
687 : 0 : out.nNodes = nNodes;
688 : 0 : out.nodeNumbers = palloc_array(int, nNodes);
689 [ # # ]: 0 : for (i = 0; i < nNodes; i++)
690 : 0 : out.nodeNumbers[i] = i;
691 : : }
692 : :
693 : : /* If allTheSame, they should all or none of them match */
694 [ + + + - : 4203 : if (innerTuple->allTheSame && out.nNodes != 0 && out.nNodes != nNodes)
+ - ]
695 [ # # # # ]: 0 : elog(ERROR, "inconsistent inner_consistent results for allTheSame inner tuple");
696 : :
697 [ - + ]: 4203 : if (out.nNodes)
698 : : {
699 : : /* collect node pointers */
700 : 4203 : SpGistNodeTuple node;
701 : 4203 : SpGistNodeTuple *nodes = palloc_array(SpGistNodeTuple, nNodes);
702 : :
703 [ + + ]: 38780 : SGITITERATE(innerTuple, i, node)
704 : : {
705 : 34577 : nodes[i] = node;
706 : 34577 : }
707 : :
708 : 4203 : MemoryContextSwitchTo(so->traversalCxt);
709 : :
710 [ + + ]: 32081 : for (i = 0; i < out.nNodes; i++)
711 : : {
712 : 27878 : int nodeN = out.nodeNumbers[i];
713 : 27878 : SpGistSearchItem *innerItem;
714 : 27878 : double *distances;
715 : :
716 [ + - ]: 27878 : Assert(nodeN >= 0 && nodeN < nNodes);
717 : :
718 : 27878 : node = nodes[nodeN];
719 : :
720 [ + + ]: 27878 : if (!ItemPointerIsValid(&node->t_tid))
721 : 11179 : continue;
722 : :
723 : : /*
724 : : * Use infinity distances if innerConsistentFn() failed to return
725 : : * them or if is a NULL item (their distances are really unused).
726 : : */
727 [ + + ]: 16699 : distances = out.distances ? out.distances[i] : so->infDistances;
728 : :
729 : 33398 : innerItem = spgMakeInnerItem(so, item, node, &out, i, isnull,
730 : 16699 : distances);
731 : :
732 : 16699 : spgAddSearchItemToQueue(so, innerItem);
733 [ - + + ]: 27878 : }
734 : 4203 : }
735 : :
736 : 4203 : MemoryContextSwitchTo(oldCxt);
737 : 4203 : }
738 : :
739 : : /* Returns a next item in an (ordered) scan or null if the index is exhausted */
740 : : static SpGistSearchItem *
741 : 77533 : spgGetNextQueueItem(SpGistScanOpaque so)
742 : : {
743 [ + + ]: 77533 : if (pairingheap_is_empty(so->scanQueue))
744 : 147 : return NULL; /* Done when both heaps are empty */
745 : :
746 : : /* Return item; caller is responsible to pfree it */
747 : 77386 : return (SpGistSearchItem *) pairingheap_remove_first(so->scanQueue);
748 : 77533 : }
749 : :
750 : : enum SpGistSpecialOffsetNumbers
751 : : {
752 : : SpGistBreakOffsetNumber = InvalidOffsetNumber,
753 : : SpGistRedirectOffsetNumber = MaxOffsetNumber + 1,
754 : : SpGistErrorOffsetNumber = MaxOffsetNumber + 2,
755 : : };
756 : :
757 : : static OffsetNumber
758 : 440364 : spgTestLeafTuple(SpGistScanOpaque so,
759 : : SpGistSearchItem *item,
760 : : Page page, OffsetNumber offset,
761 : : bool isnull, bool isroot,
762 : : bool *reportedSome,
763 : : storeRes_func storeRes)
764 : : {
765 : 880728 : SpGistLeafTuple leafTuple = (SpGistLeafTuple)
766 : 440364 : PageGetItem(page, PageGetItemId(page, offset));
767 : :
768 [ - + ]: 440364 : if (leafTuple->tupstate != SPGIST_LIVE)
769 : : {
770 [ # # ]: 0 : if (!isroot) /* all tuples on root should be live */
771 : : {
772 [ # # ]: 0 : if (leafTuple->tupstate == SPGIST_REDIRECT)
773 : : {
774 : : /* redirection tuple should be first in chain */
775 [ # # ]: 0 : Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
776 : : /* transfer attention to redirect point */
777 : 0 : item->heapPtr = ((SpGistDeadTuple) leafTuple)->pointer;
778 [ # # ]: 0 : Assert(ItemPointerGetBlockNumber(&item->heapPtr) != SPGIST_METAPAGE_BLKNO);
779 : 0 : return SpGistRedirectOffsetNumber;
780 : : }
781 : :
782 [ # # ]: 0 : if (leafTuple->tupstate == SPGIST_DEAD)
783 : : {
784 : : /* dead tuple should be first in chain */
785 [ # # ]: 0 : Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
786 : : /* No live entries on this page */
787 [ # # ]: 0 : Assert(SGLT_GET_NEXTOFFSET(leafTuple) == InvalidOffsetNumber);
788 : 0 : return SpGistBreakOffsetNumber;
789 : : }
790 : 0 : }
791 : :
792 : : /* We should not arrive at a placeholder */
793 [ # # # # ]: 0 : elog(ERROR, "unexpected SPGiST tuple state: %d", leafTuple->tupstate);
794 : 0 : return SpGistErrorOffsetNumber;
795 : : }
796 : :
797 [ + - ]: 440364 : Assert(ItemPointerIsValid(&leafTuple->heapPtr));
798 : :
799 : 440364 : spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
800 : :
801 : 440364 : return SGLT_GET_NEXTOFFSET(leafTuple);
802 : 440364 : }
803 : :
804 : : /*
805 : : * Walk the tree and report all tuples passing the scan quals to the storeRes
806 : : * subroutine.
807 : : *
808 : : * If scanWholeIndex is true, we'll do just that. If not, we'll stop at the
809 : : * next page boundary once we have reported at least one tuple.
810 : : */
811 : : static void
812 : 62915 : spgWalk(Relation index, SpGistScanOpaque so, bool scanWholeIndex,
813 : : storeRes_func storeRes)
814 : : {
815 : 62915 : Buffer buffer = InvalidBuffer;
816 : 62915 : bool reportedSome = false;
817 : :
818 [ + + + + ]: 140301 : while (scanWholeIndex || !reportedSome)
819 : : {
820 : 77533 : SpGistSearchItem *item = spgGetNextQueueItem(so);
821 : :
822 [ + + ]: 77533 : if (item == NULL)
823 : 147 : break; /* No more items in queue -> done */
824 : :
825 : : redirect:
826 : : /* Check for interrupts, just in case of infinite loop */
827 [ + - ]: 77386 : CHECK_FOR_INTERRUPTS();
828 : :
829 [ + + ]: 77386 : if (item->isLeaf)
830 : : {
831 : : /* We store heap items in the queue only in case of ordered search */
832 [ - + ]: 60559 : Assert(so->numberOfNonNullOrderBys > 0);
833 : 121118 : storeRes(so, &item->heapPtr, item->value, item->isNull,
834 : 60559 : item->leafTuple, item->recheck,
835 : 60559 : item->recheckDistances, item->distances);
836 : 60559 : reportedSome = true;
837 : 60559 : }
838 : : else
839 : : {
840 : 16827 : BlockNumber blkno = ItemPointerGetBlockNumber(&item->heapPtr);
841 : 16827 : OffsetNumber offset = ItemPointerGetOffsetNumber(&item->heapPtr);
842 : 16827 : Page page;
843 : 16827 : bool isnull;
844 : :
845 [ + + ]: 16827 : if (buffer == InvalidBuffer)
846 : : {
847 : 3345 : buffer = ReadBuffer(index, blkno);
848 : 3345 : LockBuffer(buffer, BUFFER_LOCK_SHARE);
849 : 3345 : }
850 [ + + ]: 13482 : else if (blkno != BufferGetBlockNumber(buffer))
851 : : {
852 : 9287 : UnlockReleaseBuffer(buffer);
853 : 9287 : buffer = ReadBuffer(index, blkno);
854 : 9287 : LockBuffer(buffer, BUFFER_LOCK_SHARE);
855 : 9287 : }
856 : :
857 : : /* else new pointer points to the same page, no work needed */
858 : :
859 : 16827 : page = BufferGetPage(buffer);
860 : :
861 : 16827 : isnull = SpGistPageStoresNulls(page) ? true : false;
862 : :
863 [ + + ]: 16827 : if (SpGistPageIsLeaf(page))
864 : : {
865 : : /* Page is a leaf - that is, all its tuples are heap items */
866 : 12624 : OffsetNumber max = PageGetMaxOffsetNumber(page);
867 : :
868 [ + + + + ]: 12624 : if (SpGistBlockIsRoot(blkno))
869 : : {
870 : : /* When root is a leaf, examine all its tuples */
871 [ + + ]: 1021 : for (offset = FirstOffsetNumber; offset <= max; offset++)
872 : 1976 : (void) spgTestLeafTuple(so, item, page, offset,
873 : 988 : isnull, true,
874 : 988 : &reportedSome, storeRes);
875 : 33 : }
876 : : else
877 : : {
878 : : /* Normal case: just examine the chain we arrived at */
879 [ + + ]: 451967 : while (offset != InvalidOffsetNumber)
880 : : {
881 [ + - ]: 439376 : Assert(offset >= FirstOffsetNumber && offset <= max);
882 : 878752 : offset = spgTestLeafTuple(so, item, page, offset,
883 : 439376 : isnull, false,
884 : 439376 : &reportedSome, storeRes);
885 [ - + ]: 439376 : if (offset == SpGistRedirectOffsetNumber)
886 : 0 : goto redirect;
887 : : }
888 : : }
889 [ - + ]: 12624 : }
890 : : else /* page is inner */
891 : : {
892 : 8406 : SpGistInnerTuple innerTuple = (SpGistInnerTuple)
893 : 4203 : PageGetItem(page, PageGetItemId(page, offset));
894 : :
895 [ + - ]: 4203 : if (innerTuple->tupstate != SPGIST_LIVE)
896 : : {
897 [ # # ]: 0 : if (innerTuple->tupstate == SPGIST_REDIRECT)
898 : : {
899 : : /* transfer attention to redirect point */
900 : 0 : item->heapPtr = ((SpGistDeadTuple) innerTuple)->pointer;
901 [ # # ]: 0 : Assert(ItemPointerGetBlockNumber(&item->heapPtr) !=
902 : : SPGIST_METAPAGE_BLKNO);
903 : 0 : goto redirect;
904 : : }
905 [ # # # # ]: 0 : elog(ERROR, "unexpected SPGiST tuple state: %d",
906 : : innerTuple->tupstate);
907 : 0 : }
908 : :
909 : 4203 : spgInnerTest(so, item, innerTuple, isnull);
910 [ - + ]: 4203 : }
911 [ + - ]: 16827 : }
912 : :
913 : : /* done with this scan item */
914 : 77386 : spgFreeSearchItem(so, item);
915 : : /* clear temp context before proceeding to the next one */
916 : 77386 : MemoryContextReset(so->tempCxt);
917 [ + + ]: 77533 : }
918 : :
919 [ + + ]: 62915 : if (buffer != InvalidBuffer)
920 : 3345 : UnlockReleaseBuffer(buffer);
921 : 62915 : }
922 : :
923 : :
924 : : /* storeRes subroutine for getbitmap case */
925 : : static void
926 : 175369 : storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
927 : : Datum leafValue, bool isnull,
928 : : SpGistLeafTuple leafTuple, bool recheck,
929 : : bool recheckDistances, double *distances)
930 : : {
931 [ + - ]: 175369 : Assert(!recheckDistances && !distances);
932 : 175369 : tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
933 : 175369 : so->ntids++;
934 : 175369 : }
935 : :
936 : : int64
937 : 58 : spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
938 : : {
939 : 58 : SpGistScanOpaque so = (SpGistScanOpaque) scan->opaque;
940 : :
941 : : /* Copy want_itup to *so so we don't need to pass it around separately */
942 : 58 : so->want_itup = false;
943 : :
944 : 58 : so->tbm = tbm;
945 : 58 : so->ntids = 0;
946 : :
947 : 58 : spgWalk(scan->indexRelation, so, true, storeBitmap);
948 : :
949 : 116 : return so->ntids;
950 : 58 : }
951 : :
952 : : /* storeRes subroutine for gettuple case */
953 : : static void
954 : 167737 : storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
955 : : Datum leafValue, bool isnull,
956 : : SpGistLeafTuple leafTuple, bool recheck,
957 : : bool recheckDistances, double *nonNullDistances)
958 : : {
959 [ + - ]: 167737 : Assert(so->nPtrs < MaxIndexTuplesPerPage);
960 : 167737 : so->heapPtrs[so->nPtrs] = *heapPtr;
961 : 167737 : so->recheck[so->nPtrs] = recheck;
962 : 167737 : so->recheckDistances[so->nPtrs] = recheckDistances;
963 : :
964 [ + + ]: 167737 : if (so->numberOfOrderBys > 0)
965 : : {
966 [ + + + - ]: 60559 : if (isnull || so->numberOfNonNullOrderBys <= 0)
967 : 8 : so->distances[so->nPtrs] = NULL;
968 : : else
969 : : {
970 : 60551 : IndexOrderByDistance *distances = palloc_array(IndexOrderByDistance,
971 : : so->numberOfOrderBys);
972 : 60551 : int i;
973 : :
974 [ + + ]: 121105 : for (i = 0; i < so->numberOfOrderBys; i++)
975 : : {
976 : 60554 : int offset = so->nonNullOrderByOffsets[i];
977 : :
978 [ + + ]: 60554 : if (offset >= 0)
979 : : {
980 : : /* Copy non-NULL distance value */
981 : 60553 : distances[i].value = nonNullDistances[offset];
982 : 60553 : distances[i].isnull = false;
983 : 60553 : }
984 : : else
985 : : {
986 : : /* Set distance's NULL flag. */
987 : 1 : distances[i].value = 0.0;
988 : 1 : distances[i].isnull = true;
989 : : }
990 : 60554 : }
991 : :
992 : 60551 : so->distances[so->nPtrs] = distances;
993 : 60551 : }
994 : 60559 : }
995 : :
996 [ + + ]: 167737 : if (so->want_itup)
997 : : {
998 : : /*
999 : : * Reconstruct index data. We have to copy the datum out of the temp
1000 : : * context anyway, so we may as well create the tuple here.
1001 : : */
1002 : 153231 : Datum leafDatums[INDEX_MAX_KEYS];
1003 : 153231 : bool leafIsnulls[INDEX_MAX_KEYS];
1004 : :
1005 : : /* We only need to deform the old tuple if it has INCLUDE attributes */
1006 [ + + ]: 153231 : if (so->state.leafTupDesc->natts > 1)
1007 : 20 : spgDeformLeafTuple(leafTuple, so->state.leafTupDesc,
1008 : 10 : leafDatums, leafIsnulls, isnull);
1009 : :
1010 : 153231 : leafDatums[spgKeyColumn] = leafValue;
1011 : 153231 : leafIsnulls[spgKeyColumn] = isnull;
1012 : :
1013 : 306462 : so->reconTups[so->nPtrs] = heap_form_tuple(so->reconTupDesc,
1014 : 153231 : leafDatums,
1015 : 153231 : leafIsnulls);
1016 : 153231 : }
1017 : 167737 : so->nPtrs++;
1018 : 167737 : }
1019 : :
1020 : : bool
1021 : 167810 : spggettuple(IndexScanDesc scan, ScanDirection dir)
1022 : : {
1023 : 167810 : SpGistScanOpaque so = (SpGistScanOpaque) scan->opaque;
1024 : :
1025 [ + - ]: 167810 : if (dir != ForwardScanDirection)
1026 [ # # # # ]: 0 : elog(ERROR, "SP-GiST only supports forward scan direction");
1027 : :
1028 : : /* Copy want_itup to *so so we don't need to pass it around separately */
1029 : 167810 : so->want_itup = scan->xs_want_itup;
1030 : :
1031 : 230578 : for (;;)
1032 : : {
1033 [ + + ]: 230578 : if (so->iPtr < so->nPtrs)
1034 : : {
1035 : : /* continuing to return reported tuples */
1036 : 167721 : scan->xs_heaptid = so->heapPtrs[so->iPtr];
1037 : 167721 : scan->xs_recheck = so->recheck[so->iPtr];
1038 : 167721 : scan->xs_hitup = so->reconTups[so->iPtr];
1039 : :
1040 [ + + ]: 167721 : if (so->numberOfOrderBys > 0)
1041 : 121118 : index_store_float8_orderby_distances(scan, so->orderByTypes,
1042 : 60559 : so->distances[so->iPtr],
1043 : 60559 : so->recheckDistances[so->iPtr]);
1044 : 167721 : so->iPtr++;
1045 : 167721 : return true;
1046 : : }
1047 : :
1048 [ + + ]: 62857 : if (so->numberOfOrderBys > 0)
1049 : : {
1050 : : /* Must pfree distances to avoid memory leak */
1051 : 60568 : int i;
1052 : :
1053 [ + + ]: 121123 : for (i = 0; i < so->nPtrs; i++)
1054 [ + + ]: 121102 : if (so->distances[i])
1055 : 60547 : pfree(so->distances[i]);
1056 : 60568 : }
1057 : :
1058 [ + + ]: 62857 : if (so->want_itup)
1059 : : {
1060 : : /* Must pfree reconstructed tuples to avoid memory leak */
1061 : 48405 : int i;
1062 : :
1063 [ + + ]: 201613 : for (i = 0; i < so->nPtrs; i++)
1064 : 153208 : pfree(so->reconTups[i]);
1065 : 48405 : }
1066 : 62857 : so->iPtr = so->nPtrs = 0;
1067 : :
1068 : 62857 : spgWalk(scan->indexRelation, so, false, storeGettuple);
1069 : :
1070 [ + + ]: 62857 : if (so->nPtrs == 0)
1071 : 89 : break; /* must have completed scan */
1072 : : }
1073 : :
1074 : 89 : return false;
1075 : 167810 : }
1076 : :
1077 : : bool
1078 : 305 : spgcanreturn(Relation index, int attno)
1079 : : {
1080 : 305 : SpGistCache *cache;
1081 : :
1082 : : /* INCLUDE attributes can always be fetched for index-only scans */
1083 [ + + ]: 305 : if (attno > 1)
1084 : 2 : return true;
1085 : :
1086 : : /* We can do it if the opclass config function says so */
1087 : 303 : cache = spgGetCache(index);
1088 : :
1089 : 303 : return cache->config.canReturnData;
1090 : 305 : }
|