| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * Copyright (c) 2026 Tiger Data, Inc. | ||
| 3 | * Licensed under the PostgreSQL License. See LICENSE for details. | ||
| 4 | * | ||
| 5 | * posting_insert.c - Runtime insert primitives (see header) | ||
| 6 | */ | ||
| 7 | |||
| 8 | #include <math.h> | ||
| 9 | |||
| 10 | #include "core/memory.h" | ||
| 11 | #include "index/posting_insert.h" | ||
| 12 | |||
| 13 | bool | ||
| 14 | 17542 | prism_posting_insert_one( | |
| 15 | VsStorage *storage, | ||
| 16 | const RaBitQParams *params, | ||
| 17 | Dimension dim, | ||
| 18 | BlockNumber head_blkno, | ||
| 19 | ItemPointerData tid, | ||
| 20 | const float *pt_input, | ||
| 21 | RaBitQScratch *scratch, | ||
| 22 | bool unreachable, | ||
| 23 | bool *head_retired) | ||
| 24 | { | ||
| 25 |
2/2✓ Branch 0 taken 17420 times.
✓ Branch 1 taken 122 times.
|
17542 | if (head_retired != NULL) |
| 26 | 17420 | *head_retired = false; | |
| 27 | |||
| 28 |
4/8✓ Branch 0 taken 17542 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 122 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 122 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 122 times.
✗ Branch 7 not taken.
|
17542 | if (storage == NULL || params == NULL || pt_input == NULL || |
| 29 |
3/4✓ Branch 0 taken 17420 times.
✓ Branch 1 taken 122 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 17420 times.
|
17542 | scratch == NULL || head_blkno == InvalidBlockNumber) |
| 30 | ✗ | return false; | |
| 31 | |||
| 32 | /* | ||
| 33 | * Phase A: read the head — compute the rotated residual against its | ||
| 34 | * pt_centroid and snapshot the head metadata, then release it (the PG | ||
| 35 | * backing holds only one buffer at a time). | ||
| 36 | */ | ||
| 37 | 17542 | Page head = vs_storage_read_page(storage, head_blkno); | |
| 38 | 17542 | const PrismPostingPageOpaque *hop = prism_posting_opaque(head); | |
| 39 | |||
| 40 | /* | ||
| 41 | * If the head was retired since it was routed to (split away by a | ||
| 42 | * concurrent rebalance), don't insert into a dead chain — report it so the | ||
| 43 | * caller re-routes. Checked here, on the read we already do, so the caller | ||
| 44 | * needn't re-read the head just to test these flags. | ||
| 45 | */ | ||
| 46 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 17541 times.
|
17542 | if (hop->flags & PRISM_POSTING_PAGE_DELETED) |
| 47 | { | ||
| 48 | 1 | vs_storage_release_page(storage, head_blkno); | |
| 49 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (head_retired != NULL) |
| 50 | 1 | *head_retired = true; | |
| 51 | 1 | return false; | |
| 52 | } | ||
| 53 | |||
| 54 | 17541 | uint32_t cluster_id = hop->cluster_id; | |
| 55 | 17541 | BlockNumber tail_blkno = hop->tail_blkno; | |
| 56 | 17541 | uint32_t live = hop->live_count; | |
| 57 | 17541 | const float *pt_cent = prism_posting_pt_centroid(head); | |
| 58 | |||
| 59 | /* pt_residual = pt_input - pt_centroid (P^T linear). Reuse scratch | ||
| 60 | * (encode_from_pt only touches scratch->xu_cb). */ | ||
| 61 | 17541 | float *pt_residual = scratch->transformed; | |
| 62 |
2/2✓ Branch 0 taken 462855 times.
✓ Branch 1 taken 17541 times.
|
480396 | for (Dimension i = 0; i < dim; i++) |
| 63 | 462855 | pt_residual[i] = pt_input[i] - pt_cent[i]; | |
| 64 | |||
| 65 | 17541 | vs_storage_release_page(storage, head_blkno); | |
| 66 | |||
| 67 | /* Encode relative to the leaf centroid. */ | ||
| 68 | 17541 | RaBitQData *rdata = vs_alloc(VS_RABITQ_DATA_SIZE(dim)); | |
| 69 | 17541 | vs_rabitq_encode_from_pt(params, pt_residual, rdata, scratch); | |
| 70 | 17419 | float f_error = | |
| 71 | 17541 | vs_rabitq_derive_f_error(rdata->f_add, rdata->f_rescale, dim); | |
| 72 | |||
| 73 | /* No defined distance under the index metric: estimated distance | ||
| 74 | * +inf with zero error, so scans prune the entry before it can | ||
| 75 | * enter the top-k threshold heap (see mark_entry_unreachable in | ||
| 76 | * posting_build.c for the full rationale). */ | ||
| 77 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 17540 times.
|
17541 | if (unreachable) |
| 78 | { | ||
| 79 | 1 | rdata->f_add = INFINITY; | |
| 80 | 1 | f_error = 0.0f; | |
| 81 | } | ||
| 82 | |||
| 83 | /* | ||
| 84 | * tail_blkno / live are read straight from the head: the build stamps both | ||
| 85 | * for every cluster (see prism_posting_builder_finish and the parallel | ||
| 86 | * leader's finalize), and inserts maintain them, so a built head always | ||
| 87 | * has a valid tail — no chain walk on the insert path. | ||
| 88 | */ | ||
| 89 | |||
| 90 | /* | ||
| 91 | * Phase B: append to the tail. Read it first to check for room (released | ||
| 92 | * before any write); the caller's per-cluster lock prevents a concurrent | ||
| 93 | * fill in between. | ||
| 94 | */ | ||
| 95 | 17541 | Page tcheck = vs_storage_read_page(storage, tail_blkno); | |
| 96 | /* | ||
| 97 | * Only append into an AoS tail that has room. A FASTSCAN page packs codes | ||
| 98 | * in immutable 32-vector groups, so an insert can never append to it — it | ||
| 99 | * starts a fresh AoS overflow page instead (the scan merges the mixed | ||
| 100 | * chain by per-page format). | ||
| 101 | */ | ||
| 102 | 17541 | bool tail_is_aos = (prism_posting_opaque(tcheck)->flags & | |
| 103 | PRISM_POSTING_PAGE_FASTSCAN) == 0; | ||
| 104 |
4/4✓ Branch 0 taken 17443 times.
✓ Branch 1 taken 98 times.
✓ Branch 3 taken 146 times.
✓ Branch 4 taken 17297 times.
|
17541 | bool has_room = tail_is_aos && prism_posting_page_has_room(tcheck); |
| 105 | 17541 | vs_storage_release_page(storage, tail_blkno); | |
| 106 | |||
| 107 | 17541 | BlockNumber new_tail = tail_blkno; | |
| 108 | 17541 | bool head_updated = false; | |
| 109 | |||
| 110 |
2/2✓ Branch 0 taken 17413 times.
✓ Branch 1 taken 128 times.
|
17541 | if (has_room) |
| 111 | { | ||
| 112 | 17413 | Page wp = vs_storage_write_page(storage, tail_blkno); | |
| 113 | 17413 | prism_posting_page_add( | |
| 114 | wp, | ||
| 115 | dim, | ||
| 116 | tid, | ||
| 117 | rdata->f_add, | ||
| 118 | rdata->f_rescale, | ||
| 119 | f_error, | ||
| 120 | 17413 | rdata->bits, | |
| 121 | 0); | ||
| 122 |
2/2✓ Branch 0 taken 1427 times.
✓ Branch 1 taken 15986 times.
|
17413 | if (tail_blkno == head_blkno) |
| 123 | { | ||
| 124 | /* Tail is the head — fold the metadata update into this write so | ||
| 125 | * single-page clusters cost one head write, not two. */ | ||
| 126 | 1427 | PrismPostingPageOpaque *op = prism_posting_opaque(wp); | |
| 127 | 1427 | op->tail_blkno = new_tail; | |
| 128 | 1427 | op->live_count = live + 1; | |
| 129 | 1427 | head_updated = true; | |
| 130 | } | ||
| 131 | 17413 | vs_storage_commit_page(storage, tail_blkno); | |
| 132 | } | ||
| 133 | else | ||
| 134 | { | ||
| 135 | /* Tail full: build the new overflow page fully and commit it BEFORE | ||
| 136 | * linking, so a crash never points the chain at an uninitialized | ||
| 137 | * page (it leaves at worst an unreferenced page). */ | ||
| 138 | 124 | BlockNumber t2; | |
| 139 | 128 | Page np = vs_storage_new_page(storage, &t2); | |
| 140 | 128 | prism_posting_page_init( | |
| 141 | np, cluster_id, dim, PRISM_POSTING_PAGE_OVERFLOW); | ||
| 142 | 128 | prism_posting_page_add( | |
| 143 | np, | ||
| 144 | dim, | ||
| 145 | tid, | ||
| 146 | rdata->f_add, | ||
| 147 | rdata->f_rescale, | ||
| 148 | f_error, | ||
| 149 | 128 | rdata->bits, | |
| 150 | 0); | ||
| 151 | 128 | vs_storage_commit_page(storage, t2); | |
| 152 | |||
| 153 | 128 | Page lp = vs_storage_write_page(storage, tail_blkno); | |
| 154 | 128 | prism_posting_opaque(lp)->next_blkno = t2; | |
| 155 |
2/2✓ Branch 0 taken 103 times.
✓ Branch 1 taken 25 times.
|
128 | if (tail_blkno == head_blkno) |
| 156 | { | ||
| 157 | 103 | PrismPostingPageOpaque *op = prism_posting_opaque(lp); | |
| 158 | 103 | op->tail_blkno = t2; | |
| 159 | 103 | op->live_count = live + 1; | |
| 160 | 103 | head_updated = true; | |
| 161 | } | ||
| 162 | 128 | vs_storage_commit_page(storage, tail_blkno); | |
| 163 | 128 | new_tail = t2; | |
| 164 | } | ||
| 165 | |||
| 166 | /* Phase C: update the head metadata when it wasn't folded above. */ | ||
| 167 |
2/2✓ Branch 0 taken 16011 times.
✓ Branch 1 taken 1530 times.
|
17541 | if (!head_updated) |
| 168 | { | ||
| 169 | 16011 | Page hw = vs_storage_write_page(storage, head_blkno); | |
| 170 | 16011 | PrismPostingPageOpaque *op = prism_posting_opaque(hw); | |
| 171 | 16011 | op->tail_blkno = new_tail; | |
| 172 | 16011 | op->live_count = live + 1; | |
| 173 | 16011 | vs_storage_commit_page(storage, head_blkno); | |
| 174 | } | ||
| 175 | |||
| 176 | 122 | return true; | |
| 177 | } | ||
| 178 | |||
| 179 | /* | ||
| 180 | * Contract is in the header. Implementation notes not stated there: each page | ||
| 181 | * is read-scanned first and only write-locked / WAL-logged when it actually | ||
| 182 | * has a dead entry to mark, so vacuuming a chain with no dead tuples dirties | ||
| 183 | * nothing. Marking is idempotent — an already-deleted entry is skipped, not | ||
| 184 | * recounted — so a repeated VACUUM over the same dead TIDs is a no-op. | ||
| 185 | */ | ||
| 186 | uint32_t | ||
| 187 | 283 | prism_posting_tombstone_chain( | |
| 188 | VsStorage *storage, | ||
| 189 | Dimension dim, | ||
| 190 | BlockNumber head_blkno, | ||
| 191 | bool (*is_dead)(ItemPointerData tid, void *state), | ||
| 192 | void *state) | ||
| 193 | { | ||
| 194 |
3/6✓ Branch 0 taken 283 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 283 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 18 times.
|
283 | if (storage == NULL || is_dead == NULL || head_blkno == InvalidBlockNumber) |
| 195 | ✗ | return 0; | |
| 196 | |||
| 197 | 18 | uint32_t total_marked = 0; | |
| 198 | 18 | BlockNumber blk = head_blkno; | |
| 199 | |||
| 200 |
2/2✓ Branch 0 taken 321 times.
✓ Branch 1 taken 283 times.
|
604 | while (blk != InvalidBlockNumber) |
| 201 | { | ||
| 202 | /* | ||
| 203 | * Phase 1: under a SHARE lock, scan the page's entries to find whether | ||
| 204 | * it has any dead entry that still needs marking. This is only a probe | ||
| 205 | * — nothing is mutated — so a page with no dead tuples is never | ||
| 206 | * dirtied or WAL-logged. Both formats early-break at the first entry | ||
| 207 | * that needs marking; they only need to know "is there work?" here. | ||
| 208 | */ | ||
| 209 | 321 | Page p = vs_storage_read_page(storage, blk); | |
| 210 | 321 | const PrismPostingPageOpaque *op = prism_posting_opaque(p); | |
| 211 | 321 | BlockNumber next = op->next_blkno; | |
| 212 | 321 | uint16_t flags = op->flags; | |
| 213 | 321 | uint32_t n = op->entry_count; | |
| 214 | |||
| 215 | 321 | prism_posting_check_count(blk, op, dim); | |
| 216 | |||
| 217 | /* Already fully tombstoned: nothing to mark, and skip so its entries | ||
| 218 | * aren't counted into the live_count decrement twice. */ | ||
| 219 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 321 times.
|
321 | if (flags & PRISM_POSTING_PAGE_TOMBSTONED) |
| 220 | { | ||
| 221 | ✗ | vs_storage_release_page(storage, blk); | |
| 222 | ✗ | blk = next; | |
| 223 | ✗ | continue; | |
| 224 | } | ||
| 225 | |||
| 226 | 321 | bool is_fastscan = (flags & PRISM_POSTING_PAGE_FASTSCAN) != 0; | |
| 227 | 321 | char *content = prism_posting_page_content(p, dim); | |
| 228 | 321 | bool needs_mark = false; /* has a not-yet-marked dead entry */ | |
| 229 | |||
| 230 |
2/2✓ Branch 0 taken 224 times.
✓ Branch 1 taken 97 times.
|
321 | if (!is_fastscan) |
| 231 | { | ||
| 232 |
2/2✓ Branch 0 taken 4319 times.
✓ Branch 1 taken 51 times.
|
4370 | for (uint32_t i = 0; i < n; i++) |
| 233 | { | ||
| 234 | 4295 | PrismPostingEntryHeader *h = | |
| 235 |
2/2✓ Branch 0 taken 4287 times.
✓ Branch 1 taken 8 times.
|
4319 | prism_posting_entry_at(content, i, dim); |
| 236 |
4/4✓ Branch 0 taken 4305 times.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 4126 times.
✓ Branch 3 taken 179 times.
|
8624 | if (!(h->meta.flags & PRISM_POSTING_FLAG_DELETED) && |
| 237 | 4305 | is_dead(h->meta.tid, state)) | |
| 238 | { | ||
| 239 | 6 | needs_mark = true; | |
| 240 | 6 | break; | |
| 241 | } | ||
| 242 | } | ||
| 243 | } | ||
| 244 | else | ||
| 245 | { | ||
| 246 | 97 | uint32_t ngroups = (n + VS_FASTSCAN_GROUP - 1) / VS_FASTSCAN_GROUP; | |
| 247 |
4/4✓ Branch 0 taken 180 times.
✓ Branch 1 taken 87 times.
✓ Branch 2 taken 48 times.
✓ Branch 3 taken 10 times.
|
267 | for (uint32_t g = 0; g < ngroups && !needs_mark; g++) |
| 248 | { | ||
| 249 | 170 | uint32_t g_count = n - g * VS_FASTSCAN_GROUP; | |
| 250 |
2/2✓ Branch 0 taken 42 times.
✓ Branch 1 taken 6 times.
|
170 | if (g_count > VS_FASTSCAN_GROUP) |
| 251 | 42 | g_count = VS_FASTSCAN_GROUP; | |
| 252 | 170 | uint32_t mask = | |
| 253 | 170 | *prism_fastscan_group_tombstone_mask(content, g, dim); | |
| 254 | 122 | ItemPointerData *tids = | |
| 255 | 170 | prism_fastscan_group_tids(content, g, dim); | |
| 256 |
2/2✓ Branch 0 taken 3289 times.
✓ Branch 1 taken 134 times.
|
3423 | for (uint32_t v = 0; v < g_count; v++) |
| 257 |
5/5✓ Branch 0 taken 2011 times.
✓ Branch 1 taken 1272 times.
✓ Branch 2 taken 1993 times.
✓ Branch 3 taken 36 times.
✓ Branch 4 taken 1260 times.
|
6572 | if (!prism_fastscan_lane_is_tombstoned(mask, v) && |
| 258 | 3283 | is_dead(tids[v], state)) | |
| 259 | { | ||
| 260 | 12 | needs_mark = true; | |
| 261 | 12 | break; | |
| 262 | } | ||
| 263 | } | ||
| 264 | } | ||
| 265 | 321 | vs_storage_release_page(storage, blk); | |
| 266 | |||
| 267 | /* | ||
| 268 | * Phase 2: only if phase 1 found work, take the EXCLUSIVE write lock | ||
| 269 | * (which WAL-logs the page on commit) and mark the dead entries. The | ||
| 270 | * share lock was dropped above and PG has no atomic lock upgrade, so | ||
| 271 | * the page may have changed; re-derive everything from scratch here | ||
| 272 | * (re-read entry_count, re-test is_dead and the dead marker) rather | ||
| 273 | * than trusting phase 1's findings. This makes marking idempotent. | ||
| 274 | */ | ||
| 275 |
2/2✓ Branch 0 taken 124 times.
✓ Branch 1 taken 197 times.
|
321 | if (needs_mark) |
| 276 | { | ||
| 277 | 209 | Page wp = vs_storage_write_page(storage, blk); | |
| 278 | 209 | PrismPostingPageOpaque *wop = prism_posting_opaque(wp); | |
| 279 | 209 | char *c = prism_posting_page_content(wp, dim); | |
| 280 | 209 | uint32_t deleted_on_page = 0; | |
| 281 | |||
| 282 | /* Read the format fresh rather than trust phase 1's is_fastscan, | ||
| 283 | * matching the "re-derive everything from scratch" rule above -- | ||
| 284 | * a page's format never actually changes today, but this way the | ||
| 285 | * code doesn't quietly rely on that to stay correct. */ | ||
| 286 |
2/2✓ Branch 0 taken 173 times.
✓ Branch 1 taken 36 times.
|
209 | if (!(wop->flags & PRISM_POSTING_PAGE_FASTSCAN)) |
| 287 | { | ||
| 288 |
2/2✓ Branch 0 taken 8098 times.
✓ Branch 1 taken 173 times.
|
8271 | for (uint32_t i = 0; i < wop->entry_count; i++) |
| 289 | { | ||
| 290 | 8050 | PrismPostingEntryHeader *h = | |
| 291 |
2/2✓ Branch 0 taken 30 times.
✓ Branch 1 taken 8020 times.
|
8098 | prism_posting_entry_at(c, i, dim); |
| 292 |
2/2✓ Branch 0 taken 30 times.
✓ Branch 1 taken 8068 times.
|
8098 | if (h->meta.flags & PRISM_POSTING_FLAG_DELETED) |
| 293 | { | ||
| 294 | 30 | deleted_on_page++; | |
| 295 | 30 | continue; | |
| 296 | } | ||
| 297 |
2/2✓ Branch 1 taken 2368 times.
✓ Branch 2 taken 5700 times.
|
8068 | if (is_dead(h->meta.tid, state)) |
| 298 | { | ||
| 299 | 2368 | h->meta.flags |= PRISM_POSTING_FLAG_DELETED; | |
| 300 | 2368 | total_marked++; | |
| 301 | 2368 | deleted_on_page++; | |
| 302 | } | ||
| 303 | } | ||
| 304 | } | ||
| 305 | else | ||
| 306 | { | ||
| 307 | 36 | uint32_t wn = wop->entry_count; | |
| 308 | 36 | uint32_t ngroups = (wn + VS_FASTSCAN_GROUP - 1) / | |
| 309 | VS_FASTSCAN_GROUP; | ||
| 310 |
2/2✓ Branch 0 taken 156 times.
✓ Branch 1 taken 36 times.
|
192 | for (uint32_t g = 0; g < ngroups; g++) |
| 311 | { | ||
| 312 | 156 | uint32_t g_count = wn - g * VS_FASTSCAN_GROUP; | |
| 313 |
2/2✓ Branch 0 taken 52 times.
✓ Branch 1 taken 12 times.
|
156 | if (g_count > VS_FASTSCAN_GROUP) |
| 314 | 52 | g_count = VS_FASTSCAN_GROUP; | |
| 315 | 92 | uint32_t *mask_p = | |
| 316 | 156 | prism_fastscan_group_tombstone_mask(c, g, dim); | |
| 317 | 156 | uint32_t mask = *mask_p; | |
| 318 | 92 | ItemPointerData *tids = | |
| 319 | 156 | prism_fastscan_group_tids(c, g, dim); | |
| 320 |
2/2✓ Branch 0 taken 4425 times.
✓ Branch 1 taken 156 times.
|
4581 | for (uint32_t v = 0; v < g_count; v++) |
| 321 | { | ||
| 322 |
3/3✓ Branch 0 taken 20 times.
✓ Branch 1 taken 2533 times.
✓ Branch 2 taken 1872 times.
|
4425 | if (prism_fastscan_lane_is_tombstoned(mask, v)) |
| 323 | { | ||
| 324 | 20 | deleted_on_page++; | |
| 325 | 20 | continue; | |
| 326 | } | ||
| 327 |
2/2✓ Branch 1 taken 832 times.
✓ Branch 2 taken 3573 times.
|
4405 | if (is_dead(tids[v], state)) |
| 328 | { | ||
| 329 | 832 | mask = prism_fastscan_lane_set_tombstone(mask, v); | |
| 330 | 832 | total_marked++; | |
| 331 | 832 | deleted_on_page++; | |
| 332 | } | ||
| 333 | } | ||
| 334 | 156 | *mask_p = mask; | |
| 335 | } | ||
| 336 | } | ||
| 337 | /* Whole page now dead: flag it so the scan skips its scoring. */ | ||
| 338 |
3/4✓ Branch 0 taken 209 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 188 times.
|
209 | if (wop->entry_count > 0 && deleted_on_page == wop->entry_count) |
| 339 | 21 | wop->flags |= PRISM_POSTING_PAGE_TOMBSTONED; | |
| 340 | 209 | vs_storage_commit_page(storage, blk); | |
| 341 | } | ||
| 342 | |||
| 343 | 24 | blk = next; | |
| 344 | } | ||
| 345 | |||
| 346 | /* Keep the head's live_count current (one head write per cluster). It is | ||
| 347 | * stamped at build and maintained by inserts, so it is always meaningful | ||
| 348 | * here; clamp defensively. */ | ||
| 349 |
2/2✓ Branch 0 taken 205 times.
✓ Branch 1 taken 78 times.
|
283 | if (total_marked > 0) |
| 350 | { | ||
| 351 | 205 | Page hw = vs_storage_write_page(storage, head_blkno); | |
| 352 | 205 | PrismPostingPageOpaque *op = prism_posting_opaque(hw); | |
| 353 | 410 | op->live_count = (op->live_count >= total_marked) | |
| 354 | 14 | ? op->live_count - total_marked | |
| 355 |
1/2✓ Branch 0 taken 205 times.
✗ Branch 1 not taken.
|
205 | : 0; |
| 356 | 205 | vs_storage_commit_page(storage, head_blkno); | |
| 357 | } | ||
| 358 | |||
| 359 | 18 | return total_marked; | |
| 360 | } | ||
| 361 |