fix(8.0): column-store range queries honor exclusive bounds (lessThan/greaterThan)

getIdsForRange computed includeMin/includeMax correctly for every operator
(gt→false/true, lt→true/false, between→true/true) but DROPPED them when the
column store served the query — it called columnStore.rangeQuery(field,min,max)
with no flags, and the column-store path was inclusive-only. So whenever a field
lived in the column store, strict lessThan/greaterThan silently behaved as
lte/gte. The sparse-fallback path already forwarded the flags, so this was
column-store-only.

Thread includeMin/includeMax end to end:
- ColumnSegmentCursor: add textbook lowerBound/upperBound helpers and express
  binarySearchRange in terms of them. Inclusive/inclusive is byte-identical to
  the old impl (lowerBound(lo) == old binarySearchValue(lo).index; upperBound(hi)
  == old "first position after hi"). Exclusive lower advances past ALL duplicates
  of the boundary value; exclusive upper stops before them.
- ColumnStore.rangeQuery: accept the flags, apply them in the segment cursor AND
  the tail-buffer linear scan (strict > / < when exclusive). A bound taken from a
  segment's own min/max stays inclusive — it is a real stored value.
- VectorIndex/types interface + getIdsForRange call site updated.

Surfaced by brainy-complete dual-bound test (year/popularity exclusive both ends
→ ['Express','Vue.js']; React@95 and Angular@75 correctly excluded). Added 5
column-store unit tests: exclusive lower, exclusive upper, both, duplicate
boundary values, and the unflushed tail-buffer path. 1469 unit green.
This commit is contained in:
David Snelling 2026-06-19 11:01:41 -07:00
parent d918f49287
commit 009e50681e
5 changed files with 185 additions and 37 deletions

View file

@ -1023,8 +1023,10 @@ export class MetadataIndexManager implements MetadataIndexProvider {
// Column store path: O(log n) binary search on sorted column.
// Use raw values (no normalization) — column store stores exact values.
// Thread includeMin/includeMax so strict lessThan/greaterThan stay strict
// (the column store is no longer inclusive-only).
if (this.columnStore && this.columnStore.hasField(field)) {
const bitmap = await this.columnStore.rangeQuery(field, min, max)
const bitmap = await this.columnStore.rangeQuery(field, min, max, includeMin, includeMax)
return this.idMapper.intsIterableToUuids(bitmap)
}