GCC Code Coverage Report


Directory: src/
File: src/pg/inspect.c
Date: 2026-09-30 11:11:31
Exec Total Coverage
Lines: 389 412 94.4%
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 22744 check_centroid_count(
60 BlockNumber blkno,
61 uint32_t count,
62 Dimension dim,
63 PrismCentroidFormat fmt)
64 {
65 22744 uint32_t max_entries = prism_centroid_max_entries_fmt(dim, fmt);
66
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22744 times.
22744 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 22744 }
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 162 require_index_select(Relation index, LOCKMODE lockmode)
93 {
94 162 Oid heaprelid = IndexGetRelation(RelationGetRelid(index), false);
95 162 AclResult aclresult =
96 162 pg_class_aclcheck(heaprelid, GetUserId(), ACL_SELECT);
97
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 159 times.
162 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 159 }
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 92397 grow_if_full(void *arr, int len, int *cap, Size elem_size)
133 {
134 92397 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 405 collect_leaf_entries(
147 Relation index,
148 BlockNumber first_centroid,
149 uint8_t nlevels,
150 Dimension dim,
151 LeafEntry **out)
152 {
153 405 VsPgStorage store;
154 405 VsStorage *st = &store.base;
155
156 405 vs_pg_storage_init_inspect(&store, index);
157
158 405 int wl_cap = 64;
159 405 int wl_len = 0;
160 405 int wl_head = 0;
161 405 BlockNumber *wl = palloc(wl_cap * sizeof(BlockNumber));
162
163 405 int leaves_cap = 64;
164 405 int leaves_len = 0;
165 405 LeafEntry *leaves = palloc(leaves_cap * sizeof(LeafEntry));
166
167 405 wl[wl_len++] = first_centroid;
168
169
2/2
✓ Branch 0 taken 22434 times.
✓ Branch 1 taken 405 times.
22839 while (wl_head < wl_len)
170 {
171 22434 BlockNumber blkno = wl[wl_head++];
172
173 22434 Page page = vs_storage_read_page(st, blkno);
174
175 22434 const PrismCentroidPageOpaque *opaque = PRISM_CENTROID_OPAQUE(page);
176 22434 uint16_t nentries = opaque->entry_count;
177 22434 PrismCentroidFormat fmt =
178 22434 (PrismCentroidFormat)(opaque->flags & PRISM_CENTROID_FMT_MASK);
179 22434 bool is_leaf_page = (opaque->level == nlevels - 1);
180
181 22434 check_centroid_count(blkno, nentries, dim, fmt);
182
183
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22434 times.
22434 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 92397 times.
✓ Branch 1 taken 22434 times.
114831 for (uint16_t i = 0; i < nentries; i++)
190 {
191 92397 BlockNumber child;
192 92397 bool is_leaf;
193
194
2/2
✓ Branch 0 taken 90602 times.
✓ Branch 1 taken 1795 times.
92397 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 90602 char *content = (char *)PageGetContents(page);
199 90602 uint32_t g = i / VS_FASTSCAN_GROUP;
200 90602 uint32_t slot = i % VS_FASTSCAN_GROUP;
201 90602 BlockNumber *grp =
202 90602 prism_centroid_fastscan_group_child(content, g, dim);
203 90602 child = grp[slot];
204 90602 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 92397 times.
92397 if (!BlockNumberIsValid(child))
215 ✗ continue;
216
217
2/2
✓ Branch 0 taken 70368 times.
✓ Branch 1 taken 22029 times.
92397 if (is_leaf)
218 {
219
2/2
✓ Branch 0 taken 526 times.
✓ Branch 1 taken 69842 times.
70368 leaves = grow_if_full(
220 leaves, leaves_len, &leaves_cap, sizeof(*leaves));
221 70368 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 22434 vs_storage_release_page(st, blkno);
235 }
236
237 405 pfree(wl);
238 405 *out = leaves;
239 405 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 1471 times.
✓ Branch 1 taken 205 times.
1676 for (uint16_t i = 0; i < nentries; i++)
350 {
351 1471 uint32_t g = i / VS_FASTSCAN_GROUP;
352 1471 uint32_t slot = i % VS_FASTSCAN_GROUP;
353 1471 BlockNumber *child =
354 1471 prism_centroid_fastscan_group_child(content, g, dim);
355
356 1471 Datum values[7];
357 1471 bool nulls[7] = {0};
358
359 1471 values[0] = Int32GetDatum((int32)blkno);
360 1471 values[1] = Int16GetDatum((int16)i);
361 1471 values[2] = Int16GetDatum((int16)opaque->level);
362
1/2
✓ Branch 1 taken 1471 times.
✗ Branch 2 not taken.
1471 values[3] = CStringGetTextDatum(centroid_format_names[fmt]);
363
364
1/2
✓ Branch 0 taken 1471 times.
✗ Branch 1 not taken.
1471 if (BlockNumberIsValid(child[slot]))
365 1471 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 1471 nulls[5] = true;
374
2/2
✓ Branch 0 taken 193 times.
✓ Branch 1 taken 1278 times.
1471 values[6] = BoolGetDatum(is_leaf_page);
375
376 1471 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 1278 times.
✓ Branch 2 taken 193 times.
✗ Branch 3 not taken.
1471 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 2931 emit_posting_row(PrismPostingChainPos *pos, void *state)
474 {
475 2931 PostingRowCtx *ctx = state;
476 2931 Dimension dim = ctx->dim;
477 2931 const PrismPostingPageOpaque *op = prism_posting_opaque(pos->page);
478 2931 bool is_first = (op->flags & PRISM_POSTING_PAGE_FIRST) != 0;
479 2931 bool tombstoned = (op->flags & PRISM_POSTING_PAGE_TOMBSTONED) != 0;
480 2931 bool is_fastscan = (op->flags & PRISM_POSTING_PAGE_FASTSCAN) != 0;
481
482 2931 Datum values[10];
483 2931 bool nulls[10] = {0};
484
485
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 2847 times.
2931 values[0] = Int32GetDatum((int32)pos->blkno);
486 2931 values[1] = Int32GetDatum((int32)op->cluster_id);
487
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 2847 times.
2931 values[2] = BoolGetDatum(is_first);
488
2/2
✓ Branch 0 taken 2913 times.
✓ Branch 1 taken 18 times.
2931 values[3] = BoolGetDatum(tombstoned);
489
2/2
✓ Branch 0 taken 1410 times.
✓ Branch 1 taken 1521 times.
2931 values[4] = Int32GetDatum((int32)op->entry_count);
490
491
2/2
✓ Branch 0 taken 1410 times.
✓ Branch 1 taken 1521 times.
2931 if (!is_fastscan)
492 {
493 /* AoS entries carry a per-entry DELETED flag; count them. FASTSCAN
494 * packs entries into SIMD groups with no per-entry state (deletion
495 * is page-granular there), so dead_count is NULL for fastscan
496 * pages. */
497 1410 char *content = prism_posting_page_content(pos->page, dim);
498 1410 int32 dead = 0;
499
500 1410 prism_posting_check_count(pos->blkno, op, dim);
501
2/2
✓ Branch 1 taken 74430 times.
✓ Branch 2 taken 1410 times.
75840 for (uint32_t i = 0; i < op->entry_count; i++)
502 {
503 74430 const PrismPostingEntryHeader *h =
504
2/2
✓ Branch 0 taken 1037 times.
✓ Branch 1 taken 73393 times.
74430 prism_posting_entry_at(content, i, dim);
505
506
2/2
✓ Branch 0 taken 1037 times.
✓ Branch 1 taken 73393 times.
74430 if (h->meta.flags & PRISM_POSTING_FLAG_DELETED)
507 1037 dead++;
508 }
509 1410 values[5] = Int32GetDatum(dead);
510 }
511 else
512 1521 nulls[5] = true;
513
514
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 2847 times.
2931 values[6] = Int32GetDatum((int32)op->max_entries);
515
516
2/2
✓ Branch 0 taken 84 times.
✓ Branch 1 taken 2847 times.
2931 if (BlockNumberIsValid(pos->next))
517 84 values[7] = Int32GetDatum((int32)pos->next);
518 else
519 2847 nulls[7] = true;
520
521
2/2
✓ Branch 0 taken 1410 times.
✓ Branch 1 taken 1521 times.
2931 values[8] = Int32GetDatum(ctx->chain_pos++);
522
2/2
✓ Branch 0 taken 1410 times.
✓ Branch 1 taken 1521 times.
4341 values[9] = CStringGetTextDatum(is_fastscan ? "fastscan" : "aos");
523
524 2931 tuplestore_putvalues(
525 2931 ctx->rsinfo->setResult, ctx->rsinfo->setDesc, values, nulls);
526 2931 return true;
527 }
528
529 /* ----------------------------------------------------------------
530 * prism_posting_pages(regclass)
531 *
532 * Returns one row per posting page: blkno, cluster_id, is_first,
533 * tombstoned, entry_count, dead_count, max_entries, next_blkno,
534 * chain_pos, format. Tombstoned (all-dead, but still linked) pages stay
535 * in the output so bloat is visible; filter with WHERE NOT tombstoned
536 * for live pages. dead_count is the per-entry DELETED tally for AoS
537 * pages and NULL for fastscan pages (no per-entry state).
538 *
539 * Walks all posting chains by finding leaf centroids (which store
540 * posting_head block numbers) and following next_blkno links.
541 * ---------------------------------------------------------------- */
542 Datum
543 86 vs_posting_pages(PG_FUNCTION_ARGS)
544 {
545 86 Oid indexoid = PG_GETARG_OID(0);
546 86 ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo;
547
548 86 InitMaterializedSRF(fcinfo, 0);
549
550 86 Relation index = relation_open(indexoid, AccessShareLock);
551
552
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 86 times.
86 if (index->rd_rel->relkind != RELKIND_INDEX)
553 {
554 ✗ relation_close(index, AccessShareLock);
555 ✗ ereport(ERROR,
556 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
557 errmsg("\"%s\" is not an index",
558 RelationGetRelationName(index))));
559 }
560
561 86 require_index_select(index, AccessShareLock);
562
563 85 VsPgStorage store;
564 85 VsStorage *st = &store.base;
565
566 85 vs_pg_storage_init_inspect(&store, index);
567
568 85 Page meta_page = vs_storage_read_page(st, 0);
569
570 85 const PrismMetaPage *meta = (const PrismMetaPage *)PageGetSpecialPointer(
571 meta_page);
572
573
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 84 times.
85 if (meta->magic != PRISM_META_MAGIC)
574 {
575 1 vs_storage_release_page(st, 0);
576 1 relation_close(index, AccessShareLock);
577
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 ereport(ERROR,
578 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
579 errmsg("\"%s\" is not a prism index",
580 RelationGetRelationName(index))));
581 }
582
583 84 BlockNumber first_centroid = meta->first_centroid;
584 84 Dimension dim = meta->dim;
585 84 uint8_t nlevels = meta->nlevels;
586 84 vs_storage_release_page(st, 0);
587
588
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 84 times.
84 if (!BlockNumberIsValid(first_centroid))
589 {
590 ✗ relation_close(index, AccessShareLock);
591 ✗ PG_RETURN_NULL();
592 }
593
594 84 LeafEntry *leaves;
595 84 int nleaves =
596 84 collect_leaf_entries(index, first_centroid, nlevels, dim, &leaves);
597
598 /* Walk each posting chain */
599
2/2
✓ Branch 1 taken 2847 times.
✓ Branch 2 taken 84 times.
2931 for (int c = 0; c < nleaves; c++)
600 {
601 2847 PostingRowCtx ctx = {.rsinfo = rsinfo, .dim = dim};
602
603 2847 prism_posting_chain_walk(
604 2847 st, leaves[c].posting_head, emit_posting_row, &ctx);
605 }
606
607 84 pfree(leaves);
608 84 relation_close(index, AccessShareLock);
609
610 84 PG_RETURN_NULL();
611 }
612
613 /* ----------------------------------------------------------------
614 * prism_tids_clusters(regclass, tid[])
615 *
616 * Diagnostic: for each input heap TID, return which cluster(s) it is
617 * stored in (primary + any SOAR/boundary replica). One pass over all
618 * leaf posting lists; emits (tid, cluster_id) for matched TIDs only.
619 * Lets a caller compare "where the true nearest neighbors live" against
620 * "which clusters a query scans". Owner-only: a SELECT grant must not be
621 * enough to confirm which TIDs the index holds.
622 * ---------------------------------------------------------------- */
623 static int
624 52373 cmp_u64(const void *a, const void *b)
625 {
626 52373 uint64 x = *(const uint64 *)a, y = *(const uint64 *)b;
627
2/2
✓ Branch 0 taken 25494 times.
✓ Branch 1 taken 22883 times.
48377 return (x > y) - (x < y);
628 }
629
630 /* Emit (tid, cluster_id) if tid is one of the sorted target keys. */
631 static void
632 7163 emit_tid_cluster(
633 ReturnSetInfo *rsinfo,
634 const uint64 *keys,
635 int nk,
636 ItemPointer tid,
637 uint32_t cluster_id)
638 {
639 7163 uint64 enc = prism_posting_encode_tid(tid);
640
2/2
✓ Branch 0 taken 2000 times.
✓ Branch 1 taken 5163 times.
14326 if (bsearch(&enc, keys, nk, sizeof(uint64), cmp_u64) == NULL)
641 2000 return;
642
643 5163 Datum values[2];
644 5163 bool nulls[2] = {0};
645 5163 ItemPointer out = palloc(sizeof(ItemPointerData));
646 5163 *out = *tid;
647 5163 values[0] = PointerGetDatum(out);
648 5163 values[1] = Int32GetDatum((int32)cluster_id);
649 5163 tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
650 }
651
652 Datum
653 7 vs_tids_clusters(PG_FUNCTION_ARGS)
654 {
655 7 Oid indexoid = PG_GETARG_OID(0);
656 7 ArrayType *arr = PG_GETARG_ARRAYTYPE_P(1);
657 7 ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo;
658
659 7 InitMaterializedSRF(fcinfo, 0);
660
661 /* Deconstruct tid[] into a sorted array of encoded uint64 keys. */
662 7 Datum *elems;
663 7 bool *elnulls;
664 7 int nelems;
665 7 deconstruct_array(
666 arr,
667 TIDOID,
668 sizeof(ItemPointerData),
669 false,
670 TYPALIGN_SHORT,
671 &elems,
672 &elnulls,
673 &nelems);
674 7 uint64 *keys = palloc(Max(nelems, 1) * sizeof(uint64));
675 7 int nk = 0;
676
2/2
✓ Branch 1 taken 4003 times.
✓ Branch 2 taken 7 times.
4010 for (int i = 0; i < nelems; i++)
677 {
678
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4003 times.
4003 if (elnulls[i])
679 ✗ continue;
680 4003 keys[nk++] = prism_posting_encode_tid(
681 4003 (ItemPointer)DatumGetPointer(elems[i]));
682 }
683 7 qsort(keys, nk, sizeof(uint64), cmp_u64);
684
685 7 Relation index = relation_open(indexoid, AccessShareLock);
686
687 7 require_index_owner(index, AccessShareLock);
688
689 5 VsPgStorage store;
690 5 VsStorage *st = &store.base;
691
692 5 vs_pg_storage_init_inspect(&store, index);
693
694 /*
695 * Through a local: PageGetSpecialPointer is a macro that evaluates its
696 * argument three times, and reading a page is not free of side effects
697 * -- inlining the read pins the buffer once per evaluation.
698 */
699 5 Page meta_page = vs_storage_read_page(st, 0);
700 5 const PrismMetaPage *meta = (const PrismMetaPage *)PageGetSpecialPointer(
701 meta_page);
702 5 Dimension dim = (Dimension)meta->dim;
703 5 vs_storage_release_page(st, 0);
704
705 /* Scan every block and pick out posting pages directly (identified by
706 * page_id), rather than walking the centroid tree to find posting
707 * heads — that avoids any dependency on the leaf-enumeration path and
708 * each posting page already carries its cluster_id. */
709 5 BlockNumber nblocks = RelationGetNumberOfBlocks(index);
710
2/2
✓ Branch 1 taken 222 times.
✓ Branch 2 taken 5 times.
227 for (BlockNumber blkno = 1; nk > 0 && blkno < nblocks; blkno++)
711 {
712 222 Page page = vs_storage_read_page(st, blkno);
713
714
2/2
✓ Branch 1 taken 217 times.
✓ Branch 2 taken 5 times.
222 if (prism_page_is_posting(page))
715 {
716 217 const PrismPostingPageOpaque *op = prism_posting_opaque(page);
717 217 char *content = prism_posting_page_content(page, dim);
718 217 uint32_t count = op->entry_count;
719
720 217 prism_posting_check_count(blkno, op, dim);
721
722
2/2
✓ Branch 0 taken 93 times.
✓ Branch 1 taken 124 times.
217 if (op->flags & PRISM_POSTING_PAGE_FASTSCAN)
723 {
724 /* SoA: TIDs packed per group of VS_FASTSCAN_GROUP. */
725 93 uint32_t ngroups = (count + VS_FASTSCAN_GROUP - 1) /
726 VS_FASTSCAN_GROUP;
727
2/2
✓ Branch 0 taken 151 times.
✓ Branch 1 taken 93 times.
244 for (uint32_t g = 0; g < ngroups; g++)
728 {
729 151 ItemPointerData *tids =
730 151 prism_fastscan_group_tids(content, g, dim);
731 151 uint32_t gc = count - g * VS_FASTSCAN_GROUP;
732 151 if (gc > VS_FASTSCAN_GROUP)
733 gc = VS_FASTSCAN_GROUP;
734
2/2
✓ Branch 0 taken 3163 times.
✓ Branch 1 taken 151 times.
3314 for (uint32_t v = 0; v < gc; v++)
735 3163 emit_tid_cluster(
736 3163 rsinfo, keys, nk, &tids[v], op->cluster_id);
737 }
738 }
739 else
740 {
741 /* AoS: the TID is at the head of each entry. */
742
2/2
✓ Branch 0 taken 4000 times.
✓ Branch 1 taken 124 times.
4124 for (uint32_t i = 0; i < count; i++)
743 {
744 4000 PrismPostingEntryHeader *e =
745 4000 prism_posting_entry_at(content, i, dim);
746 4000 emit_tid_cluster(
747 4000 rsinfo, keys, nk, &e->meta.tid, op->cluster_id);
748 }
749 }
750 }
751 222 vs_storage_release_page(st, blkno);
752 }
753
754 5 relation_close(index, AccessShareLock);
755 5 PG_RETURN_NULL();
756 }
757
758 /* ----------------------------------------------------------------
759 * prism_index_settings(regclass)
760 *
761 * Returns one (name, setting, source) row per effective index
762 * setting, with automatic values resolved to what the build (or the
763 * current session) actually uses. The source column tells where the
764 * value came from:
765 *
766 * 'option' -- reloption set explicitly at CREATE INDEX
767 * 'auto' -- resolved from an automatic default (nlist from the
768 * row count, formats from metric/dimension, nprobe
769 * from nlist)
770 * 'default' -- reloption default in effect
771 * 'column' -- from the indexed column definition
772 * 'opclass' -- from the operator class
773 * 'derived' -- computed from other settings
774 * 'session' -- a session GUC overriding the index setting
775 *
776 * Values the build persists (nlist, fan_out, nlevels, formats) come
777 * from the metadata page and are authoritative for the index as
778 * built. Options only consumed during the build but not persisted
779 * (soar_lambda, boundary_epsilon, kmeans_nredo) are read from the
780 * catalog, so they reflect the build only as long as they have not
781 * been changed with ALTER INDEX ... SET afterwards.
782 * ---------------------------------------------------------------- */
783
784 /* Metric name lookup (indexed by DistanceMetric); the names match
785 * the vector_<metric>_ops opclass names. */
786 static const char *metric_names[] = {
787 [DISTANCE_L2] = "l2",
788 [DISTANCE_INNER_PRODUCT] = "ip",
789 [DISTANCE_COSINE] = "cosine",
790 };
791
792 /* The reloptions explicitly set on a relation, as a list of DefElem.
793 * Unlike rd_options this excludes defaults, so it tells apart "set
794 * to the default value" from "defaulted". Caller must free with
795 * list_free_deep. */
796 static List *
797 31 explicit_reloptions(Oid relid)
798 {
799 31 HeapTuple tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
800
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 if (!HeapTupleIsValid(tuple))
801 ✗ elog(ERROR, "cache lookup failed for relation %u", relid);
802
803 31 bool isnull;
804 31 Datum datum =
805 31 SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, &isnull);
806
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
31 List *options = isnull ? NIL : untransformRelOptions(datum);
807 31 ReleaseSysCache(tuple);
808 31 return options;
809 }
810
811 static bool
812 300 reloption_is_set(const List *options, const char *name)
813 {
814 300 ListCell *lc;
815
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)
816 {
817 463 const DefElem *def = lfirst_node(DefElem, lc);
818
2/2
✓ Branch 0 taken 413 times.
✓ Branch 1 taken 50 times.
463 if (strcmp(def->defname, name) == 0)
819 return true;
820 }
821 return false;
822 }
823
824 static void
825 527 settings_row(
826 ReturnSetInfo *rsinfo,
827 const char *name,
828 const char *setting,
829 const char *source)
830 {
831 527 Datum values[3];
832 527 bool nulls[3] = {0};
833
834 527 values[0] = CStringGetTextDatum(name);
835 527 values[1] = CStringGetTextDatum(setting);
836 527 values[2] = CStringGetTextDatum(source);
837 527 tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
838 527 }
839
840 Datum
841 33 vs_index_settings(PG_FUNCTION_ARGS)
842 {
843 33 Oid indexoid = PG_GETARG_OID(0);
844 33 ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo;
845
846 33 InitMaterializedSRF(fcinfo, 0);
847
848 33 Relation index = relation_open(indexoid, AccessShareLock);
849
850
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
33 if (index->rd_rel->relkind != RELKIND_INDEX)
851 {
852 ✗ relation_close(index, AccessShareLock);
853 ✗ ereport(ERROR,
854 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
855 errmsg("\"%s\" is not an index",
856 RelationGetRelationName(index))));
857 }
858
859 33 require_index_select(index, AccessShareLock);
860
861 32 VsPgStorage store;
862 32 VsStorage *st = &store.base;
863
864 32 vs_pg_storage_init_inspect(&store, index);
865
866 /*
867 * Through a local: PageGetSpecialPointer is a macro that evaluates its
868 * argument three times, and reading a page is not free of side effects
869 * -- inlining the read pins the buffer once per evaluation.
870 */
871 32 Page meta_page = vs_storage_read_page(st, 0);
872 32 const PrismMetaPage *meta = (const PrismMetaPage *)PageGetSpecialPointer(
873 meta_page);
874
875
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 31 times.
32 if (meta->magic != PRISM_META_MAGIC)
876 {
877 1 vs_storage_release_page(st, 0);
878 1 relation_close(index, AccessShareLock);
879
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 ereport(ERROR,
880 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
881 errmsg("\"%s\" is not a prism index",
882 RelationGetRelationName(index))));
883 }
884
885 31 Dimension dim = meta->dim;
886 31 uint8_t nlevels = meta->nlevels;
887 31 PrismCentroidFormat centroid_format = (PrismCentroidFormat)
888 31 meta->centroid_format;
889 31 uint32_t nlist = meta->nlist;
890 31 DistanceMetric metric = (DistanceMetric)meta->metric;
891 31 uint8_t fan_out = meta->fan_out;
892 31 bool fastscan = (meta->flags & PRISM_META_FLAG_FASTSCAN) != 0;
893 31 uint32_t ncentroid_pages = meta->ncentroid_pages;
894 31 BlockNumber first_posting = meta->first_posting;
895 31 vs_storage_release_page(st, 0);
896
897 31 const PrismOptions *opts = (const PrismOptions *)index->rd_options;
898 31 List *set = explicit_reloptions(indexoid);
899
900 /* Index definition */
901 31 settings_row(rsinfo, "dim", psprintf("%d", dim), "column");
902 31 settings_row(rsinfo, "metric", metric_names[metric], "opclass");
903
904 /* Build-resolved shape, from the metadata page */
905
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 6 times.
62 settings_row(
906 rsinfo,
907 "nlist",
908 31 psprintf("%u", nlist),
909 31 reloption_is_set(set, "nlist") ? "option" : "auto");
910
2/2
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 2 times.
62 settings_row(
911 rsinfo,
912 "fan_out",
913 31 psprintf("%u", (uint32_t)fan_out),
914 31 reloption_is_set(set, "fan_out") ? "option" : "auto");
915 31 settings_row(
916 31 rsinfo, "nlevels", psprintf("%u", (uint32_t)nlevels), "derived");
917 /* The resting list size nlist was derived from, and the page target
918 * behind it -- resolved, so the effective default shows rather than the
919 * 0 that means "derive". */
920
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
62 settings_row(
921 rsinfo,
922 "target_pages",
923
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
58 psprintf(
924 "%u",
925
1/2
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
27 (opts != NULL && opts->target_pages > 0)
926 ? (uint32_t)opts->target_pages
927 : (uint32_t)PRISM_DEFAULT_TARGET_PAGES),
928 31 reloption_is_set(set, "target_pages") ? "option" : "default");
929 31 settings_row(
930 rsinfo,
931 "target_entries",
932
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
58 psprintf(
933 "%u",
934 prism_target_entries_per_dim(
935 dim,
936 27 (opts != NULL) ? (uint32_t)opts->target_pages
937 : 0)),
938 "derived");
939 31 settings_row(
940 rsinfo,
941 "centroid_pages",
942 31 psprintf("%u", ncentroid_pages),
943 "maintained");
944 31 settings_row(
945 31 rsinfo, "first_posting", psprintf("%u", first_posting), "derived");
946 /*
947 * The posting page count the scan cost estimate prices, from the same
948 * helper it calls and the same page count: get_relation_info fills
949 * IndexOptInfo.pages from RelationGetNumberOfBlocks too, so this and the
950 * planner's figure agree without waiting on ANALYZE.
951 */
952 31 settings_row(
953 rsinfo,
954 "posting_pages",
955
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 31 times.
31 psprintf(
956 "%.0f",
957 prism_index_posting_pages(
958 31 (double)RelationGetNumberOfBlocks(index),
959 (double)ncentroid_pages)),
960 "derived");
961
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 9 times.
62 settings_row(
962 rsinfo,
963 "centroid_format",
964 centroid_format_names[centroid_format],
965
2/2
✓ Branch 1 taken 18 times.
✓ Branch 2 taken 4 times.
53 (reloption_is_set(set, "centroid_compression") ||
966 22 reloption_is_set(set, "centroid_fastscan"))
967 ? "option"
968 : "auto");
969
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(
970 rsinfo,
971 "fastscan",
972 fastscan ? "on" : "off",
973 31 reloption_is_set(set, "fastscan") ? "option" : "auto");
974
975 /* Build options not persisted in the index: current catalog
976 * values, with reloption defaults filled in */
977
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 3 times.
62 settings_row(
978 rsinfo,
979 "soar_lambda",
980
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
31 psprintf(
981 "%g",
982 (opts != NULL) ? opts->soar_lambda
983 : PRISM_DEFAULT_SOAR_LAMBDA),
984 31 reloption_is_set(set, "soar_lambda") ? "option" : "default");
985
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 3 times.
62 settings_row(
986 rsinfo,
987 "boundary_epsilon",
988
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
31 psprintf(
989 "%g",
990 (opts != NULL) ? opts->boundary_epsilon
991 : PRISM_DEFAULT_BOUNDARY_EPSILON),
992 31 reloption_is_set(set, "boundary_epsilon") ? "option" : "default");
993
2/2
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 2 times.
62 settings_row(
994 rsinfo,
995 "kmeans_nredo",
996
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
31 psprintf("%d", (opts != NULL) ? opts->kmeans_nredo : 1),
997 31 reloption_is_set(set, "kmeans_nredo") ? "option" : "default");
998
999 /* Query-time settings whose effective value depends on this
1000 * index: the session GUC wins when set, otherwise the index
1001 * option or automatic resolution applies. */
1002
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 30 times.
31 if (prism_distance_mode != VS_DISTANCE_MODE_DEFAULT)
1003 1 settings_row(
1004 rsinfo,
1005 "distance_mode",
1006 vs_distance_mode_name((VsDistanceMode)prism_distance_mode),
1007 "session");
1008 else
1009
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 2 times.
60 settings_row(
1010 rsinfo,
1011 "distance_mode",
1012 vs_distance_mode_name(PrismGetDistanceMode(index)),
1013 30 reloption_is_set(set, "distance_mode") ? "option" : "default");
1014 31 settings_row(
1015 rsinfo,
1016 "nprobe",
1017
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 7 times.
55 psprintf(
1018 "%u",
1019 (prism_nprobe > 0) ? (uint32_t)prism_nprobe
1020 24 : prism_auto_nprobe(nlist)),
1021
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 7 times.
31 (prism_nprobe > 0) ? "session" : "auto");
1022
1023 31 list_free_deep(set);
1024 31 relation_close(index, AccessShareLock);
1025 31 PG_RETURN_NULL();
1026 }
1027