| 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 |