1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
/*
* Copyright (c) 2025-2026 Anton Kundenko <singaraiona@gmail.com>
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* idxop.h -- Per-vector accelerator indices.
*
* A vector with RAY_ATTR_HAS_INDEX set carries a child ray_t of type
* RAY_INDEX in its aux[0..7] slot. The index ray_t holds:
* - the kind (hash / sort / zone / bloom)
* - kind-specific payload (keys vec, perm vec, min/max, bloom bits)
* - a snapshot of the parent's original 16-byte aux union plus
* the relevant attrs bits, so detach can restore the vector to its
* pre-attach state byte-for-byte.
*
* Attach precondition: parent must not be a slice, must not already
* carry an index, must be COW'd to rc==1 by the caller's path.
*
* Mutation invalidates: any in-place write to the parent vector must
* call ray_index_drop() first AND clear RAY_ATTR_SORTED — a stale index
* or a stale sorted marker is a wrong-answer bug (e.g. an asof-join that
* trusts a false ordering claim). Operators that build a fresh result
* vector get this for free (fresh vectors start at attrs==0, no index).
*/
/* Maximum key columns a RAY_IDX_UKEY map covers. Chosen so the arm fits
* the existing union (see the static assert below); a wider key simply gets
* no index and falls back to the per-call build. */
/* Index kinds. Stored in ray_index_t.kind. */
typedef enum ray_idx_kind_t;
/* Marker bits stored in ray_index_t.markers (block-resident attributes
* that have no dedicated attrs bit). sorted lives in attrs (RAY_ATTR_SORTED),
* not here. */
/* Set when this index and its child vecs are passengers in the parent column's
* file mapping (restored in-place from the on-disk inline index region). The
* parent's single munmap frees them; ray_release_owned_refs must NOT free a
* passenger index by pointer. Clear = heap-resident index (freed normally),
* including a runtime-built index attached to an mmap'd column. */
/* The payload stored inside data[] of a RAY_INDEX ray_t. */
typedef struct ray_index_t;
/* The ukey arm is sized to land exactly on the hash arm's 64 bytes so
* ray_index_t does not grow: it is stored inline in persisted column files,
* and a larger payload would change that on-disk layout for every kind. */
;
/* On-disk index persistence stores the RAY_INDEX object and its child vecs as
* contiguous 32-byte-aligned ray_t blocks mmap'd in place (no serialization).
* For the trailing child blocks to stay 32-aligned, the index payload must be a
* 32-byte multiple — enforce it so the layout invariant can't silently break. */
;
/* Inline accessor — returns ray_index_t* for a RAY_INDEX block. */
static inline ray_index_t*
/* ── Routing observability: per-site consult/hit counters ──
* Diagnostic, unsynchronized (same caveat as ray_expr_bail_counts). */
typedef enum idx_site_t;
extern uint64_t ray_idx_consults;
extern uint64_t ray_idx_hits;
void ; /* atexit dump when RAY_IDX_STATS set */
/* == */
/* Build an accelerator and attach. Numeric types are supported by every
* index kind below; hash and part also support SYM domain ids.
* On success, *vp is the (possibly new) parent vector with HAS_INDEX set.
* On failure, *vp is unchanged and a RAY_ERROR is returned. */
ray_t* ;
ray_t* ;
ray_t* ;
ray_t* ;
ray_t* ;
/* Build per-chunk min/max + null bit at chunk_size = 1 << chunk_log2.
* Passing 0 picks the default (16 → 64 K rows / chunk). Only valid on
* numeric and temporal vectors; SYM/STR/GUID return RAY_ERR_NYI. */
ray_t* ;
/* Build a chunk-zone index WITHOUT attaching it — returns a standalone
* RAY_INDEX object (caller releases). Used by the splayed-store builder to
* compute an index for persistence without COWing a shared column. */
ray_t* ;
/* == */
typedef struct ray_zone_acc_t;
/* true when `type` gets a chunk zone (the ten numeric/temporal types). */
bool ;
/* Start accumulating rows of `type` from global row `start_row`. */
ray_err_t ;
/* Rows [next_row, next_row + v->len) are v; v->type must equal a->type. */
/* A non-OK return (OOM, CANCEL) leaves the accumulator partial: free/discard it. */
ray_err_t ;
/* dst += src, chunk by chunk; both must have the same type. src is untouched. */
ray_err_t ;
/* The standalone RAY_IDX_CHUNK_ZONE index for a column of `len` rows
* (chunk_log2 16), identical to ray_index_chunk_zone_compute(col, 16);
* NULL when len < 65536. Frees the accumulator either way. */
ray_t* ;
void ;
/* 128-bit two's-complement accumulation of int64 values: (hi, lo) += v.
* The engine's integer avg sums this way — exact for any column, and the
* same bits whatever the morsel split — and the chunk-zone metadata keeps
* the per-chunk (hi, lo) so it can answer the same value. */
static inline void
static inline void
/* Double nearest the 128-bit value (hi, lo): the magnitude is converted
* (high word scaled by 2^64 plus the low word) and the sign reapplied, so
* a small negative total is not lost in 2^64 - |s|. One formula
* everywhere so every path agrees bit for bit. */
static inline double
/* Whole-column sum (int64 wraparound) and non-null count of an integer
* column from its chunk-zone per-chunk aggregates; false when the column
* carries none for its current length. */
bool ;
/* The exact 128-bit sum (hi, lo) and non-null count from the same
* metadata: true when the per-chunk high words are stored, or when no
* partial sum can wrap int64 (then the wrapped sum is the exact one). */
bool ;
/* Build a RAY_IDX_DICT (codes + distinct values) for STR vector `v` WITHOUT
* attaching it — standalone RAY_INDEX object (caller releases / attaches).
* Returns RAY_ERR_NYI for non-STR. Used at splayed save to persist the dict
* and by ray_index_attach_dict for the runtime path. */
ray_t* ;
ray_t* ;
/* == */
typedef struct ray_dict_acc_t;
ray_err_t ;
/* Appends every row of STR vector v. There is no cardinality cap: the
* result matches ray_index_dict_compute for any number of distinct strings.
* Only a row count past INT32_MAX (codes are int32) frees the accumulator,
* sets dead and returns RAY_OK; further adds are no-ops. A non-OK return
* (OOM, CANCEL) leaves the accumulator partial: free/discard it. */
ray_err_t ;
/* The standalone RAY_IDX_DICT for a column of `len` rows, identical to
* ray_index_dict_compute; NULL when dead or len < 65536. Frees `a`. */
ray_t* ;
void ;
/* Attach an already-built standalone RAY_INDEX object (zero-copy on rc=1). */
ray_t* ;
/* ── Inline on-disk index region (zero-copy mmap) ──
* ray_index_inline_size: bytes the region occupies (32-aligned ray_t blocks).
* ray_index_inline_write: serialize `ix` into dst (child ptrs → region offsets).
* ray_index_inline_map: patch an mmap'd region's child offsets → absolute ptrs
* in place and return the RAY_INDEX object (flagged RAY_MARK_MMAP). */
int64_t ;
void ;
/* The same bytes as ray_index_inline_write, streamed to `f` block by block
* straight from the child vecs: no region-sized staging buffer (a 100 M-row
* hash region is ~4.5 GB) and one copy fewer. false on a short write. */
bool ;
ray_t* ;
/* Drop any attached index from *vp. No-op if none. Restores the
* pre-attach aux state byte-for-byte. Returns *vp. */
ray_t* ;
/* ===== Table-resident key index (RAY_IDX_UKEY) =====
* Attached to a RAY_TABLE, not to a vector, so the generic attach path does
* not apply: a table's aux is zero-init, there is nothing to snapshot and
* restore. Retain/release ride the type-agnostic RAY_ATTR_HAS_INDEX arms in
* heap.c, so the index dies with the table. */
/* Build a standalone RAY_IDX_UKEY sized for `entries` rows (caller attaches
* or releases). The slot vector is allocated zeroed. */
ray_t* ;
/* Attach `idx` to `tbl`, taking ownership of the caller's reference.
* Returns false (and keeps the caller's reference) if `tbl` already carries
* an index — the caller drops first. */
bool ;
/* The attached ukey payload, or NULL when the table carries no index or one
* of another kind. */
ray_index_t* ;
/* Detach and release any index on `tbl`. Safe on a table with none. */
void ;
/* == */
static inline bool
/* Returns RAY_IDX_NONE if no index is attached. */
static inline ray_idx_kind_t
/* --- Semantic attribute markers (see mem/heap.h: RAY_ATTR_SORTED) --- */
/* True iff v is a vector flagged sorted (non-descending). */
static inline bool
/* Returns a fresh RAY_DICT with {kind, length, ...kind-specific...}
* or RAY_NULL_OBJ when no index is attached. */
ray_t* ;
/* ===== Zone-index all/none classification =====
*
* O(1) pre-check: given the zone index on `col` and a comparison
* (col cmp_op key), decide whether the predicate is definitely true for
* ALL rows, definitely false for ALL rows (NONE), or UNKNOWN (need scan).
*
* cmp_op is the OP_EQ..OP_GE opcode. key_i is used for integer/temporal
* column families; key_f for float families. NaN key_f → UNKNOWN.
* Returns UNKNOWN when the column is not eligible (no zone index, stale,
* null-bearing, unsupported type). */
typedef enum ray_zone_class_t;
ray_zone_class_t ;
/* Build an all-NONE rowsel of `n` rows. Wraps rowsel_from_sorted_ids(n, NULL, 0).
* Returns NULL on OOM; returns a valid zero-match rowsel on success. */
ray_t* ;
/* ===== Bloom-index definite-absent probe =====
*
* Returns true ONLY when the bloom filter PROVES the key is absent
* (all k probe bits are clear). Returns false when the key may be
* present OR when the column is not eligible — caller proceeds to
* other consults / the scan either way.
*
* Integer-family columns only in v1 (mirrors the hash path's
* conservatism on float equality: F32/F64 NaN/-0 semantics).
* No range validation is performed: an out-of-range key hashes to
* some bit positions; if they are set we fall through — the absent
* proof is still sound (no false absent is possible). */
bool ;
/* ===== Hash-index point-lookup probe =====
*
* Build a ray_rowsel directly from a hash probe on `col`'s
* RAY_IDX_HASH for rows where the payload equals `key`. Bypasses
* the intermediate BOOL pred vec entirely — touches O(matches)
* memory instead of O(rows), which is the whole reason to ship
* this fast path.
*
* Returns:
* - A fresh rowsel block (rc=1) on success — install on
* g->selection. The block carries per-segment NONE/MIX/ALL
* flags and the morsel-local indices for matching rows.
* Pure NONE blocks (no matches) are returned as a valid empty
* rowsel rather than NULL — NULL is the "all-pass" sentinel
* in the consumer and would let every row through.
* - NULL when the column is not eligible: no index, wrong kind,
* built_for_len mismatch (stale), type mismatch, or out-of-
* range key. Caller must fall back to the full scan path.
*
* Eligibility (and the canonical hashing used) match
* ray_index_attach_hash: BOOL/U8/I16/I32/I64/DATE/TIME/TIMESTAMP.
* Floats are intentionally not supported — equality on F32/F64
* has NaN / -0 semantics the unfused compare kernel handles. */
ray_t* ;
/* Direct equality selection for a fresh part index. `key` is a numeric
* value or a column-domain SYM id. Dense matches remain eligible because
* the part payload already stores one contiguous physical span. */
ray_t* ;
/* ===== Hash/part-index IN probe =====
*
* Build a rowsel of all rows whose value is in set_vec via per-element hash
* probes or direct physical spans from a part index. Integer-family and SYM
* columns (same conservatism as the hash-eq path: F32/F64 NaN/-0 semantics
* belong to the scan kernel).
*
* set_vec must match the column family: an integer-family typed vec for an
* integer column, a SYM vec for a SYM column (each set symbol is resolved
* through the COLUMN's domain; domain-absent symbols match no rows).
* Other combinations → NULL.
*
* Returns:
* - A fresh rowsel block (rc=1) on success — install on g->selection.
* An empty set yields a valid all-NONE rowsel (NOT NULL).
* - NULL when the column is not eligible (no hash/part index, stale,
* float-family column, unsupported set type, OOM) or a hash-index union
* is too dense. Part-index unions retain dense full morsels efficiently.
* Caller falls back to the scan path. */
ray_t* ;
/* ===== Hash-index find (point lookup) =====
*
* Returns the minimum row id where the column value equals `key`, or
* -1 when the index proves the key is absent (provably-no-match).
* Returns -2 when the column is not eligible: no hash index, stale,
* float-family column, or out-of-eligibility condition — caller must
* fall back to the linear scan.
*
* Contract:
* >= 0 → key found; this is the FIRST (minimum) row id match.
* -1 → key provably absent; caller returns find's miss value.
* -2 → not eligible; caller falls back to the scan.
*
* Uses idx_fresh_nonull: null-bearing columns fall back (-2) so the
* scan correctly surfaces null-equality searches. */
int64_t ;
/* ===== Hash-index find: one atom / a vector of needles =====
*
* ray_index_find_atom: like ray_index_find_row but keyed by an ATOM, so it
* also serves SYM columns (the symbol re-expressed in the column's domain)
* and STR columns (the bytes). Same contract: >= 0 first row, -1 provably
* absent, -2 not eligible (no fresh null-free hash index, null / float /
* cross-family needle) — caller falls back to the scan.
*
* ray_index_find_vec: one probe per needle of the typed vector `needles`
* into `out[needles->len]` (first matching row, NULL_I64 on a miss; a null
* needle misses — the index is only consulted on null-free columns).
* Returns 1 when handled, 0 when not eligible (out is then unspecified and
* the caller builds its own hash). Integer-family needles for an
* integer-family column, SYM for SYM (any domain), STR for STR. */
int64_t ;
int ;
/* ===== Hash-index carry across an append =====
*
* `dst` is a fresh (rc=1, unsliced, unindexed, null-free) vector whose first
* src->len rows are src's rows and whose tail is appended rows. When src
* carries a fresh null-free hash index and every appended row is a NEW key,
* dst gets an equivalent index in O(groups + appended) without re-hashing the
* old rows; markers (`unique`) and the within-group order symbol carry over.
* A repeated key, a float column, a SYM domain change or OOM leave dst
* unindexed (the caller re-attaches if it wants one). Best-effort: never
* fails the append. */
void ;
/* ===== Hash-index group slice (CSR accessor) =====
*
* Resolve `key` to its group's contiguous ascending row-id slice.
* SYM columns take the COLUMN-DOMAIN id as key; numeric columns take the
* value (canonicalized through the storage width like the eq probe).
*
* Returns:
* 1 → hit: *rows_out points at the ascending row ids, *n_out the size.
* The slice borrows the index payload — valid while the index is.
* 0 → key provably absent (*rows_out NULL, *n_out 0).
* -1 → not eligible (no fresh hash index / bad key) — caller falls back. */
int ;
/* Build a rowsel from ASCENDING row ids (empty n=0 is valid). NULL on OOM. */
ray_t* ;
/* ===== Slice-group resolution (FILTER + GROUP-by-key fusion) =====
*
* One surviving group of the fused shape: the key's column-domain id and
* its CSR row slice (ascending row ids, borrowed from the index payload —
* valid while the index is). */
typedef struct ray_idx_slice_t;
/* Resolve a SYM eq key (sym ATOM, global intern id) or IN set (SYM vec,
* any domain) against `col`'s fresh hash or part index into per-key slices,
* canonicalized ascending by column-domain id (duplicates collapsed;
* absent / rowless keys dropped). Hash slices borrow CSR row ids; part
* slices use rows==NULL plus their contiguous physical first row. On success
* returns K >= 0 and, for
* K > 0, *out points into a fresh alloc block returned via *hdr_out
* (release with ray_free). K == 0 → provably no surviving rows (*out
* and *hdr_out NULL). Returns -1 when ineligible: stale or missing
* hash/part index, null-bearing or non-SYM column, wrong key shape. */
int64_t ;
/* ===== Sort-index range probe =====
*
* Build a rowsel from a binary search over the sort-index permutation for
* rows satisfying (col cmp_op key). Supports EQ/LT/LE/GT/GE. NE returns
* NULL (two disjoint spans — unsupported).
*
* Returns:
* - A fresh rowsel block on success — install on g->selection.
* span == 0 yields a valid all-NONE rowsel (NOT NULL).
* - NULL when not eligible: no sort index, stale, type mismatch,
* span > col->len / IDX_RANGE_MAX_FRAC (selectivity guard), OOM,
* or NE operator. Caller falls back to the scan.
*
* key_i is used for integer-family columns; key_f for float-family.
* is_float must match the column family (mirror of idx_filter_decode
* contract); mismatch returns NULL defensively. */
ray_t* ;
/* Build a rowsel by binary-searching a physically non-descending column.
* Unlike the sort-index route above, matching row ids are already one
* contiguous interval, so dense spans are eligible and the rowsel stores
* only the at-most-two partial boundary morsels. Requires the verified
* RAY_ATTR_SORTED marker and a null-free numeric column. */
ray_t* ;
/* ===== Sort-index ORDER BY permutation probe =====
*
* Returns a BORROWED pointer to the ascending permutation vector stored in
* the sort index on `col` when the column carries a fresh, null-free sort
* index; NULL otherwise. The caller MUST NOT release the returned pointer
* (it is owned by the index payload). To use it as an owned reference,
* call ray_retain() immediately after. */
ray_t* ;
/* ===== Sort-index distinct fast path =====
*
* Walk the sort permutation once to collect the first-occurrence row id of
* each distinct value (the first element of each equal-value run, which is
* the minimum row id of the run because the sort is stable with iota
* tie-breaking). Returns an OWNED I64 vector of those row ids, sorted
* ascending by VALUE (matching distinct_vec_eager's qsort-of-indices
* contract), and sets *out_count to the number of distinct values.
*
* Returns NULL if not eligible (no sort index, stale, null-bearing) or on
* OOM. The caller is responsible for releasing the returned vector. */
ray_t* ;
/* ===== Internal helpers (used by retain/release/detach in heap.c
* and by mutation paths in vec.c) ===== */
/* Release the saved-aux pointers carried by a RAY_INDEX ray_t.
* Invoked from ray_release_owned_refs when the index ray_t is freed. */
void ;
/* Retain the saved-aux pointers carried by a RAY_INDEX ray_t.
* Invoked from ray_retain_owned_refs after a copy of the index ray_t. */
void ;
/* Release per-kind payload children (keys/table/perm/bits...). */
void ;
/* The payload child blocks of an index (the same set retain/release walk),
* written to out[0..cap) — returns the count (at most 4). Footprint
* accounting (ray_shallow_bytes / ray_retained_bytes) sums them; a
* RAY_MARK_MMAP index's children live inside the column's mapping and are
* charged there, so callers skip them. */
int ;
/* Retain per-kind payload children. */
void ;
/* == */
ray_t* ; /* (.idx.zone v) -> v with zone attached */
ray_t* ; /* (.idx.hash v) -> v with hash attached */
ray_t* ; /* (.idx.sort v) -> v with sort attached */
ray_t* ; /* (.idx.bloom v) -> v with bloom attached */
ray_t* ; /* (.idx.drop v) -> v with index removed */
ray_t* ; /* (.idx.has? v) -> 0b/1b */
ray_t* ; /* (.idx.info v) -> dict of metadata */
ray_t* ; /* (.attr.set 'name v) */
ray_t* ; /* (.attr.get v) -> sym vec */
ray_t* ; /* (.attr.drop v) -> v cleared */
/* RAY_IDXOP_H */