GCC Code Coverage Report


Directory: src/
File: src/pg/iam_handler.c
Date: 2026-09-30 11:11:31
Exec Total Coverage
Lines: 181 189 95.8%
Functions: 9 10 90.0%
Branches: 46 68 67.6%

Line Branch Exec Source
1 /*
2 * Copyright (c) 2026 Tiger Data, Inc.
3 * Licensed under the PostgreSQL License. See LICENSE for details.
4 *
5 * iam_handler.c - prism index access method handler
6 *
7 * Registers the prism index access method with PostgreSQL. Build and
8 * scan callbacks delegate to build.c and scan.c; trivial
9 * stubs for unimplemented callbacks remain here.
10 */
11
12 #include <postgres.h>
13
14 #include <access/amapi.h>
15 #include <access/reloptions.h>
16 #include <access/relscan.h>
17 #include <commands/vacuum.h>
18 #include <fmgr.h>
19 #include <storage/bufmgr.h>
20 #include <storage/lmgr.h>
21 #include <utils/float.h>
22 #include <utils/injection_point.h>
23 #include <utils/memutils.h>
24 #include <utils/selfuncs.h>
25
26 #include "algo/vecops.h"
27 #include "amcache.h"
28 #include "build.h"
29 #include "cost.h"
30 #include "index/index_base.h"
31 #include "index/posting_insert.h"
32 #include "index/query_scan.h"
33 #include "pg/bufstorage.h"
34 #include "quant/rabitq.h"
35 #include "scan.h"
36 #include "support_pg.h"
37 #include "typeinfo.h"
38 #include "types/vec32.h"
39
40 256 PG_FUNCTION_INFO_V1(prism_handler);
41
42 /* ----------------------------------------------------------------
43 * Trivial stubs (no separate file needed)
44 * ---------------------------------------------------------------- */
45
46 /*
47 * ambuildempty populates the init fork of an unlogged index so that crash
48 * recovery has a valid image to copy over the main fork. prism has no
49 * notion of a valid "empty" index image -- even a build over zero rows
50 * produces a real single-cluster tree -- so a no-op here would leave the
51 * init fork at zero blocks and let recovery wipe the whole index, metadata
52 * page included. Rather than seed the init fork with an index shape nothing
53 * else ever exercises, refuse unlogged tables outright at CREATE INDEX time.
54 */
55 static void
56 1 prism_buildempty(Relation index)
57 {
58
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 ereport(ERROR,
59 (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
60 errmsg("prism indexes do not support unlogged tables"),
61 errhint("Use a logged table, or run ALTER TABLE ... SET LOGGED "
62 "before creating the index.")));
63 }
64
65 /*
66 * Beam width used when routing an inserted vector to its nearest leaf. Wide
67 * enough that multi-level tree descent lands the true nearest leaf; we still
68 * insert into a single list (results[0]). Phase 0 does no SOAR replication on
69 * insert — that's restored in bulk at rebuild / compaction.
70 */
71 #define PRISM_INSERT_ROUTE_BEAM 8
72
73 /*
74 * How many times an insert re-routes when the head it locked turns out to have
75 * been split away. A split retires the head it replaces, so an insert that was
76 * waiting on the head's lock has to route again -- bounded, so churn cannot
77 * spin forever, and reaching the bound fails the insert rather than dropping
78 * the tuple.
79 */
80 #define PRISM_INSERT_ROUTE_ATTEMPTS 8
81
82 static bool
83 17463 prism_insert(
84 Relation index,
85 Datum *values,
86 bool *isnull,
87 ItemPointer heap_tid,
88 Relation heap,
89 IndexUniqueCheck check_unique,
90 bool index_unchanged,
91 struct IndexInfo *index_info)
92 {
93 17463 (void)heap;
94 17463 (void)check_unique;
95 17463 (void)index_info;
96
97 /* index_unchanged is deliberately ignored. PostgreSQL sets it when an
98 * UPDATE left the indexed column untouched but still had to place the
99 * new tuple version elsewhere (a non-HOT update); it is a hint for
100 * access methods that can deduplicate against the old version, not a
101 * signal that the old entry still covers the new TID. A HOT update,
102 * where the old entry does still apply, never reaches aminsert at all.
103 * Skipping the insert here left the new version unreachable through
104 * the index. NULL vectors get no entry. */
105
1/2
✓ Branch 0 taken 17463 times.
✗ Branch 1 not taken.
17463 if (isnull[0])
106 return false;
107
108 /* Per-insert scratch context: beam-search + encode allocations are freed
109 * in one shot and don't accumulate in the inserting transaction. */
110 17463 MemoryContext insert_ctx = AllocSetContextCreate(
111 CurrentMemoryContext, "prism insert", ALLOCSET_DEFAULT_SIZES);
112 17463 MemoryContext old_ctx = MemoryContextSwitchTo(insert_ctx);
113
114 /* Immutable index parameters from the per-backend cache (metapage read at
115 * most once per backend, not once per insert). The checkout is
116 * registered with the current resource owner; capture it for the
117 * release below (and for the error paths in between, which release
118 * through the owner instead). */
119 17463 ResourceOwner params_owner = CurrentResourceOwner;
120 17463 PrismIndexBase base;
121 17463 prism_index_base_init(index, &base);
122 17463 Dimension dim = base.dim;
123
124 17463 VsPgStorage storage;
125 17463 vs_pg_storage_init(&storage, index, NULL, base.metric);
126 17463 base.centroid_storage = &storage.base;
127 17463 base.posting_storage = &storage.base;
128 17463 base.page_base = NULL;
129
130 /* Inserted vector, converted to float32 when the column type is not. */
131 17463 Vec32Access input = vec32_access(
132 17463 prism_cache_type_info(index), dim, CurrentMemoryContext);
133 17463 Vec32Ref vref = vec32_read(&input, values[0]);
134
135
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17463 times.
17463 if (vref.dim != dim)
136 ✗ ereport(ERROR,
137 (errcode(ERRCODE_DATA_EXCEPTION),
138 errmsg("inserted vector dimension %u does not match index "
139 "dimension %u",
140 vref.dim,
141 dim)));
142
143 /* No defined distance under cosine: encoded as unreachable below.
144 * Squared norm -- zero iff the norm is zero, without the sqrt. */
145
4/4
✓ Branch 0 taken 55 times.
✓ Branch 1 taken 17408 times.
✓ Branch 2 taken 54 times.
✓ Branch 3 taken 1 times.
17518 bool degenerate = base.metric == DISTANCE_COSINE &&
146 55 vs_l2_norm_squared(vref.data, dim) == 0.0f;
147
148 /*
149 * Route to a leaf the same way a query does (prism_query_route normalizes
150 * for cosine, rotates into qs.pt_query, and runs the beam search;
151 * qs.pt_query is then exactly the rotated residual the encode needs), lock
152 * its head, and append. If the head was split away while we waited for the
153 * lock it is now tombstoned; re-route to the new head and retry. Bounded
154 * so a pathological churn can't spin forever -- and if the bound is
155 * reached the insert fails rather than returning as though it had indexed
156 * the tuple.
157 *
158 * The query state is initialized once and reused across attempts -- each
159 * prism_query_route re-runs the search from scratch on it -- so a retry
160 * does not re-allocate its beam buffers.
161 */
162 17463 RaBitQScratch enc;
163 17463 bool enc_init = false;
164 17463 PrismQueryState qs;
165 17463 prism_query_state_init(&qs, &base, 1, PRISM_INSERT_ROUTE_BEAM);
166 17463 bool inserted = false;
167 17463 bool routed = true;
168
169
2/2
✓ Branch 1 taken 17471 times.
✓ Branch 2 taken 1 times.
17472 for (int attempt = 0; attempt < PRISM_INSERT_ROUTE_ATTEMPTS; attempt++)
170 {
171 17471 uint32_t n = prism_query_route(
172 &qs,
173 vref.data,
174 PRISM_INSERT_ROUTE_BEAM,
175 VS_DISTANCE_MODE_ASYMMETRIC,
176 NULL);
177
178 52404 BlockNumber head = (n > 0) ? qs.beam_results[0].posting_head
179
1/2
✓ Branch 0 taken 17471 times.
✗ Branch 1 not taken.
17471 : InvalidBlockNumber;
180
1/2
✓ Branch 0 taken 17471 times.
✗ Branch 1 not taken.
17471 if (head == InvalidBlockNumber)
181 {
182 routed = false;
183 17462 break;
184 }
185
186 /*
187 * Serialize concurrent inserts into this cluster with a heavyweight
188 * page lock on the head — distinct from the buffer content locks the
189 * insert primitive takes per page, so it doesn't fight the
190 * single-buffer storage model. Released here, not held to xact end.
191 */
192 17471 LockPage(index, head, ExclusiveLock);
193
194
2/2
✓ Branch 0 taken 17463 times.
✓ Branch 1 taken 8 times.
17471 if (!enc_init)
195 {
196 17463 vs_rabitq_scratch_init(&enc, dim);
197 17463 enc_init = true;
198 }
199 /*
200 * Test hook: fires while this insert holds the per-cluster page lock,
201 * so an isolation test can pause here and observe a second insert into
202 * the same cluster block on the lock (proving the serialization and
203 * that it does not deadlock). No-op unless PG was built with injection
204 * points and a test attached an action.
205 */
206 17471 INJECTION_POINT("prism-insert-locked", NULL);
207 /*
208 * If the head was split away while we waited for the lock it is now
209 * retired (TOMBSTONED immediate or DELETED XID-gated); insert_one
210 * reports that from the head read it does anyway, and we re-route.
211 */
212 17471 bool head_retired = false;
213
214 /*
215 * Test hook: stands in for finding the head retired, so a test can
216 * drive the retry budget to its end. It replaces the insert rather
217 * than following it -- an insert that had already written the entry
218 * would write it again on every retry. Compiles to a constant false
219 * unless PostgreSQL was built with injection points, and is only
220 * true while a test holds the point attached.
221 */
222
2/2
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 17463 times.
17471 if (IS_INJECTION_POINT_ATTACHED("prism-insert-force-reroute"))
223 {
224 8 INJECTION_POINT("prism-insert-force-reroute", NULL);
225 8 head_retired = true;
226 }
227 else
228 17463 prism_posting_insert_one(
229 &storage.base,
230 17463 base.params,
231 dim,
232 head,
233 *heap_tid,
234 17463 qs.pt_query,
235 &enc,
236 degenerate,
237 &head_retired);
238 17471 UnlockPage(index, head, ExclusiveLock);
239
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 17462 times.
17471 if (head_retired)
240 9 continue; /* head was split; re-route */
241 inserted = true;
242 break;
243 }
244 17463 prism_query_state_cleanup(&qs);
245
246 17463 MemoryContextSwitchTo(old_ctx);
247 17463 MemoryContextDelete(insert_ctx);
248
249 /* Check the RaBitQParams checkout back in — see prism_index_base_init
250 * above. */
251 17463 prism_release_params(dim, base.rabitq_seed, params_owner);
252
253 /*
254 * Every path out of the loop above must have indexed the tuple. Returning
255 * normally without having done so would leave a committed row that no scan
256 * of this index can ever find, with nothing to say it happened -- so fail
257 * the insert and let the transaction that owns the row decide.
258 */
259
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17463 times.
17463 if (!routed)
260 ✗ ereport(ERROR,
261 (errcode(ERRCODE_DATA_CORRUPTED),
262 errmsg("no leaf partition found for an insert into index "
263 "\"%s\"",
264 RelationGetRelationName(index))));
265
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 17462 times.
17463 if (!inserted)
266
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 ereport(ERROR,
267 (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
268 errmsg("insert into index \"%s\" gave way to concurrent "
269 "maintenance %d times",
270 RelationGetRelationName(index),
271 PRISM_INSERT_ROUTE_ATTEMPTS),
272 errhint("Retry the transaction.")));
273
274 /* bool result is only meaningful for unique indexes. */
275 return false;
276 }
277
278 /*
279 * Adapt PostgreSQL's IndexBulkDeleteCallback (takes ItemPointer) to the shared
280 * tombstone predicate (takes ItemPointerData by value).
281 */
282 typedef struct PrismBulkDeleteCtx
283 {
284 IndexBulkDeleteCallback cb;
285 void *cb_state;
286 } PrismBulkDeleteCtx;
287
288 static bool
289 12584 tid_is_dead(ItemPointerData tid, void *state)
290 {
291 12584 PrismBulkDeleteCtx *c = (PrismBulkDeleteCtx *)state;
292 12584 return c->cb(&tid, c->cb_state);
293 }
294
295 /*
296 * VACUUM's dead-tuple removal. Block-scans the index and tombstones each
297 * posting chain from its FIRST (head) page via prism_posting_tombstone_chain —
298 * the head walk covers the chain's overflow pages, so only heads are acted on.
299 * Tombstoned entries are skipped by later scans; physical reclaim happens at a
300 * later compaction/rebuild. This is the cleanup path for both explicit DELETEs
301 * and the dead old-version of every vector-column UPDATE.
302 */
303 static IndexBulkDeleteResult *
304 20 prism_bulkdelete(
305 IndexVacuumInfo *info,
306 IndexBulkDeleteResult *stats,
307 IndexBulkDeleteCallback callback,
308 void *cb_state)
309 {
310
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 if (stats == NULL)
311 20 stats = palloc0(sizeof(IndexBulkDeleteResult));
312
313 20 Relation index = info->index;
314 20 BlockNumber nblocks = RelationGetNumberOfBlocks(index);
315
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 if (nblocks <= 1)
316 return stats; /* block 0 is the metadata page */
317
318 /* dim + metric + first_posting from the per-backend cache (metapage read
319 * at most once per backend). prism_cache_meta skips the rotation-matrix
320 * work the scan / insert cache path does — VACUUM never needs it. */
321 20 Dimension dim;
322 20 DistanceMetric metric;
323 20 BlockNumber first_posting;
324 20 prism_cache_meta(index, &dim, &metric, &first_posting);
325
326 20 VsPgStorage storage;
327 20 vs_pg_storage_init(&storage, index, NULL, metric);
328
329 20 PrismBulkDeleteCtx ctx = {.cb = callback, .cb_state = cb_state};
330
331 /*
332 * The index is laid out as: block 0 metadata, then the contiguous
333 * centroid region [1, first_posting), then the posting pages.
334 * first_posting is fixed at build time and the relation only grows from
335 * there, so nothing below it is ever a posting page or a split-appended
336 * centroid page -- start there, as before, to skip that region without
337 * scanning it.
338 *
339 * A split that needs to grow the centroid tree but finds no room on the
340 * level-0 page appends a new centroid page by extending the relation
341 * (see posting_split.c), which lands past every existing posting page --
342 * i.e. still >= first_posting, just no longer separable from the
343 * posting region by block number alone. Classify each page in the
344 * scanned range by its own page_id instead of relying on position: a
345 * posting page is acted on, a centroid page and an empty
346 * (not-yet-initialized) page are both expected and skipped, and
347 * anything else is the only real anomaly.
348 *
349 * Within the posting region we act only on chain heads; overflow pages
350 * are reached via the chain from their head. A page that is none of the
351 * above is the only anomaly worth surfacing (corruption or a format
352 * bug); count those and emit a single WARNING after the walk rather
353 * than one per page, so a badly corrupt index can't flood the log
354 * (bulkdelete also runs once per dead-tuple batch, i.e. potentially
355 * many times per VACUUM).
356 */
357 20 uint32_t unrecognized = 0;
358 20 BlockNumber first_bad = InvalidBlockNumber;
359 20 BlockNumber start = Max(first_posting, 1);
360
361
2/2
✓ Branch 0 taken 295 times.
✓ Branch 1 taken 20 times.
315 for (BlockNumber blk = start; blk < nblocks; blk++)
362 {
363 295 vacuum_delay_point(false);
364
365 295 Buffer buf = ReadBuffer(index, blk);
366 295 LockBuffer(buf, BUFFER_LOCK_SHARE);
367 295 Page page = BufferGetPage(buf);
368 295 bool is_head = false;
369
1/2
✓ Branch 0 taken 295 times.
✗ Branch 1 not taken.
295 bool recognized = PageIsNew(page); /* an empty page is expected */
370
371
3/4
✓ Branch 0 taken 295 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 294 times.
✓ Branch 4 taken 1 times.
295 if (!recognized && prism_page_is_posting(page))
372 {
373 294 PrismPostingPageOpaque *op = prism_posting_opaque(page);
374 294 recognized = true;
375 /* Skip a retired (DELETED) chain: it is superseded by a split,
376 * its live_count slot now holds delete_xid, and cleanup
377 * reclaims it once safe. */
378 294 is_head = (op->flags & PRISM_POSTING_PAGE_FIRST) != 0 &&
379 !(op->flags & PRISM_POSTING_PAGE_DELETED);
380 }
381 /* Centroid pages carry no heap TIDs — there is nothing for
382 * bulkdelete to do with one, only to not mistake it for corruption. */
383
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 294 times.
295 if (!recognized && prism_page_is_centroid(page))
384 295 recognized = true;
385 295 UnlockReleaseBuffer(buf);
386
387
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 295 times.
295 if (!recognized)
388 {
389 ✗ if (first_bad == InvalidBlockNumber)
390 ✗ first_bad = blk;
391 ✗ unrecognized++;
392 }
393
394
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 262 times.
295 if (!is_head)
395 33 continue;
396
397 /*
398 * Mutating a cluster's chain requires the head's page lock -- the same
399 * lock inserts take, and the one a split holds while it rewrites the
400 * cluster. The check above ran under a buffer lock that has since been
401 * released, so without this a split could retire the chain in the gap:
402 * the head's live_count slot then holds delete_xid, and the tombstone
403 * pass would decrement that instead. A shrunken delete_xid reads as
404 * older than it is, which brings the chain's physical reclaim forward
405 * past the scans it was being kept alive for.
406 */
407 262 LockPage(index, blk, ExclusiveLock);
408
409 /* Re-read under the lock: a live head at this point stays live. */
410 262 Page hp = vs_storage_read_page(&storage.base, blk);
411 262 const PrismPostingPageOpaque *hop = prism_posting_opaque(hp);
412 524 bool still_head = (hop->flags & PRISM_POSTING_PAGE_FIRST) != 0 &&
413
3/4
✓ Branch 0 taken 261 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 261 times.
262 !(hop->flags & PRISM_POSTING_PAGE_DELETED) &&
414 !(hop->flags & PRISM_POSTING_PAGE_TOMBSTONED);
415 262 vs_storage_release_page(&storage.base, blk);
416
417
2/2
✓ Branch 0 taken 261 times.
✓ Branch 1 taken 1 times.
262 if (still_head)
418 {
419 261 stats->tuples_removed += prism_posting_tombstone_chain(
420 &storage.base, dim, blk, tid_is_dead, &ctx);
421
422 /* Live tuples remaining: the head's maintained live_count, which
423 * the tombstone pass just decremented (O(1), no rescan). */
424 261 Page lp = vs_storage_read_page(&storage.base, blk);
425 261 stats->num_index_tuples += prism_posting_head_live_count(lp);
426 261 vs_storage_release_page(&storage.base, blk);
427 }
428
429 262 UnlockPage(index, blk, ExclusiveLock);
430 }
431
432 /* One summary line per call, not one per bad page (see loop comment). */
433
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (unrecognized > 0)
434 ✗ ereport(WARNING,
435 (errcode(ERRCODE_DATA_CORRUPTED),
436 errmsg("skipped %u unrecognized page(s) in index \"%s\" "
437 "during bulkdelete (first at block %u); index may be "
438 "corrupt",
439 unrecognized,
440 RelationGetRelationName(index),
441 first_bad)));
442
443 return stats;
444 }
445
446 /*
447 * The tuple count reported here becomes the index's pg_class.reltuples,
448 * which the planner reads as indexinfo->tuples. Two things keep it honest.
449 *
450 * With no bulk delete (a VACUUM that found nothing to remove, or ANALYZE),
451 * return NULL so the existing statistics stand. Returning a zeroed result
452 * instead would set reltuples to 0 on every such VACUUM -- which is what
453 * happened -- leaving the planner believing an index over millions of rows
454 * is empty until the next ANALYZE.
455 *
456 * After a bulk delete, the count bulkdelete accumulated is the number of
457 * live posting entries, and that is not the number of indexed heap tuples:
458 * SOAR and boundary replication store a vector in more than one list, so
459 * the entry count overstates the row count by the replication factor.
460 * Report the heap's own live count instead, as nbtree does, and carry
461 * estimated_count through so an estimate does not overwrite an exact
462 * figure (vacuumlazy skips the update when it is set).
463 */
464 static IndexBulkDeleteResult *
465 41 prism_vacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
466 {
467
2/2
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 12 times.
41 if (info->analyze_only)
468 return stats;
469
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 10 times.
29 if (stats == NULL)
470 return NULL;
471
472 19 stats->num_pages = RelationGetNumberOfBlocks(info->index);
473 19 stats->num_index_tuples = info->num_heap_tuples;
474 19 stats->estimated_count = info->estimated_count;
475 19 return stats;
476 }
477
478 static void
479 408 prism_costestimate(
480 PlannerInfo *root,
481 IndexPath *path,
482 double loop_count,
483 Cost *startup_cost,
484 Cost *total_cost,
485 Selectivity *selectivity,
486 double *correlation,
487 double *index_pages)
488 {
489 /* Never use the index without ORDER BY <op> */
490
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 382 times.
408 if (path->indexorderbys == NIL)
491 {
492 26 *startup_cost = get_float8_infinity();
493 26 *total_cost = get_float8_infinity();
494 26 *selectivity = 0;
495 26 *correlation = 0;
496 26 *index_pages = 0;
497 26 path->path.disabled_nodes = 2;
498 26 return;
499 }
500
501 382 prism_cost_estimate(
502 root,
503 path,
504 loop_count,
505 startup_cost,
506 total_cost,
507 selectivity,
508 correlation,
509 index_pages);
510 }
511
512 static bytea *
513 997 prism_options(Datum reloptions, bool validate)
514 {
515 997 static const relopt_parse_elt tab[] = {
516 {"distance_mode",
517 RELOPT_TYPE_ENUM,
518 offsetof(PrismOptions, distance_mode)},
519 {"fan_out", RELOPT_TYPE_INT, offsetof(PrismOptions, fan_out)},
520 {"nlist", RELOPT_TYPE_INT, offsetof(PrismOptions, nlist)},
521 {"target_pages",
522 RELOPT_TYPE_INT,
523 offsetof(PrismOptions, target_pages)},
524 {"kmeans_nredo",
525 RELOPT_TYPE_INT,
526 offsetof(PrismOptions, kmeans_nredo)},
527 {"soar_lambda",
528 RELOPT_TYPE_REAL,
529 offsetof(PrismOptions, soar_lambda)},
530 {"boundary_epsilon",
531 RELOPT_TYPE_REAL,
532 offsetof(PrismOptions, boundary_epsilon)},
533 {"centroid_compression",
534 RELOPT_TYPE_ENUM,
535 offsetof(PrismOptions, centroid_compression)},
536 {"fastscan", RELOPT_TYPE_ENUM, offsetof(PrismOptions, fastscan)},
537 {"centroid_fastscan",
538 RELOPT_TYPE_ENUM,
539 offsetof(PrismOptions, centroid_fastscan)},
540 };
541 997 return (bytea *)build_reloptions(
542 reloptions,
543 validate,
544 prism_relopt_kind,
545 sizeof(PrismOptions),
546 tab,
547 lengthof(tab));
548 }
549
550 static bool
551 ✗ prism_validate(Oid opclassoid)
552 {
553 ✗ return true;
554 }
555
556 /* ----------------------------------------------------------------
557 * Handler
558 * ---------------------------------------------------------------- */
559
560 Datum
561 1324 prism_handler(PG_FUNCTION_ARGS)
562 {
563 1324 IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
564
565 /* Properties */
566 1324 amroutine->amstrategies = 0;
567 1324 amroutine->amsupport = 3;
568 1324 amroutine->amoptsprocnum = 0;
569 1324 amroutine->amcanorder = false;
570 1324 amroutine->amcanorderbyop = true;
571 1324 amroutine->amcanhash = false;
572 1324 amroutine->amconsistentequality = false;
573 1324 amroutine->amconsistentordering = false;
574 1324 amroutine->amcanbackward = false;
575 1324 amroutine->amcanunique = false;
576 1324 amroutine->amcanmulticol = false;
577 1324 amroutine->amoptionalkey = true;
578 1324 amroutine->amsearcharray = false;
579 1324 amroutine->amsearchnulls = false;
580 1324 amroutine->amstorage = false;
581 1324 amroutine->amclusterable = false;
582 1324 amroutine->ampredlocks = false;
583 1324 amroutine->amcanparallel = false;
584 1324 amroutine->amcanbuildparallel = true;
585 1324 amroutine->amcaninclude = false;
586 1324 amroutine->amusemaintenanceworkmem = false;
587 1324 amroutine->amsummarizing = false;
588 1324 amroutine->amparallelvacuumoptions = VACUUM_OPTION_PARALLEL_BULKDEL;
589 1324 amroutine->amkeytype = InvalidOid;
590
591 /* Build callbacks */
592 1324 amroutine->ambuild = prism_build;
593 1324 amroutine->ambuildempty = prism_buildempty;
594 1324 amroutine->ambuildphasename = prism_buildphasename;
595
596 /* Insert / maintenance */
597 1324 amroutine->aminsert = prism_insert;
598 1324 amroutine->aminsertcleanup = NULL;
599 1324 amroutine->ambulkdelete = prism_bulkdelete;
600 1324 amroutine->amvacuumcleanup = prism_vacuumcleanup;
601
602 /* Cost estimation / validation */
603 1324 amroutine->amcanreturn = NULL;
604 1324 amroutine->amcostestimate = prism_costestimate;
605 1324 amroutine->amgettreeheight = NULL;
606 1324 amroutine->amoptions = prism_options;
607 1324 amroutine->amproperty = NULL;
608 1324 amroutine->amvalidate = prism_validate;
609 1324 amroutine->amadjustmembers = NULL;
610
611 /* Scan callbacks */
612 1324 amroutine->ambeginscan = prism_beginscan;
613 1324 amroutine->amrescan = prism_rescan;
614 1324 amroutine->amgettuple = prism_gettuple;
615 1324 amroutine->amgetbitmap = NULL;
616 1324 amroutine->amendscan = prism_endscan;
617 1324 amroutine->ammarkpos = NULL;
618 1324 amroutine->amrestrpos = NULL;
619
620 /* Parallel scan (not supported) */
621 1324 amroutine->amestimateparallelscan = NULL;
622 1324 amroutine->aminitparallelscan = NULL;
623 1324 amroutine->amparallelrescan = NULL;
624
625 /* Planning */
626 1324 amroutine->amtranslatestrategy = NULL;
627 1324 amroutine->amtranslatecmptype = NULL;
628
629 1324 PG_RETURN_POINTER(amroutine);
630 }
631