GCC Code Coverage Report


Directory: src/
File: src/pg/inspect.c
Date: 2026-10-23 09:17:32
Exec Total Coverage
Lines: 394 417 94.5%
Functions: 18 18 100.0%
Branches: 156 192 81.2%

Line Branch Exec Source
1 /*
2 * Copyright (c) 2026 Tiger Data, Inc.
3 * Licensed under the PostgreSQL License. See LICENSE for details.
4 *
5 * inspect.c - read-only index inspection functions
6 *
7 * Set-returning functions that expose the internal structure of prism
8 * indexes via SQL. All are read-only (AccessShareLock, no WAL); the mutating
9 * maintenance operations live in maintenance.c. The centroid-tree leaf walk
10 * they share, collect_leaf_entries, is declared in inspect.h.
11 *
12 * Functions:
13 * prism_centroid_pages(regclass) -- centroid tree structure
14 * prism_posting_pages(regclass) -- posting list page chains
15 * prism_tids_clusters(regclass, tid[]) -- which cluster(s) hold each TID
16 * prism_index_settings(regclass) -- effective (resolved) settings
17 */
18
19 #include <postgres.h>
20
21 #include <access/relation.h>
22 #include <catalog/index.h>
23 #include <catalog/pg_class.h>
24 #include <catalog/pg_type.h>
25 #include <funcapi.h>
26 #include <miscadmin.h>
27 #include <nodes/parsenodes.h>
28 #include <storage/bufmgr.h>
29 #include <utils/acl.h>
30 #include <utils/array.h>
31 #include <utils/builtins.h>
32 #include <utils/lsyscache.h>
33 #include <utils/rel.h>
34 #include <utils/syscache.h>
35 #include <utils/tuplestore.h>
36
37 #include "index/centroid_page.h"
38 #include "index/index_base.h"
39 #include "index/index_build.h"
40 #include "index/posting_page.h"
41 #include "index/query_scan.h"
42 #include "inspect.h"
43 #include "meta.h"
44 #include "pg/bufstorage.h"
45 #include "support_pg.h"
46
47 26 PG_FUNCTION_INFO_V1(vs_centroid_pages);
48 39 PG_FUNCTION_INFO_V1(vs_posting_pages);
49 9 PG_FUNCTION_INFO_V1(vs_tids_clusters);
50 16 PG_FUNCTION_INFO_V1(vs_index_settings);
51
52 /*
53 * The functions below iterate an on-disk entry_count read straight from a
54 * page. Reject a count past the format's real per-page capacity before
55 * looping so a corrupt or truncated page can't drive a read past the page.
56 * Posting pages get the same treatment from prism_posting_check_count.
57 */
58 static void
59 22745 check_centroid_count(
60 BlockNumber blkno,
61 uint32_t count,
62 Dimension dim,
63 PrismCentroidFormat fmt)
64 {
65 22745 uint32_t max_entries = prism_centroid_max_entries_fmt(dim, fmt);
66
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22745 times.
22745 if (count > max_entries)
67 ✗ ereport(ERROR,
68 (errcode(ERRCODE_DATA_CORRUPTED),
69 errmsg("centroid page %u has an invalid entry count "
70 "(%u > %u)",
71 blkno,
72 count,
73 max_entries),
74 errhint("The index may be corrupted; REINDEX it.")));
75 22745 }
76
77 /*
78 * Authorization for the inspection functions. They expose an index's
79 * internal structure (block numbers, cluster ids, tombstone state), so
80 * gate them on the caller's privileges on the *table* the index belongs
81 * to -- the same model PostgreSQL's pgrowlocks/pgstattuple use for
82 * relation inspection. EXECUTE stays granted to PUBLIC; these runtime
83 * checks do the per-object authorization that a static GRANT cannot
84 * express for a regclass argument. Superusers pass automatically.
85 *
86 * Read-only inspectors require SELECT on the table (its owner has that, so an
87 * owner can always inspect their own index). prism_tids_clusters is the
88 * exception: it answers which rows live where, so it requires ownership.
89 * The mutating maintenance functions check ownership separately.
90 */
91 static void
92 163 require_index_select(Relation index, LOCKMODE lockmode)
93 {
94 163 Oid heaprelid = IndexGetRelation(RelationGetRelid(index), false);
95 163 AclResult aclresult =
96 163 pg_class_aclcheck(heaprelid, GetUserId(), ACL_SELECT);
97
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 160 times.
163 if (aclresult != ACLCHECK_OK)
98 {
99 3 char *relname = get_rel_name(heaprelid);
100 3 ObjectType objtype = get_relkind_objtype(get_rel_relkind(heaprelid));
101 3 relation_close(index, lockmode);
102 3 aclcheck_error(aclresult, objtype, relname);
103 }
104 160 }
105
106 /* Same ownership gate as the maintenance procedures. A SELECT grant is not
107 * enough: the function confirms which TIDs the index holds. */
108 static void
109 7 require_index_owner(Relation index, LOCKMODE lockmode)
110 {
111 7 Oid heaprelid = IndexGetRelation(RelationGetRelid(index), false);
112
2/2
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 5 times.
7 if (!object_ownercheck(RelationRelationId, heaprelid, GetUserId()))
113 {
114 2 char *relname = get_rel_name(heaprelid);
115 2 ObjectType objtype = get_relkind_objtype(get_rel_relkind(heaprelid));
116 2 relation_close(index, lockmode);
117 2 aclcheck_error(ACLCHECK_NOT_OWNER, objtype, relname);
118 }
119 5 }
120
121 /* ----------------------------------------------------------------
122 * Shared helpers
123 * ---------------------------------------------------------------- */
124
125 /*
126 * Grow a doubling array before an append: when len has reached *cap, double
127 * *cap and repalloc. Returns the (possibly moved) base pointer; pass
128 * sizeof(*arr) as elem_size. A no-op with room to spare, so it is safe to call
129 * unconditionally before every append.
130 */
131 static void *
132 92401 grow_if_full(void *arr, int len, int *cap, Size elem_size)
133 {
134 92401 if (len < *cap)
135 return arr;
136 787 *cap *= 2;
137 787 return repalloc(arr, (Size)*cap * elem_size);
138 }
139
140 /*
141 * BFS the centroid tree and collect all leaf entries. Returns the count and
142 * fills *out (palloc'd array). Caller must pfree. Declared in inspect.h and
143 * shared with the maintenance functions; LeafEntry is defined there too.
144 */
145 int
146 406 collect_leaf_entries(
147 Relation index,
148 BlockNumber first_centroid,
149 uint8_t nlevels,
150 Dimension dim,
151 LeafEntry **out)
152 {
153 406 VsPgStorage store;
154 406 VsStorage *st = &store.base;
155
156 406 vs_pg_storage_init_inspect(&store, index);
157
158 406 int wl_cap = 64;
159 406 int wl_len = 0;
160 406 int wl_head = 0;
161 406 BlockNumber *wl = palloc(wl_cap * sizeof(BlockNumber));
162
163 406 int leaves_cap = 64;
164 406 int leaves_len = 0;
165 406 LeafEntry *leaves = palloc(leaves_cap * sizeof(LeafEntry));
166
167 406 wl[wl_len++] = first_centroid;
168
169
2/2
✓ Branch 0 taken 22435 times.
✓ Branch 1 taken 406 times.
22841 while (wl_head < wl_len)
170 {
171 22435 BlockNumber blkno = wl[wl_head++];
172
173 22435 Page page = vs_storage_read_page(st, blkno);
174
175 22435 const PrismCentroidPageOpaque *opaque = PRISM_CENTROID_OPAQUE(page);
176 22435 uint16_t nentries = opaque->entry_count;
177 22435 PrismCentroidFormat fmt =
178 22435 (PrismCentroidFormat)(opaque->flags & PRISM_CENTROID_FMT_MASK);
179 22435 bool is_leaf_page = (opaque->level == nlevels - 1);
180
181 22435 check_centroid_count(blkno, nentries, dim, fmt);
182
183
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22435 times.
22435 if (BlockNumberIsValid(opaque->next_blkno))
184 {
185 ✗ wl = grow_if_full(wl, wl_len, &wl_cap, sizeof(*wl));
186 ✗ wl[wl_len++] = opaque->next_blkno;
187 }
188
189
2/2
✓ Branch 0 taken 92401 times.
✓ Branch 1 taken 22435 times.
114836 for (uint16_t i = 0; i < nentries; i++)
190 {
191 92401 BlockNumber child;
192 92401 bool is_leaf;
193
194
2/2
✓ Branch 0 taken 90606 times.
✓ Branch 1 taken 1795 times.
92401 if (fmt == PRISM_CENTROID_FMT_FASTSCAN)
195 {
196 /* No per-entry meta: read the child from the packed
197 * group array and take leaf status from the page level. */
198 90606 char *content = (char *)PageGetContents(page);
199 90606 uint32_t g = i / VS_FASTSCAN_GROUP;
200 90606 uint32_t slot = i % VS_FASTSCAN_GROUP;
201 90606 BlockNumber *grp =
202 90606 prism_centroid_fastscan_group_child(content, g, dim);
203 90606 child = grp[slot];
204 90606 is_leaf = is_leaf_page;
205 }
206 else
207 {
208 1795 const PrismCentroidEntryMeta *entry =
209 1795 prism_centroid_meta(page, i);
210 1795 child = entry->child_blkno;
211 1795 is_leaf = (entry->flags & PRISM_CENTROID_FLAG_LEAF) != 0;
212 }
213
214
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 92401 times.
92401 if (!BlockNumberIsValid(child))
215 ✗ continue;
216
217
2/2
✓ Branch 0 taken 70372 times.
✓ Branch 1 taken 22029 times.
92401 if (is_leaf)
218 {
219
2/2
✓ Branch 0 taken 526 times.
✓ Branch 1 taken 69846 times.
70372 leaves = grow_if_full(
220 leaves, leaves_len, &leaves_cap, sizeof(*leaves));
221 70372 leaves[leaves_len++] = (LeafEntry){
222 .posting_head = child,
223 .centroid_page = blkno,
224 .entry_idx = i,
225 };
226 }
227 else
228 {
229
2/2
✓ Branch 0 taken 261 times.
✓ Branch 1 taken 21768 times.
22029 wl = grow_if_full(wl, wl_len, &wl_cap, sizeof(*wl));
230 22029 wl[wl_len++] = child;
231 }
232 }
233
234 22435 vs_storage_release_page(st, blkno);
235 }
236
237 406 pfree(wl);
238 406 *out = leaves;
239 406 return leaves_len;
240 }
241
242 /* Format name lookup (indexed by PrismCentroidFormat) */
243 static const char *centroid_format_names[] = {
244 [PRISM_CENTROID_FMT_RABITQ] = "rabitq",
245 [PRISM_CENTROID_FMT_FLOAT] = "float",
246 [PRISM_CENTROID_FMT_HALF] = "half",
247 [PRISM_CENTROID_FMT_FASTSCAN] = "fastscan",
248 };
249
250 /*
251 * vs_centroid_pages(regclass)
252 *
253 * Returns one row per centroid entry: blkno, entry, level, format,
254 * child_blkno, child_count, is_leaf. Traverses the tree via BFS
255 * starting from the metapage's first_centroid, following next_blkno
256 * chains and child_blkno links.
257 */
258 Datum
259 43 vs_centroid_pages(PG_FUNCTION_ARGS)
260 {
261 43 Oid indexoid = PG_GETARG_OID(0);
262 43 ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo;
263
264 43 InitMaterializedSRF(fcinfo, 0);
265
266 /* Open relation and verify it's an index */
267 43 Relation index = relation_open(indexoid, AccessShareLock);
268
269
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 43 times.
43 if (index->rd_rel->relkind != RELKIND_INDEX)
270 {
271 ✗ relation_close(index, AccessShareLock);
272 ✗ ereport(ERROR,
273 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
274 errmsg("\"%s\" is not an index",
275 RelationGetRelationName(index))));
276 }
277
278 43 require_index_select(index, AccessShareLock);
279
280 42 VsPgStorage store;
281 42 VsStorage *st = &store.base;
282
283 42 vs_pg_storage_init_inspect(&store, index);
284
285 /* Read metapage and verify magic */
286 42 Page meta_page = vs_storage_read_page(st, 0);
287
288 42 const PrismMetaPage *meta = (const PrismMetaPage *)PageGetSpecialPointer(
289 meta_page);
290
291
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 41 times.
42 if (meta->magic != PRISM_META_MAGIC)
292 {
293 1 vs_storage_release_page(st, 0);
294 1 relation_close(index, AccessShareLock);
295
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 ereport(ERROR,
296 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
297 errmsg("\"%s\" is not a prism index",
298 RelationGetRelationName(index))));
299 }
300
301 41 BlockNumber first_centroid = meta->first_centroid;
302 41 Dimension dim = (Dimension)meta->dim;
303 41 uint8_t nlevels = meta->nlevels;
304
305 41 vs_storage_release_page(st, 0);
306
307
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 41 times.
41 if (!BlockNumberIsValid(first_centroid))
308 {
309 ✗ relation_close(index, AccessShareLock);
310 ✗ PG_RETURN_NULL();
311 }
312
313 /*
314 * BFS worklist — simple dynamic array of block numbers.
315 * Start with the root centroid page.
316 */
317 41 int worklist_cap = 64;
318 41 int worklist_len = 0;
319 41 int worklist_head = 0;
320 41 BlockNumber *worklist = palloc(worklist_cap * sizeof(BlockNumber));
321
322 41 worklist[worklist_len++] = first_centroid;
323
324
2/2
✓ Branch 0 taken 310 times.
✓ Branch 1 taken 41 times.
351 while (worklist_head < worklist_len)
325 {
326 310 BlockNumber blkno = worklist[worklist_head++];
327
328 310 Page page = vs_storage_read_page(st, blkno);
329
330 310 const PrismCentroidPageOpaque *opaque = PRISM_CENTROID_OPAQUE(page);
331 310 PrismCentroidFormat fmt =
332 310 (PrismCentroidFormat)(opaque->flags & PRISM_CENTROID_FMT_MASK);
333 /* FASTSCAN doesn't have per-entry flags, so determine leaf
334 * status from tree depth: bottom level holds posting heads. */
335 310 bool is_leaf_page = (opaque->level == nlevels - 1);
336
337 /* Emit one row per entry. FASTSCAN pages have a different
338 * layout (group section instead of per-entry meta + data),
339 * so child_blkno lives in the group array and per-entry
340 * flags don't exist — the leaf bit is page-level. */
341 310 uint16_t nentries = opaque->entry_count;
342
343 310 check_centroid_count(blkno, nentries, dim, fmt);
344
345
2/2
✓ Branch 0 taken 205 times.
✓ Branch 1 taken 105 times.
310 if (fmt == PRISM_CENTROID_FMT_FASTSCAN)
346 {
347 char *content = (char *)PageGetContents(page);
348
349
2/2
✓ Branch 0 taken 1476 times.
✓ Branch 1 taken 205 times.
1681 for (uint16_t i = 0; i < nentries; i++)
350 {
351 1476 uint32_t g = i / VS_FASTSCAN_GROUP;
352 1476 uint32_t slot = i % VS_FASTSCAN_GROUP;
353 1476 BlockNumber *child =
354 1476 prism_centroid_fastscan_group_child(content, g, dim);
355
356 1476 Datum values[7];
357 1476 bool nulls[7] = {0};
358
359 1476 values[0] = Int32GetDatum((int32)blkno);
360 1476 values[1] = Int16GetDatum((int16)i);
361 1476 values[2] = Int16GetDatum((int16)opaque->level);
362
1/2
✓ Branch 1 taken 1476 times.
✗ Branch 2 not taken.
1476 values[3] = CStringGetTextDatum(centroid_format_names[fmt]);
363
364
1/2
✓ Branch 0 taken 1476 times.
✗ Branch 1 not taken.
1476 if (BlockNumberIsValid(child[slot]))
365 1476 values[4] = Int32GetDatum((int32)child[slot]);
366 else
367 ✗ nulls[4] = true;
368
369 /* child_count is not stored per-entry in FASTSCAN — the
370 * page-level leaf flag tells us whether children are
371 * posting heads (child_count = 0) or another centroid
372 * page. Leave as 0 / NULL. */
373 1476 nulls[5] = true;
374
2/2
✓ Branch 0 taken 193 times.
✓ Branch 1 taken 1283 times.
1476 values[6] = BoolGetDatum(is_leaf_page);
375
376 1476 tuplestore_putvalues(
377 rsinfo->setResult, rsinfo->setDesc, values, nulls);
378
379 /* Enqueue children if this isn't a leaf page */
380
3/4
✓ Branch 0 taken 193 times.
✓ Branch 1 taken 1283 times.
✓ Branch 2 taken 193 times.
✗ Branch 3 not taken.
1476 if (!is_leaf_page && BlockNumberIsValid(child[slot]))
381 {
382
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 192 times.
193 if (worklist_len >= worklist_cap)
383 {
384 1 worklist_cap *= 2;
385 1 worklist = repalloc(
386 worklist, worklist_cap * sizeof(BlockNumber));
387 }
388 193 worklist[worklist_len++] = child[slot];
389 }
390 }
391 }
392 else
393 {
394
2/2
✓ Branch 0 taken 1325 times.
✓ Branch 1 taken 105 times.
1430 for (uint16_t i = 0; i < nentries; i++)
395 {
396 1325 const PrismCentroidEntryMeta *entry =
397 1325 prism_centroid_meta(page, i);
398 1325 bool is_leaf = (entry->flags & PRISM_CENTROID_FLAG_LEAF) != 0;
399
400 1325 Datum values[7];
401 1325 bool nulls[7] = {0};
402
403 1325 values[0] = Int32GetDatum((int32)blkno);
404 1325 values[1] = Int16GetDatum((int16)i);
405 1325 values[2] = Int16GetDatum((int16)opaque->level);
406
1/2
✓ Branch 1 taken 1325 times.
✗ Branch 2 not taken.
1325 values[3] = CStringGetTextDatum(centroid_format_names[fmt]);
407
408 /* Leaf entries carry their posting-list head block in
409 * child_blkno (formula-derived: first_posting + leaf), so
410 * expose it -- structural tests join it against the posting
411 * heads. */
412
1/2
✓ Branch 0 taken 1325 times.
✗ Branch 1 not taken.
1325 if (BlockNumberIsValid(entry->child_blkno))
413 1325 values[4] = Int32GetDatum((int32)entry->child_blkno);
414 else
415 ✗ nulls[4] = true;
416
417
2/2
✓ Branch 0 taken 61 times.
✓ Branch 1 taken 1264 times.
1325 values[5] = Int16GetDatum((int16)entry->child_count);
418
2/2
✓ Branch 0 taken 61 times.
✓ Branch 1 taken 1264 times.
1325 values[6] = BoolGetDatum(is_leaf);
419
420 1325 tuplestore_putvalues(
421 rsinfo->setResult, rsinfo->setDesc, values, nulls);
422
423
3/4
✓ Branch 0 taken 61 times.
✓ Branch 1 taken 1264 times.
✓ Branch 2 taken 61 times.
✗ Branch 3 not taken.
1325 if (!is_leaf && BlockNumberIsValid(entry->child_blkno))
424 {
425
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 61 times.
61 if (worklist_len >= worklist_cap)
426 {
427 ✗ worklist_cap *= 2;
428 ✗ worklist = repalloc(
429 worklist, worklist_cap * sizeof(BlockNumber));
430 }
431 61 worklist[worklist_len++] = entry->child_blkno;
432 }
433 }
434 }
435
436 /* Enqueue sibling (next_blkno chain) */
437
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 295 times.
310 if (BlockNumberIsValid(opaque->next_blkno))
438 {
439
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
15 if (worklist_len >= worklist_cap)
440 {
441 ✗ worklist_cap *= 2;
442 ✗ worklist =
443 ✗ repalloc(worklist, worklist_cap * sizeof(BlockNumber));
444 }
445 15 worklist[worklist_len++] = opaque->next_blkno;
446 }
447
448 310 vs_storage_release_page(st, blkno);
449 }
450
451 41 pfree(worklist);
452 41 relation_close(index, AccessShareLock);
453
454 41 PG_RETURN_NULL();
455 }
456
457 /*
458 * One prism_posting_pages() row per page of a chain.
459 *
460 * Everything the rows need beyond the page itself -- the tuplestore to emit
461 * into, the dimension, the running position in the chain -- rides in the
462 * walk's caller state, so the walk itself carries nothing about what it is
463 * being walked for.
464 */
465 typedef struct PostingRowCtx
466 {
467 ReturnSetInfo *rsinfo;
468 Dimension dim;
469 int chain_pos;
470 } PostingRowCtx;
471
472 static bool
473 2935 emit_posting_row(PrismPostingChainPos *pos, void *state)
474 {
475 2935 PostingRowCtx *ctx = state;
476 2935 Dimension dim = ctx->dim;
477 2935 const PrismPostingPageOpaque *op = prism_posting_opaque(pos->page);
478 2935 bool is_first = (op->flags & PRISM_POSTING_PAGE_FIRST) != 0;
479 2935 bool tombstoned = (op->flags & PRISM_POSTING_PAGE_TOMBSTONED) != 0;
480 2935 bool is_fastscan = (op->flags & PRISM_POSTING_PAGE_FASTSCAN) != 0;
481
482 2935 Datum values[10];
483 2935 bool nulls[10] = {0};
484
485
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 2851 times.
2935 values[0] = Int32GetDatum((int32)pos->blkno);
486 2935 values[1] = Int32GetDatum((int32)op->cluster_id);
487
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 2851 times.
2935 values[2] = BoolGetDatum(is_first);
488
2/2
✓ Branch 0 taken 2917 times.
✓ Branch 1 taken 18 times.
2935 values[3] = BoolGetDatum(tombstoned);
489 2935 values[4] = Int32GetDatum((int32)op->entry_count);
490
491 {
492 /* AoS entries carry a per-entry DELETED flag; count them directly.
493 * FASTSCAN entries carry a per-group tombstone_mask instead, one bit
494 * per lane; popcount every group's mask and sum. Either way this is
495 * the exact dead count, not an estimate. */
496 2935 char *content = prism_posting_page_content(pos->page, dim);
497 2935 int32 dead = 0;
498
499 2935 prism_posting_check_count(pos->blkno, op, dim);
500
501
2/2
✓ Branch 0 taken 1410 times.
✓ Branch 1 taken 1525 times.
2935 if (!is_fastscan)
502 {
503
2/2
✓ Branch 0 taken 74430 times.
✓ Branch 1 taken 1410 times.
75840 for (uint32_t i = 0; i < op->entry_count; i++)
504 {
505 74430 const PrismPostingEntryHeader *h =
506
2/2
✓ Branch 0 taken 1037 times.
✓ Branch 1 taken 73393 times.
74430 prism_posting_entry_at(content, i, dim);
507
508
2/2
✓ Branch 0 taken 1037 times.
✓ Branch 1 taken 73393 times.
74430 if (h->meta.flags & PRISM_POSTING_FLAG_DELETED)
509 1037 dead++;
510 }
511 }
512 else
513 {
514 1525 uint32_t n = op->entry_count;
515 1525 uint32_t ngroups = (n + VS_FASTSCAN_GROUP - 1) / VS_FASTSCAN_GROUP;
516
517
2/2
✓ Branch 0 taken 2535 times.
✓ Branch 1 taken 1525 times.
4060 for (uint32_t g = 0; g < ngroups; g++)
518 {
519 2535 uint32_t mask =
520 2535 *prism_fastscan_group_tombstone_mask(content, g, dim);
521 /* GCC/Clang builtin, not pg_popcount32: this file already
522 * relies on compiler builtins elsewhere, and a per-page,
523 * per-call count is far from hot enough to need a
524 * runtime-dispatched popcount. */
525 2535 dead += __builtin_popcount(mask);
526 }
527 }
528
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 2851 times.
2935 values[5] = Int32GetDatum(dead);
529 }
530
531 2935 values[6] = Int32GetDatum((int32)op->max_entries);
532
533
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 2851 times.
2935 if (BlockNumberIsValid(pos->next))
534 84 values[7] = Int32GetDatum((int32)pos->next);
535 else
536 2851 nulls[7] = true;
537
538
2/2
✓ Branch 0 taken 1410 times.
✓ Branch 1 taken 1525 times.
2935 values[8] = Int32GetDatum(ctx->chain_pos++);
539
2/2
✓ Branch 0 taken 1410 times.
✓ Branch 1 taken 1525 times.
4345 values[9] = CStringGetTextDatum(is_fastscan ? "fastscan" : "aos");
540
541 2935 tuplestore_putvalues(
542 2935 ctx->rsinfo->setResult, ctx->rsinfo->setDesc, values, nulls);
543 2935 return true;
544 }
545
546 /* ----------------------------------------------------------------
547 * prism_posting_pages(regclass)
548 *
549 * Returns one row per posting page: blkno, cluster_id, is_first,
550 * tombstoned, entry_count, dead_count, max_entries, next_blkno,
551 * chain_pos, format. Tombstoned (all-dead, but still linked) pages stay
552 * in the output so bloat is visible; filter with WHERE NOT tombstoned
553 * for live pages. dead_count is the per-entry DELETED tally for AoS pages
554 * and the summed tombstone_mask popcount across groups for fastscan
555 * pages -- exact either way, never NULL.
556 *
557 * Walks all posting chains by finding leaf centroids (which store
558 * posting_head block numbers) and following next_blkno links.
559 * ---------------------------------------------------------------- */
560 Datum
561 87 vs_posting_pages(PG_FUNCTION_ARGS)
562 {
563 87 Oid indexoid = PG_GETARG_OID(0);
564 87 ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo;
565
566 87 InitMaterializedSRF(fcinfo, 0);
567
568 87 Relation index = relation_open(indexoid, AccessShareLock);
569
570
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 87 times.
87 if (index->rd_rel->relkind != RELKIND_INDEX)
571 {
572 ✗ relation_close(index, AccessShareLock);
573 ✗ ereport(ERROR,
574 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
575 errmsg("\"%s\" is not an index",
576 RelationGetRelationName(index))));
577 }
578
579 87 require_index_select(index, AccessShareLock);
580
581 86 VsPgStorage store;
582 86 VsStorage *st = &store.base;
583
584 86 vs_pg_storage_init_inspect(&store, index);
585
586 86 Page meta_page = vs_storage_read_page(st, 0);
587
588 86 const PrismMetaPage *meta = (const PrismMetaPage *)PageGetSpecialPointer(
589 meta_page);
590
591
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 85 times.
86 if (meta->magic != PRISM_META_MAGIC)
592 {
593 1 vs_storage_release_page(st, 0);
594 1 relation_close(index, AccessShareLock);
595
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 ereport(ERROR,
596 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
597 errmsg("\"%s\" is not a prism index",
598 RelationGetRelationName(index))));
599 }
600
601 85 BlockNumber first_centroid = meta->first_centroid;
602 85 Dimension dim = meta->dim;
603 85 uint8_t nlevels = meta->nlevels;
604 85 vs_storage_release_page(st, 0);
605
606
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 85 times.
85 if (!BlockNumberIsValid(first_centroid))
607 {
608 ✗ relation_close(index, AccessShareLock);
609 ✗ PG_RETURN_NULL();
610 }
611
612 85 LeafEntry *leaves;
613 85 int nleaves =
614 85 collect_leaf_entries(index, first_centroid, nlevels, dim, &leaves);
615
616 /* Walk each posting chain */
617
2/2
✓ Branch 1 taken 2851 times.
✓ Branch 2 taken 85 times.
2936 for (int c = 0; c < nleaves; c++)
618 {
619 2851 PostingRowCtx ctx = {.rsinfo = rsinfo, .dim = dim};
620
621 2851 prism_posting_chain_walk(
622 2851 st, leaves[c].posting_head, emit_posting_row, &ctx);
623 }
624
625 85 pfree(leaves);
626 85 relation_close(index, AccessShareLock);
627
628 85 PG_RETURN_NULL();
629 }
630
631 /* ----------------------------------------------------------------
632 * prism_tids_clusters(regclass, tid[])
633 *
634 * Diagnostic: for each input heap TID, return which cluster(s) it is
635 * stored in (primary + any SOAR/boundary replica). One pass over all
636 * leaf posting lists; emits (tid, cluster_id) for matched TIDs only.
637 * Lets a caller compare "where the true nearest neighbors live" against
638 * "which clusters a query scans". Owner-only: a SELECT grant must not be
639 * enough to confirm which TIDs the index holds.
640 * ---------------------------------------------------------------- */
641 static int
642 52373 cmp_u64(const void *a, const void *b)
643 {
644 52373 uint64 x = *(const uint64 *)a, y = *(const uint64 *)b;
645
2/2
✓ Branch 0 taken 25494 times.
✓ Branch 1 taken 22883 times.
48377 return (x > y) - (x < y);
646 }
647
648 /* Emit (tid, cluster_id) if tid is one of the sorted target keys. */
649 static void
650 7163 emit_tid_cluster(
651 ReturnSetInfo *rsinfo,
652 const uint64 *keys,
653 int nk,
654 ItemPointer tid,
655 uint32_t cluster_id)
656 {
657 7163 uint64 enc = prism_posting_encode_tid(tid);
658
2/2
✓ Branch 0 taken 2000 times.
✓ Branch 1 taken 5163 times.
14326 if (bsearch(&enc, keys, nk, sizeof(uint64), cmp_u64) == NULL)
659 2000 return;
660
661 5163 Datum values[2];
662 5163 bool nulls[2] = {0};
663 5163 ItemPointer out = palloc(sizeof(ItemPointerData));
664 5163 *out = *tid;
665 5163 values[0] = PointerGetDatum(out);
666 5163 values[1] = Int32GetDatum((int32)cluster_id);
667 5163 tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
668 }
669
670 Datum
671 7 vs_tids_clusters(PG_FUNCTION_ARGS)
672 {
673 7 Oid indexoid = PG_GETARG_OID(0);
674 7 ArrayType *arr = PG_GETARG_ARRAYTYPE_P(1);
675 7 ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo;
676
677 7 InitMaterializedSRF(fcinfo, 0);
678
679 /* Deconstruct tid[] into a sorted array of encoded uint64 keys. */
680 7 Datum *elems;
681 7 bool *elnulls;
682 7 int nelems;
683 7 deconstruct_array(
684 arr,
685 TIDOID,
686 sizeof(ItemPointerData),
687 false,
688 TYPALIGN_SHORT,
689 &elems,
690 &elnulls,
691 &nelems);
692 7 uint64 *keys = palloc(Max(nelems, 1) * sizeof(uint64));
693 7 int nk = 0;
694
2/2
✓ Branch 1 taken 4003 times.
✓ Branch 2 taken 7 times.
4010 for (int i = 0; i < nelems; i++)
695 {
696
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4003 times.
4003 if (elnulls[i])
697 ✗ continue;
698 4003 keys[nk++] = prism_posting_encode_tid(
699 4003 (ItemPointer)DatumGetPointer(elems[i]));
700 }
701 7 qsort(keys, nk, sizeof(uint64), cmp_u64);
702
703 7 Relation index = relation_open(indexoid, AccessShareLock);
704
705 7 require_index_owner(index, AccessShareLock);
706
707 5 VsPgStorage store;
708 5 VsStorage *st = &store.base;
709
710 5 vs_pg_storage_init_inspect(&store, index);
711
712 /*
713 * Through a local: PageGetSpecialPointer is a macro that evaluates its
714 * argument three times, and reading a page is not free of side effects
715 * -- inlining the read pins the buffer once per evaluation.
716 */
717 5 Page meta_page = vs_storage_read_page(st, 0);
718 5 const PrismMetaPage *meta = (const PrismMetaPage *)PageGetSpecialPointer(
719 meta_page);
720 5 Dimension dim = (Dimension)meta->dim;
721 5 vs_storage_release_page(st, 0);
722
723 /* Scan every block and pick out posting pages directly (identified by
724 * page_id), rather than walking the centroid tree to find posting
725 * heads — that avoids any dependency on the leaf-enumeration path and
726 * each posting page already carries its cluster_id. */
727 5 BlockNumber nblocks = RelationGetNumberOfBlocks(index);
728
2/2
✓ Branch 1 taken 222 times.
✓ Branch 2 taken 5 times.
227 for (BlockNumber blkno = 1; nk > 0 && blkno < nblocks; blkno++)
729 {
730 222 Page page = vs_storage_read_page(st, blkno);
731
732
2/2
✓ Branch 1 taken 217 times.
✓ Branch 2 taken 5 times.
222 if (prism_page_is_posting(page))
733 {
734 217 const PrismPostingPageOpaque *op = prism_posting_opaque(page);
735 217 char *content = prism_posting_page_content(page, dim);
736 217 uint32_t count = op->entry_count;
737
738 217 prism_posting_check_count(blkno, op, dim);
739
740
2/2
✓ Branch 0 taken 93 times.
✓ Branch 1 taken 124 times.
217 if (op->flags & PRISM_POSTING_PAGE_FASTSCAN)
741 {
742 /* SoA: TIDs packed per group of VS_FASTSCAN_GROUP. */
743 93 uint32_t ngroups = (count + VS_FASTSCAN_GROUP - 1) /
744 VS_FASTSCAN_GROUP;
745
2/2
✓ Branch 0 taken 151 times.
✓ Branch 1 taken 93 times.
244 for (uint32_t g = 0; g < ngroups; g++)
746 {
747 151 ItemPointerData *tids =
748 151 prism_fastscan_group_tids(content, g, dim);
749 151 uint32_t gc = count - g * VS_FASTSCAN_GROUP;
750 151 if (gc > VS_FASTSCAN_GROUP)
751 gc = VS_FASTSCAN_GROUP;
752
2/2
✓ Branch 0 taken 3163 times.
✓ Branch 1 taken 151 times.
3314 for (uint32_t v = 0; v < gc; v++)
753 3163 emit_tid_cluster(
754 3163 rsinfo, keys, nk, &tids[v], op->cluster_id);
755 }
756 }
757 else
758 {
759 /* AoS: the TID is at the head of each entry. */
760
2/2
✓ Branch 0 taken 4000 times.
✓ Branch 1 taken 124 times.
4124 for (uint32_t i = 0; i < count; i++)
761 {
762 4000 PrismPostingEntryHeader *e =
763 4000 prism_posting_entry_at(content, i, dim);
764 4000 emit_tid_cluster(
765 4000 rsinfo, keys, nk, &e->meta.tid, op->cluster_id);
766 }
767 }
768 }
769 222 vs_storage_release_page(st, blkno);
770 }
771
772 5 relation_close(index, AccessShareLock);
773 5 PG_RETURN_NULL();
774 }
775
776 /* ----------------------------------------------------------------
777 * prism_index_settings(regclass)
778 *
779 * Returns one (name, setting, source) row per effective index
780 * setting, with automatic values resolved to what the build (or the
781 * current session) actually uses. The source column tells where the
782 * value came from:
783 *
784 * 'option' -- reloption set explicitly at CREATE INDEX
785 * 'auto' -- resolved from an automatic default (nlist from the
786 * row count, formats from metric/dimension, nprobe
787 * from nlist)
788 * 'default' -- reloption default in effect
789 * 'column' -- from the indexed column definition
790 * 'opclass' -- from the operator class
791 * 'derived' -- computed from other settings
792 * 'session' -- a session GUC overriding the index setting
793 *
794 * Values the build persists (nlist, fan_out, nlevels, formats) come
795 * from the metadata page and are authoritative for the index as
796 * built. Options only consumed during the build but not persisted
797 * (soar_lambda, boundary_epsilon, kmeans_nredo) are read from the
798 * catalog, so they reflect the build only as long as they have not
799 * been changed with ALTER INDEX ... SET afterwards.
800 * ---------------------------------------------------------------- */
801
802 /* Metric name lookup (indexed by DistanceMetric); the names match
803 * the vector_<metric>_ops opclass names. */
804 static const char *metric_names[] = {
805 [DISTANCE_L2] = "l2",
806 [DISTANCE_INNER_PRODUCT] = "ip",
807 [DISTANCE_COSINE] = "cosine",
808 };
809
810 /* The reloptions explicitly set on a relation, as a list of DefElem.
811 * Unlike rd_options this excludes defaults, so it tells apart "set
812 * to the default value" from "defaulted". Caller must free with
813 * list_free_deep. */
814 static List *
815 31 explicit_reloptions(Oid relid)
816 {
817 31 HeapTuple tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
818
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 if (!HeapTupleIsValid(tuple))
819 ✗ elog(ERROR, "cache lookup failed for relation %u", relid);
820
821 31 bool isnull;
822 31 Datum datum =
823 31 SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, &isnull);
824
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
31 List *options = isnull ? NIL : untransformRelOptions(datum);
825 31 ReleaseSysCache(tuple);
826 31 return options;
827 }
828
829 static bool
830 300 reloption_is_set(const List *options, const char *name)
831 {
832 300 ListCell *lc;
833
4/4
✓ Branch 0 taken 674 times.
✓ Branch 1 taken 39 times.
✓ Branch 2 taken 463 times.
✓ Branch 3 taken 211 times.
713 foreach (lc, options)
834 {
835 463 const DefElem *def = lfirst_node(DefElem, lc);
836
2/2
✓ Branch 0 taken 413 times.
✓ Branch 1 taken 50 times.
463 if (strcmp(def->defname, name) == 0)
837 return true;
838 }
839 return false;
840 }
841
842 static void
843 527 settings_row(
844 ReturnSetInfo *rsinfo,
845 const char *name,
846 const char *setting,
847 const char *source)
848 {
849 527 Datum values[3];
850 527 bool nulls[3] = {0};
851
852 527 values[0] = CStringGetTextDatum(name);
853 527 values[1] = CStringGetTextDatum(setting);
854 527 values[2] = CStringGetTextDatum(source);
855 527 tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
856 527 }
857
858 Datum
859 33 vs_index_settings(PG_FUNCTION_ARGS)
860 {
861 33 Oid indexoid = PG_GETARG_OID(0);
862 33 ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo;
863
864 33 InitMaterializedSRF(fcinfo, 0);
865
866 33 Relation index = relation_open(indexoid, AccessShareLock);
867
868
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
33 if (index->rd_rel->relkind != RELKIND_INDEX)
869 {
870 ✗ relation_close(index, AccessShareLock);
871 ✗ ereport(ERROR,
872 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
873 errmsg("\"%s\" is not an index",
874 RelationGetRelationName(index))));
875 }
876
877 33 require_index_select(index, AccessShareLock);
878
879 32 VsPgStorage store;
880 32 VsStorage *st = &store.base;
881
882 32 vs_pg_storage_init_inspect(&store, index);
883
884 /*
885 * Through a local: PageGetSpecialPointer is a macro that evaluates its
886 * argument three times, and reading a page is not free of side effects
887 * -- inlining the read pins the buffer once per evaluation.
888 */
889 32 Page meta_page = vs_storage_read_page(st, 0);
890 32 const PrismMetaPage *meta = (const PrismMetaPage *)PageGetSpecialPointer(
891 meta_page);
892
893
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 31 times.
32 if (meta->magic != PRISM_META_MAGIC)
894 {
895 1 vs_storage_release_page(st, 0);
896 1 relation_close(index, AccessShareLock);
897
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 ereport(ERROR,
898 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
899 errmsg("\"%s\" is not a prism index",
900 RelationGetRelationName(index))));
901 }
902
903 31 Dimension dim = meta->dim;
904 31 uint8_t nlevels = meta->nlevels;
905 31 PrismCentroidFormat centroid_format = (PrismCentroidFormat)
906 31 meta->centroid_format;
907 31 uint32_t nlist = meta->nlist;
908 31 DistanceMetric metric = (DistanceMetric)meta->metric;
909 31 uint8_t fan_out = meta->fan_out;
910 31 bool fastscan = (meta->flags & PRISM_META_FLAG_FASTSCAN) != 0;
911 31 uint32_t ncentroid_pages = meta->ncentroid_pages;
912 31 BlockNumber first_posting = meta->first_posting;
913 31 vs_storage_release_page(st, 0);
914
915 31 const PrismOptions *opts = (const PrismOptions *)index->rd_options;
916 31 List *set = explicit_reloptions(indexoid);
917
918 /* Index definition */
919 31 settings_row(rsinfo, "dim", psprintf("%d", dim), "column");
920 31 settings_row(rsinfo, "metric", metric_names[metric], "opclass");
921
922 /* Build-resolved shape, from the metadata page */
923
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 6 times.
62 settings_row(
924 rsinfo,
925 "nlist",
926 31 psprintf("%u", nlist),
927 31 reloption_is_set(set, "nlist") ? "option" : "auto");
928
2/2
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 2 times.
62 settings_row(
929 rsinfo,
930 "fan_out",
931 31 psprintf("%u", (uint32_t)fan_out),
932 31 reloption_is_set(set, "fan_out") ? "option" : "auto");
933 31 settings_row(
934 31 rsinfo, "nlevels", psprintf("%u", (uint32_t)nlevels), "derived");
935 /* The resting list size nlist was derived from, and the page target
936 * behind it -- resolved, so the effective default shows rather than the
937 * 0 that means "derive". */
938
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
62 settings_row(
939 rsinfo,
940 "target_pages",
941
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
58 psprintf(
942 "%u",
943
1/2
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
27 (opts != NULL && opts->target_pages > 0)
944 ? (uint32_t)opts->target_pages
945 : (uint32_t)PRISM_DEFAULT_TARGET_PAGES),
946 31 reloption_is_set(set, "target_pages") ? "option" : "default");
947 31 settings_row(
948 rsinfo,
949 "target_entries",
950
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
58 psprintf(
951 "%u",
952 prism_target_entries_per_dim(
953 dim,
954 27 (opts != NULL) ? (uint32_t)opts->target_pages
955 : 0)),
956 "derived");
957 31 settings_row(
958 rsinfo,
959 "centroid_pages",
960 31 psprintf("%u", ncentroid_pages),
961 "maintained");
962 31 settings_row(
963 31 rsinfo, "first_posting", psprintf("%u", first_posting), "derived");
964 /*
965 * The posting page count the scan cost estimate prices, from the same
966 * helper it calls and the same page count: get_relation_info fills
967 * IndexOptInfo.pages from RelationGetNumberOfBlocks too, so this and the
968 * planner's figure agree without waiting on ANALYZE.
969 */
970 31 settings_row(
971 rsinfo,
972 "posting_pages",
973
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 psprintf(
974 "%.0f",
975 prism_index_posting_pages(
976 31 (double)RelationGetNumberOfBlocks(index),
977 (double)ncentroid_pages)),
978 "derived");
979
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 9 times.
62 settings_row(
980 rsinfo,
981 "centroid_format",
982 centroid_format_names[centroid_format],
983
2/2
✓ Branch 1 taken 18 times.
✓ Branch 2 taken 4 times.
53 (reloption_is_set(set, "centroid_compression") ||
984 22 reloption_is_set(set, "centroid_fastscan"))
985 ? "option"
986 : "auto");
987
4/4
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 18 times.
70 settings_row(
988 rsinfo,
989 "fastscan",
990 fastscan ? "on" : "off",
991 31 reloption_is_set(set, "fastscan") ? "option" : "auto");
992
993 /* Build options not persisted in the index: current catalog
994 * values, with reloption defaults filled in */
995
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 3 times.
62 settings_row(
996 rsinfo,
997 "soar_lambda",
998
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
31 psprintf(
999 "%g",
1000 (opts != NULL) ? opts->soar_lambda
1001 : PRISM_DEFAULT_SOAR_LAMBDA),
1002 31 reloption_is_set(set, "soar_lambda") ? "option" : "default");
1003
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 3 times.
62 settings_row(
1004 rsinfo,
1005 "boundary_epsilon",
1006
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
31 psprintf(
1007 "%g",
1008 (opts != NULL) ? opts->boundary_epsilon
1009 : PRISM_DEFAULT_BOUNDARY_EPSILON),
1010 31 reloption_is_set(set, "boundary_epsilon") ? "option" : "default");
1011
2/2
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 2 times.
62 settings_row(
1012 rsinfo,
1013 "kmeans_nredo",
1014
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
31 psprintf("%d", (opts != NULL) ? opts->kmeans_nredo : 1),
1015 31 reloption_is_set(set, "kmeans_nredo") ? "option" : "default");
1016
1017 /* Query-time settings whose effective value depends on this
1018 * index: the session GUC wins when set, otherwise the index
1019 * option or automatic resolution applies. */
1020
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 30 times.
31 if (prism_distance_mode != VS_DISTANCE_MODE_DEFAULT)
1021 1 settings_row(
1022 rsinfo,
1023 "distance_mode",
1024 vs_distance_mode_name((VsDistanceMode)prism_distance_mode),
1025 "session");
1026 else
1027
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 2 times.
60 settings_row(
1028 rsinfo,
1029 "distance_mode",
1030 vs_distance_mode_name(PrismGetDistanceMode(index)),
1031 30 reloption_is_set(set, "distance_mode") ? "option" : "default");
1032 31 settings_row(
1033 rsinfo,
1034 "nprobe",
1035
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 7 times.
55 psprintf(
1036 "%u",
1037 (prism_nprobe > 0) ? (uint32_t)prism_nprobe
1038 24 : prism_auto_nprobe(nlist)),
1039
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 7 times.
31 (prism_nprobe > 0) ? "session" : "auto");
1040
1041 31 list_free_deep(set);
1042 31 relation_close(index, AccessShareLock);
1043 31 PG_RETURN_NULL();
1044 }
1045