indexRange method
Range-scan a secondary index. equalPrefix (optional) pins
leading columns by equality; lower / upper then apply to the
next column after that prefix. For a single-column index, pass
an empty equalPrefix and bounds on the only column. The encoded
value-keys are byte-order-preserving, so SQL semantics carry
through.
Returns rows in index order: ascending by indexed-column tuple, ties broken by encoded primary key. Yields nothing when the index does not exist or any prefix value is null.
Implementation
Stream<Map<String, Object?>> indexRange(
String indexName, {
List<Object?> equalPrefix = const [],
Object? lower,
bool lowerInclusive = true,
Object? upper,
bool upperInclusive = false,
}) async* {
final si = _secondary[indexName];
if (si == null) return;
if (equalPrefix.length >= si.columns.length &&
lower == null &&
upper == null) {
// Pure prefix-equality = lookup.
yield* indexLookup(indexName, equalPrefix);
return;
}
// Build the fixed prefix from equalPrefix values.
final bb = BytesBuilder(copy: false);
for (var i = 0; i < equalPrefix.length; i++) {
final v = equalPrefix[i];
if (v == null) return;
bb.add(_encodeIndexValue(v, si.columnTypes[i]));
}
final fixed = bb.toBytes();
final rangeColIdx = equalPrefix.length;
Uint8List? lo;
Uint8List? hi;
if (rangeColIdx >= si.columns.length) {
// No range column available — bounds must be null. Fall back to
// prefix-only range scan.
lo = fixed;
hi = _bumpPrefix(fixed);
} else {
final t = si.columnTypes[rangeColIdx];
if (lower != null) {
final l = _encodeIndexValue(lower, t);
final combined = Uint8List(fixed.length + l.length)
..setRange(0, fixed.length, fixed)
..setRange(fixed.length, fixed.length + l.length, l);
lo = lowerInclusive ? combined : _bumpPrefix(combined);
} else if (fixed.isNotEmpty) {
lo = fixed;
}
if (upper != null) {
final u = _encodeIndexValue(upper, t);
final combined = Uint8List(fixed.length + u.length)
..setRange(0, fixed.length, fixed)
..setRange(fixed.length, fixed.length + u.length, u);
hi = upperInclusive ? _bumpPrefix(combined) : combined;
} else if (fixed.isNotEmpty) {
hi = _bumpPrefix(fixed);
}
}
if (lo == null && hi == null) {
await for (final entry in si.btree.scan()) {
final bytes = await _heap.get(entry.value);
if (bytes == null) continue;
yield _decodeRow(bytes);
}
return;
}
await for (final entry in si.btree.range(
lower: lo,
lowerInclusive: true,
upper: hi,
upperInclusive: false,
)) {
final bytes = await _heap.get(entry.value);
if (bytes == null) continue;
yield _decodeRow(bytes);
}
}