1use std::collections::{HashMap, HashSet};
2use std::fmt;
3use std::io;
4
5use serde_json::Value;
6
7use crate::errors::HaysonCipherError;
8use crate::registry::composite_index::{RegistryIndex, RegistryIndexKind};
9use crate::registry::exact_index::value_key;
10use crate::registry::fulltext_index::SearchMode;
11use crate::registry::sorted_index::{RangeBounds, SortValue};
12use crate::registry::table_registry::TableRegistry;
13use crate::schema::{HaysonRecord, UNPARTITIONED};
14use crate::storage::record::parse_entry;
15use crate::storage::{LogFile, LogSet, Slot};
16
17use super::predicates::{all_leaves, leaves_of, same_value, test_node, ComparisonOp, Predicate, PredicateNode};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum QueryStrategy {
21 Primary,
22 Exact,
23 Sorted,
24 Fulltext,
25 Union,
26 Scan,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct PlanBranch {
31 pub strategy: QueryStrategy,
32 pub field: Option<String>,
33 pub candidates: usize,
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub struct AccessPath {
38 pub strategy: QueryStrategy,
39 pub field: Option<String>,
40 pub index: Option<String>,
41 pub keys: Vec<String>,
42 pub branches: Option<Vec<PlanBranch>>,
43 pub scores: Option<HashMap<String, f64>>,
46 pub overlay: Option<PlanOverlay>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum OverlayStrategy {
53 Exact,
55 Fulltext,
57 Scan,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct PlanOverlay {
64 pub staged: usize,
66 pub suppressed: usize,
68 pub rescored: bool,
70 pub strategy: OverlayStrategy,
71 pub size: usize,
73}
74
75pub enum OverlayCandidates {
77 NotIndexed,
79 Narrowed(Vec<String>),
81}
82
83pub trait QueryOverlay {
86 fn records(&self) -> &HashMap<String, HaysonRecord>;
88 fn deleted(&self) -> &HashSet<String>;
90 fn candidates_for(&self, field: &str, encoded: &str) -> OverlayCandidates;
91}
92
93#[derive(Debug, Clone, PartialEq)]
94pub struct SearchSpec {
95 pub text: String,
96 pub mode: SearchMode,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum SortDirection {
101 Asc,
102 Desc,
103}
104
105#[derive(Debug, Clone, PartialEq)]
108pub struct OrderBy {
109 pub field: String,
110 pub direction: SortDirection,
111}
112
113#[derive(Debug, Clone, PartialEq)]
114pub struct QuerySpec {
115 pub root: PredicateNode,
116 pub search: Option<SearchSpec>,
117 pub order_by: Option<OrderBy>,
118 pub limit: Option<usize>,
119 pub offset: Option<usize>,
120 pub count_only: bool,
124}
125
126impl QuerySpec {
127 pub fn new(root: PredicateNode) -> Self {
128 QuerySpec {
129 root,
130 search: None,
131 order_by: None,
132 limit: None,
133 offset: None,
134 count_only: false,
135 }
136 }
137
138 pub fn with_search(mut self, text: impl Into<String>, mode: SearchMode) -> Self {
139 self.search = Some(SearchSpec {
140 text: text.into(),
141 mode,
142 });
143 self
144 }
145
146 pub fn with_order_by(mut self, field: impl Into<String>, direction: SortDirection) -> Self {
147 self.order_by = Some(OrderBy {
148 field: field.into(),
149 direction,
150 });
151 self
152 }
153
154 pub fn with_limit(mut self, limit: usize) -> Self {
155 self.limit = Some(limit);
156 self
157 }
158
159 pub fn with_offset(mut self, offset: usize) -> Self {
160 self.offset = Some(offset);
161 self
162 }
163
164 pub fn counting(mut self, count_only: bool) -> Self {
165 self.count_only = count_only;
166 self
167 }
168
169 pub fn unpaged(&self) -> Self {
173 QuerySpec {
174 limit: None,
175 offset: None,
176 count_only: false,
177 ..self.clone()
178 }
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
183struct KeySet {
184 strategy: QueryStrategy,
185 field: Option<String>,
186 index: Option<String>,
187 keys: Vec<String>,
188 branches: Option<Vec<PlanBranch>>,
189}
190
191impl KeySet {
192 fn into_path(self, scores: Option<HashMap<String, f64>>) -> AccessPath {
193 AccessPath {
194 strategy: self.strategy,
195 field: self.field,
196 index: self.index,
197 keys: self.keys,
198 branches: self.branches,
199 scores,
200 overlay: None,
201 }
202 }
203}
204
205fn cheaper(best: Option<KeySet>, candidate: Option<KeySet>) -> Option<KeySet> {
206 match candidate {
207 None => best,
208 Some(candidate) => match best {
209 None => Some(candidate),
210 Some(best) => {
211 if candidate.keys.len() < best.keys.len() {
212 Some(candidate)
213 } else {
214 Some(best)
215 }
216 }
217 },
218 }
219}
220
221fn is_primitive(value: &Value) -> bool {
222 matches!(value, Value::String(_) | Value::Number(_) | Value::Bool(_))
223}
224
225fn is_primitive_equality(predicate: &Predicate) -> bool {
226 predicate.op == ComparisonOp::Eq && is_primitive(&predicate.value)
227}
228
229fn equality_on<'a>(leaves: &[&'a Predicate], field: &str) -> Option<&'a Predicate> {
230 let usable: Vec<&'a Predicate> = leaves
231 .iter()
232 .copied()
233 .filter(|leaf| {
234 leaf.field == field
235 && (leaf.op == ComparisonOp::Eq || leaf.op == ComparisonOp::Contains)
236 && leaf.value != Value::Null
237 })
238 .collect();
239 usable
240 .iter()
241 .find(|leaf| leaf.op == ComparisonOp::Eq)
242 .copied()
243 .or_else(|| usable.first().copied())
244}
245
246struct ExactMatch {
247 keys: Vec<String>,
248 width: usize,
249}
250
251fn in_write_order(registry: &TableRegistry, mut keys: Vec<String>) -> Vec<String> {
257 keys.sort_by(|left, right| {
258 let earlier = registry.arrival_of(left).unwrap_or(0);
259 let later = registry.arrival_of(right).unwrap_or(0);
260 earlier.cmp(&later).then_with(|| left.cmp(right))
261 });
262 keys
263}
264
265fn repeats_predicate(registry: &TableRegistry, index: &RegistryIndex, leaves: &[&Predicate]) -> bool {
268 let Some(clauses) = registry.where_of(index) else {
269 return true;
270 };
271 clauses.iter().all(|clause| {
272 leaves
273 .iter()
274 .any(|leaf| leaf.field == clause.field && leaf.op == clause.op && same_value(&leaf.value, &clause.value))
275 })
276}
277
278fn exact_match(registry: &TableRegistry, index: &RegistryIndex, leaves: &[&Predicate]) -> Option<ExactMatch> {
280 if index.kind != RegistryIndexKind::Exact && index.kind != RegistryIndexKind::Sorted {
281 return None;
282 }
283 let mut values: Vec<Value> = Vec::new();
284 let mut proves = registry.where_of(index).is_none();
285 for field in &index.fields {
286 match equality_on(leaves, field) {
287 Some(predicate) => {
288 values.push(predicate.value.clone());
289 if !is_primitive_equality(predicate) {
290 proves = false;
291 }
292 }
293 None => break,
294 }
295 }
296 if values.is_empty() {
297 return None;
298 }
299 let width = values.len();
300 match registry.candidates_of(index, &values) {
301 Some(bucket) => Some(ExactMatch {
302 keys: in_write_order(registry, bucket.iter().cloned().collect()),
303 width,
304 }),
305 None => proves.then(|| ExactMatch {
306 keys: Vec::new(),
307 width,
308 }),
309 }
310}
311
312#[derive(Default)]
313struct RawBounds {
314 gt: Option<Value>,
315 gte: Option<Value>,
316 lt: Option<Value>,
317 lte: Option<Value>,
318}
319
320fn bounds_of(leaves: &[&Predicate], field: &str) -> Option<RawBounds> {
321 let mut bounds = RawBounds::default();
322 let mut found = false;
323 for predicate in leaves {
324 if predicate.field != field {
325 continue;
326 }
327 if !matches!(predicate.value, Value::Number(_) | Value::String(_)) {
328 continue;
329 }
330 match predicate.op {
331 ComparisonOp::Gt => {
332 bounds.gt = Some(predicate.value.clone());
333 found = true;
334 }
335 ComparisonOp::Gte => {
336 bounds.gte = Some(predicate.value.clone());
337 found = true;
338 }
339 ComparisonOp::Lt => {
340 bounds.lt = Some(predicate.value.clone());
341 found = true;
342 }
343 ComparisonOp::Lte => {
344 bounds.lte = Some(predicate.value.clone());
345 found = true;
346 }
347 _ => {}
348 }
349 }
350 found.then_some(bounds)
351}
352
353fn with_prefix(prefix: &[SortValue], bounds: &RawBounds) -> RangeBounds {
354 let vector_of = |edge: &Option<Value>| -> Option<Vec<SortValue>> {
355 let value = edge.as_ref()?;
356 let sortable = SortValue::from_json(value)?;
357 let mut vector = prefix.to_vec();
358 vector.push(sortable);
359 Some(vector)
360 };
361 RangeBounds {
362 gt: vector_of(&bounds.gt),
363 gte: vector_of(&bounds.gte),
364 lt: vector_of(&bounds.lt),
365 lte: vector_of(&bounds.lte),
366 }
367}
368
369fn sorted_keys(registry: &TableRegistry, index: &RegistryIndex, leaves: &[&Predicate]) -> Option<Vec<String>> {
370 if index.kind != RegistryIndexKind::Sorted {
371 return None;
372 }
373 let mut prefix: Vec<SortValue> = Vec::new();
374 for field in &index.fields {
375 let predicate = leaves
376 .iter()
377 .find(|leaf| leaf.field == *field && is_primitive_equality(leaf));
378 match predicate {
379 Some(predicate) => prefix.push(SortValue::from_json(&predicate.value)?),
380 None => break,
381 }
382 }
383 let open = index.fields.get(prefix.len())?;
384 let bounds = bounds_of(leaves, open)?;
385 let extended = with_prefix(&prefix, &bounds);
386 registry.range_of(index, &extended)
387}
388
389fn key_set_of(registry: &TableRegistry, index: &RegistryIndex, strategy: QueryStrategy, keys: Vec<String>) -> KeySet {
390 let named = index.fields.len() > 1 || registry.where_of(index).is_some();
391 KeySet {
392 strategy,
393 field: (!named).then(|| index.fields[0].clone()),
394 index: named.then(|| index.name.clone()),
395 keys,
396 branches: None,
397 }
398}
399
400fn primary_keys_of(leaves: &[&Predicate], primary_key: &str) -> Option<Vec<String>> {
401 for predicate in leaves {
402 if predicate.field != primary_key {
403 continue;
404 }
405 match (predicate.op, &predicate.value) {
406 (ComparisonOp::Eq, Value::String(s)) => return Some(vec![s.clone()]),
407 (ComparisonOp::Eq, Value::Number(n)) => return Some(vec![n.to_string()]),
408 (ComparisonOp::In, Value::Array(items)) => {
409 return Some(items.iter().map(value_as_key).collect());
410 }
411 _ => {}
412 }
413 }
414 None
415}
416
417fn value_as_key(value: &Value) -> String {
418 match value {
419 Value::String(s) => s.clone(),
420 other => other.to_string(),
421 }
422}
423
424fn primary_path(registry: &TableRegistry, leaves: &[&Predicate]) -> Option<KeySet> {
425 let wanted = primary_keys_of(leaves, ®istry.schema.primary_key)?;
426 let keys: Vec<String> = wanted
427 .into_iter()
428 .filter(|key| registry.slot_of(key).is_some())
429 .collect();
430 Some(KeySet {
431 strategy: QueryStrategy::Primary,
432 field: Some(registry.schema.primary_key.clone()),
433 index: None,
434 keys,
435 branches: None,
436 })
437}
438
439fn fulltext_path(scores: &HashMap<String, f64>) -> KeySet {
440 let mut keys: Vec<String> = scores.keys().cloned().collect();
442 keys.sort_by(|a, b| rank(scores, a, b));
443 KeySet {
444 strategy: QueryStrategy::Fulltext,
445 field: None,
446 index: None,
447 keys,
448 branches: None,
449 }
450}
451
452fn rank(scores: &HashMap<String, f64>, a: &str, b: &str) -> std::cmp::Ordering {
453 let left = scores.get(a).copied().unwrap_or(0.0);
454 let right = scores.get(b).copied().unwrap_or(0.0);
455 right
456 .partial_cmp(&left)
457 .unwrap_or(std::cmp::Ordering::Equal)
458 .then_with(|| a.cmp(b))
459}
460
461fn indexed_path(
462 registry: &TableRegistry,
463 nodes: &[PredicateNode],
464 scores: Option<&HashMap<String, f64>>,
465) -> Option<KeySet> {
466 let leaves = leaves_of(nodes);
467 let mut best: Option<KeySet> = None;
468 let mut widest = 0usize;
469
470 let usable: Vec<&RegistryIndex> = registry
471 .indexes()
472 .iter()
473 .filter(|index| repeats_predicate(registry, index, &leaves))
474 .collect();
475
476 for index in &usable {
477 let Some(matched) = exact_match(registry, index, &leaves) else {
478 continue;
479 };
480 let tied = best.as_ref().is_some_and(|b| matched.keys.len() == b.keys.len());
481 let cheaper_than_best = best.as_ref().is_none_or(|b| matched.keys.len() < b.keys.len());
482 if tied {
483 if matched.width > widest {
484 widest = matched.width;
485 best = Some(key_set_of(registry, index, QueryStrategy::Exact, matched.keys));
486 }
487 } else if cheaper_than_best {
488 widest = matched.width;
489 best = Some(key_set_of(registry, index, QueryStrategy::Exact, matched.keys));
490 }
491 }
492
493 if let Some(scores) = scores {
494 best = cheaper(best, Some(fulltext_path(scores)));
495 }
496
497 for index in &usable {
498 let Some(keys) = sorted_keys(registry, index, &leaves) else {
499 continue;
500 };
501 best = cheaper(best, Some(key_set_of(registry, index, QueryStrategy::Sorted, keys)));
502 }
503
504 for node in nodes {
506 if matches!(node, PredicateNode::Group { .. }) {
507 best = cheaper(best, plan(registry, node, None));
508 }
509 }
510
511 best
512}
513
514fn and_path(
515 registry: &TableRegistry,
516 nodes: &[PredicateNode],
517 scores: Option<&HashMap<String, f64>>,
518) -> Option<KeySet> {
519 let leaves = leaves_of(nodes);
520 cheaper(primary_path(registry, &leaves), indexed_path(registry, nodes, scores))
521}
522
523fn branches_of(path: &KeySet) -> Vec<PlanBranch> {
524 path.branches.clone().unwrap_or_else(|| {
525 vec![PlanBranch {
526 strategy: path.strategy,
527 field: path.field.clone(),
528 candidates: path.keys.len(),
529 }]
530 })
531}
532
533fn or_path(registry: &TableRegistry, nodes: &[PredicateNode]) -> Option<KeySet> {
535 if nodes.is_empty() {
536 return None;
537 }
538 let mut seen = HashSet::new();
539 let mut keys = Vec::new();
540 let mut branches = Vec::new();
541 for node in nodes {
542 let path = plan(registry, node, None)?;
543 for key in &path.keys {
544 if seen.insert(key.clone()) {
545 keys.push(key.clone());
546 }
547 }
548 branches.extend(branches_of(&path));
549 }
550 Some(KeySet {
551 strategy: QueryStrategy::Union,
552 field: None,
553 index: None,
554 keys,
555 branches: Some(branches),
556 })
557}
558
559fn plan(registry: &TableRegistry, node: &PredicateNode, scores: Option<&HashMap<String, f64>>) -> Option<KeySet> {
560 match node {
561 PredicateNode::Leaf(_) => and_path(registry, std::slice::from_ref(node), scores),
562 PredicateNode::Group {
563 combine: super::predicates::Combine::And,
564 nodes,
565 } => and_path(registry, nodes, scores),
566 PredicateNode::Group {
567 combine: super::predicates::Combine::Or,
568 nodes,
569 } => or_path(registry, nodes),
570 }
571}
572
573fn committed_path(registry: &TableRegistry, spec: &QuerySpec, scores: Option<&HashMap<String, f64>>) -> KeySet {
574 plan(registry, &spec.root, scores).unwrap_or_else(|| KeySet {
575 strategy: QueryStrategy::Scan,
576 field: None,
577 index: None,
578 keys: in_write_order(registry, registry.keys().cloned().collect()),
579 branches: None,
580 })
581}
582
583pub fn plan_access(registry: &TableRegistry, spec: &QuerySpec) -> AccessPath {
584 let scores = spec
585 .search
586 .as_ref()
587 .filter(|_| registry.has_fulltext())
588 .map(|search| registry.search(&search.text, search.mode));
589 committed_path(registry, spec, scores.as_ref()).into_path(scores)
590}
591
592fn top_level_leaves(spec: &QuerySpec) -> Vec<&Predicate> {
595 match &spec.root {
596 PredicateNode::Leaf(leaf) => vec![leaf],
597 PredicateNode::Group {
598 combine: super::predicates::Combine::And,
599 nodes,
600 } => nodes
601 .iter()
602 .filter_map(|node| match node {
603 PredicateNode::Leaf(leaf) => Some(leaf),
604 PredicateNode::Group { .. } => None,
605 })
606 .collect(),
607 PredicateNode::Group { .. } => Vec::new(),
608 }
609}
610
611fn indexed_staged(overlay: &dyn QueryOverlay, spec: &QuerySpec) -> Option<Vec<String>> {
614 let mut best: Option<Vec<String>> = None;
615 for leaf in top_level_leaves(spec) {
616 if !is_primitive_equality(leaf) {
617 continue;
618 }
619 let OverlayCandidates::Narrowed(keys) = overlay.candidates_for(&leaf.field, &value_key(&leaf.value)) else {
620 continue;
621 };
622 if best.as_ref().is_none_or(|found| keys.len() < found.len()) {
623 best = Some(keys);
624 }
625 }
626 best
627}
628
629fn staged_candidates(
630 path: &KeySet,
631 overlay: &dyn QueryOverlay,
632 spec: &QuerySpec,
633 scores: Option<&HashMap<String, f64>>,
634) -> (Vec<String>, OverlayStrategy) {
635 if path.strategy == QueryStrategy::Fulltext {
637 if let Some(scores) = scores {
638 let mut keys: Vec<String> = overlay
639 .records()
640 .keys()
641 .filter(|key| scores.contains_key(*key))
642 .cloned()
643 .collect();
644 keys.sort_by(|a, b| rank(scores, a, b));
645 return (keys, OverlayStrategy::Fulltext);
646 }
647 }
648 match indexed_staged(overlay, spec) {
649 Some(keys) => (keys, OverlayStrategy::Exact),
650 None => (overlay.records().keys().cloned().collect(), OverlayStrategy::Scan),
651 }
652}
653
654pub fn plan_access_overlaid(
659 registry: &TableRegistry,
660 spec: &QuerySpec,
661 scores: Option<HashMap<String, f64>>,
662 overlay: &dyn QueryOverlay,
663) -> AccessPath {
664 let path = committed_path(registry, spec, scores.as_ref());
665 let kept: Vec<String> = path
666 .keys
667 .iter()
668 .filter(|key| !overlay.records().contains_key(*key) && !overlay.deleted().contains(*key))
669 .cloned()
670 .collect();
671 let suppressed = path.keys.len() - kept.len();
672 let (staged, strategy) = staged_candidates(&path, overlay, spec, scores.as_ref());
673 let summary = PlanOverlay {
674 staged: staged.len(),
675 suppressed,
676 rescored: scores.is_some(),
677 strategy,
678 size: overlay.records().len(),
679 };
680 let mut keys = kept;
681 keys.extend(staged);
682 AccessPath {
683 keys,
684 overlay: Some(summary),
685 ..path.into_path(scores)
686 }
687}
688
689pub fn order_and_page(
696 entries: Vec<(String, Value)>,
697 spec: &QuerySpec,
698 scores: Option<&HashMap<String, f64>>,
699) -> Vec<String> {
700 let mut entries = entries;
701 match (&spec.order_by, scores) {
702 (Some(order), _) => {
703 let descending = order.direction == SortDirection::Desc;
704 entries.sort_by(|(_, left), (_, right)| {
705 let ordering = super::predicates::compare_values(left, right);
706 if descending {
707 ordering.reverse()
708 } else {
709 ordering
710 }
711 });
712 }
713 (None, Some(scores)) => entries.sort_by(|(left, _), (right, _)| rank(scores, left, right)),
714 (None, None) => {}
715 }
716 page_of(entries.into_iter().map(|(key, _)| key).collect(), spec)
717}
718
719pub fn page_of(keys: Vec<String>, spec: &QuerySpec) -> Vec<String> {
722 if spec.offset.is_none() && spec.limit.is_none() {
723 return keys;
724 }
725 let from = spec.offset.unwrap_or(0).min(keys.len());
726 let to = match spec.limit {
727 Some(limit) => from.saturating_add(limit).min(keys.len()),
728 None => keys.len(),
729 };
730 keys[from..to].to_vec()
731}
732
733#[derive(Debug)]
737pub enum RecordSourceError {
738 Cipher(Box<HaysonCipherError>),
740 Io(io::Error),
741}
742
743impl fmt::Display for RecordSourceError {
744 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
745 match self {
746 RecordSourceError::Cipher(error) => error.fmt(f),
747 RecordSourceError::Io(error) => error.fmt(f),
748 }
749 }
750}
751
752impl std::error::Error for RecordSourceError {}
753
754impl From<io::Error> for RecordSourceError {
755 fn from(value: io::Error) -> Self {
756 RecordSourceError::Io(value)
757 }
758}
759
760impl From<HaysonCipherError> for RecordSourceError {
761 fn from(value: HaysonCipherError) -> Self {
762 RecordSourceError::Cipher(Box::new(value))
763 }
764}
765
766pub trait RecordSource {
769 fn load(&mut self, key: &str) -> Result<Option<HaysonRecord>, RecordSourceError>;
770}
771
772pub type OpenRecordFn<'a> = dyn Fn(&str, HaysonRecord) -> Result<HaysonRecord, RecordSourceError> + 'a;
775
776#[allow(dead_code)]
777pub struct LogRecordSource<'a> {
778 registry: &'a TableRegistry,
779 log: &'a LogFile,
780 open: Option<&'a OpenRecordFn<'a>>,
784}
785
786#[allow(dead_code)]
787impl<'a> LogRecordSource<'a> {
788 pub fn new(registry: &'a TableRegistry, log: &'a LogFile) -> Self {
789 LogRecordSource {
790 registry,
791 log,
792 open: None,
793 }
794 }
795
796 pub fn with_open(registry: &'a TableRegistry, log: &'a LogFile, open: &'a OpenRecordFn<'a>) -> Self {
800 LogRecordSource {
801 registry,
802 log,
803 open: Some(open),
804 }
805 }
806}
807
808impl RecordSource for LogRecordSource<'_> {
809 fn load(&mut self, key: &str) -> Result<Option<HaysonRecord>, RecordSourceError> {
810 let Some(entry) = self.registry.slot_of(key) else {
811 return Ok(None);
812 };
813 let slot = Slot {
814 offset: entry.offset,
815 length: entry.length,
816 };
817 let line = self.log.read_slice(&slot)?;
818 let path = self.log.path().to_string_lossy().into_owned();
819 let parsed =
820 parse_entry(&line, &path, entry.offset).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
821 match (parsed.data, self.open) {
822 (Some(record), Some(open)) => Ok(Some(open(key, record)?)),
823 (data, _) => Ok(data),
824 }
825 }
826}
827
828pub struct LogSetRecordSource<'a> {
833 registry: &'a TableRegistry,
834 logs: &'a LogSet,
835 open: Option<&'a OpenRecordFn<'a>>,
836}
837
838impl<'a> LogSetRecordSource<'a> {
839 pub fn new(registry: &'a TableRegistry, logs: &'a LogSet) -> Self {
840 LogSetRecordSource {
841 registry,
842 logs,
843 open: None,
844 }
845 }
846
847 pub fn with_open(registry: &'a TableRegistry, logs: &'a LogSet, open: &'a OpenRecordFn<'a>) -> Self {
850 LogSetRecordSource {
851 registry,
852 logs,
853 open: Some(open),
854 }
855 }
856}
857
858impl RecordSource for LogSetRecordSource<'_> {
859 fn load(&mut self, key: &str) -> Result<Option<HaysonRecord>, RecordSourceError> {
860 let Some(entry) = self.registry.slot_of(key) else {
861 return Ok(None);
862 };
863 let part = self.registry.part_of(key).unwrap_or(UNPARTITIONED);
864 let slot = Slot {
865 offset: entry.offset,
866 length: entry.length,
867 };
868 let line = self.logs.read_slice(part, &slot)?;
869 let path = self.logs.path_of(part).to_string_lossy().into_owned();
870 let parsed =
871 parse_entry(&line, &path, entry.offset).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
872 match (parsed.data, self.open) {
873 (Some(record), Some(open)) => Ok(Some(open(key, record)?)),
874 (data, _) => Ok(data),
875 }
876 }
877}
878
879#[derive(Debug, Clone, PartialEq, Eq)]
880pub struct QueryResult {
881 pub keys: Vec<String>,
884 pub matched: usize,
886 pub plan: AccessPathSummary,
887}
888
889#[derive(Debug, Clone, PartialEq, Eq)]
890pub struct AccessPathSummary {
891 pub strategy: QueryStrategy,
892 pub field: Option<String>,
893 pub index: Option<String>,
894 pub candidates: usize,
895 pub ranked: bool,
898 pub filtered_on_projection: bool,
901 pub reads: usize,
902 pub branches: Option<Vec<PlanBranch>>,
903 pub overlay: Option<PlanOverlay>,
905}
906
907fn fields_in_play(spec: &QuerySpec) -> Vec<&str> {
910 let mut fields: Vec<&str> = all_leaves(std::slice::from_ref(&spec.root))
911 .iter()
912 .map(|leaf| leaf.field.as_str())
913 .collect();
914 if let Some(order) = &spec.order_by {
915 fields.push(order.field.as_str());
916 }
917 fields
918}
919
920fn scored_in(access: &AccessPath, spec: &QuerySpec, key: &str) -> bool {
925 if spec.search.is_none() {
926 return true;
927 }
928 access.scores.as_ref().is_some_and(|scores| scores.contains_key(key))
929}
930
931fn matched_entry(
932 record: &HaysonRecord,
933 key: &str,
934 spec: &QuerySpec,
935 sort_field: Option<&str>,
936) -> Option<(String, Value)> {
937 if !test_node(record, &spec.root) {
938 return None;
939 }
940 let sort_value = sort_field.map_or(Value::Null, |field| record.get(field).cloned().unwrap_or(Value::Null));
941 Some((key.to_string(), sort_value))
942}
943
944pub fn execute_query<S: RecordSource + ?Sized>(
952 registry: &TableRegistry,
953 source: &mut S,
954 spec: &QuerySpec,
955) -> Result<QueryResult, RecordSourceError> {
956 let access = plan_access(registry, spec);
957 execute_access(registry, source, spec, access, None)
958}
959
960pub fn execute_access<S: RecordSource + ?Sized>(
965 registry: &TableRegistry,
966 source: &mut S,
967 spec: &QuerySpec,
968 access: AccessPath,
969 overlay: Option<&dyn QueryOverlay>,
970) -> Result<QueryResult, RecordSourceError> {
971 let sort_field = spec.order_by.as_ref().map(|order| order.field.as_str());
972 let on_projection = fields_in_play(spec)
973 .into_iter()
974 .all(|field| registry.is_projected(field));
975 let mut matched: Vec<(String, Value)> = Vec::new();
976 let mut reads = 0usize;
977 for key in &access.keys {
978 let staged = overlay.and_then(|overlay| overlay.records().get(key));
979 let entry = match staged {
980 Some(_) if !scored_in(&access, spec, key) => None,
982 Some(record) => matched_entry(record, key, spec, sort_field),
983 None if on_projection => registry
984 .projection_of(key)
985 .and_then(|projection| matched_entry(projection, key, spec, sort_field)),
986 None => match source.load(key)? {
987 Some(record) => {
988 reads += 1;
989 matched_entry(&record, key, spec, sort_field)
990 }
991 None => None,
992 },
993 };
994 if let Some(entry) = entry {
995 matched.push(entry);
996 }
997 }
998 let total = matched.len();
999 let keys = if spec.count_only {
1000 Vec::new()
1001 } else {
1002 order_and_page(matched, spec, access.scores.as_ref())
1003 };
1004 Ok(QueryResult {
1005 keys,
1006 matched: total,
1007 plan: AccessPathSummary {
1008 strategy: access.strategy,
1009 field: access.field,
1010 index: access.index,
1011 candidates: access.keys.len(),
1012 ranked: access.scores.is_some() && spec.order_by.is_none(),
1013 filtered_on_projection: on_projection,
1014 reads,
1015 branches: access.branches,
1016 overlay: access.overlay,
1017 },
1018 })
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use serde_json::json;
1024
1025 use super::*;
1026 use crate::query::builder::QueryBuilder;
1027 use crate::schema::parse_schema;
1028 use crate::storage::record::{serialize_entry, LogMeta, LogOp};
1029
1030 struct CountingSource<'a> {
1031 inner: LogRecordSource<'a>,
1032 reads: usize,
1033 }
1034
1035 impl RecordSource for CountingSource<'_> {
1036 fn load(&mut self, key: &str) -> Result<Option<HaysonRecord>, RecordSourceError> {
1037 self.reads += 1;
1038 self.inner.load(key)
1039 }
1040 }
1041
1042 fn users_schema() -> crate::schema::TableSchema {
1043 parse_schema(&json!({
1044 "schemaVersion": 1,
1045 "table": "users",
1046 "primaryKey": "id",
1047 "fields": {
1048 "id": { "type": "string" },
1049 "name": { "type": "string" },
1050 "age": { "type": "int", "index": "sorted" },
1051 "role": { "type": "string", "index": "exact" },
1052 "tags": { "type": "string[]", "index": "exact" },
1053 },
1054 }))
1055 .unwrap()
1056 }
1057
1058 fn record(pairs: &[(&str, Value)]) -> HaysonRecord {
1059 pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
1060 }
1061
1062 fn ania() -> HaysonRecord {
1063 record(&[
1064 ("id", json!("user_1")),
1065 ("name", json!("Ania")),
1066 ("age", json!(31)),
1067 ("role", json!("admin")),
1068 ("tags", json!(["backend", "node"])),
1069 ])
1070 }
1071
1072 fn piotr() -> HaysonRecord {
1073 record(&[
1074 ("id", json!("user_2")),
1075 ("name", json!("Piotr")),
1076 ("age", json!(24)),
1077 ("role", json!("user")),
1078 ("tags", json!(["frontend"])),
1079 ])
1080 }
1081
1082 fn maria() -> HaysonRecord {
1083 record(&[
1084 ("id", json!("user_3")),
1085 ("name", json!("Maria")),
1086 ("age", json!(41)),
1087 ("role", json!("user")),
1088 ("tags", json!(["backend", "go"])),
1089 ])
1090 }
1091
1092 struct Fixture {
1093 _dir: tempfile::TempDir,
1094 registry: TableRegistry,
1095 log: LogFile,
1096 }
1097
1098 fn fixture_with_schema(schema: crate::schema::TableSchema, records: &[HaysonRecord]) -> Fixture {
1099 fixture_of(TableRegistry::new(schema), records)
1100 }
1101
1102 fn fixture_of(registry: TableRegistry, records: &[HaysonRecord]) -> Fixture {
1103 let dir = tempfile::tempdir().unwrap();
1104 let path = dir.path().join("users.jsonl");
1105 let mut log = LogFile::open(&path, false).unwrap();
1106 let mut registry = registry;
1107
1108 let mut seq = 0u64;
1109 for entry in records {
1110 seq += 1;
1111 let key = entry.get("id").and_then(Value::as_str).unwrap().to_string();
1112 let line = serialize_entry(
1113 &LogMeta {
1114 key: key.clone(),
1115 op: LogOp::Put,
1116 seq,
1117 ts: seq as i64,
1118 },
1119 Some(entry),
1120 );
1121 let slots = log.append(&[line]).unwrap();
1122 registry.apply_put(&key, entry, slots[0], seq);
1123 }
1124 Fixture {
1125 _dir: dir,
1126 registry,
1127 log,
1128 }
1129 }
1130
1131 fn fixture(records: &[HaysonRecord]) -> Fixture {
1132 fixture_with_schema(users_schema(), records)
1133 }
1134
1135 fn fulltext_users_schema() -> crate::schema::TableSchema {
1136 parse_schema(&json!({
1137 "schemaVersion": 1,
1138 "table": "users",
1139 "primaryKey": "id",
1140 "fields": {
1141 "id": { "type": "string" },
1142 "name": { "type": "string" },
1143 "bio": { "type": "string", "fulltext": true },
1144 "age": { "type": "int", "index": "sorted" },
1145 "role": { "type": "string", "index": "exact" },
1146 },
1147 }))
1148 .unwrap()
1149 }
1150
1151 fn ania_bio() -> HaysonRecord {
1152 record(&[
1153 ("id", json!("user_1")),
1154 ("name", json!("Ania")),
1155 ("bio", json!("Backend developer, Łódź")),
1156 ("age", json!(31)),
1157 ("role", json!("admin")),
1158 ])
1159 }
1160
1161 fn piotr_bio() -> HaysonRecord {
1162 record(&[
1163 ("id", json!("user_2")),
1164 ("name", json!("Piotr")),
1165 ("bio", json!("Frontend developer from Warsaw")),
1166 ("age", json!(24)),
1167 ("role", json!("user")),
1168 ])
1169 }
1170
1171 fn maria_bio() -> HaysonRecord {
1172 record(&[
1173 ("id", json!("user_3")),
1174 ("name", json!("Maria")),
1175 ("bio", json!("Backend developer, Kraków")),
1176 ("age", json!(41)),
1177 ("role", json!("user")),
1178 ])
1179 }
1180
1181 fn fulltext_fixture() -> Fixture {
1182 fixture_with_schema(fulltext_users_schema(), &[ania_bio(), piotr_bio(), maria_bio()])
1183 }
1184
1185 fn overwrite(fixture: &mut Fixture, entry: &HaysonRecord, seq: u64) {
1186 let key = entry.get("id").and_then(Value::as_str).unwrap().to_string();
1187 let line = serialize_entry(
1188 &LogMeta {
1189 key: key.clone(),
1190 op: LogOp::Put,
1191 seq,
1192 ts: seq as i64,
1193 },
1194 Some(entry),
1195 );
1196 let slots = fixture.log.append(&[line]).unwrap();
1197 fixture.registry.apply_put(&key, entry, slots[0], seq);
1198 }
1199
1200 fn run(fixture: &Fixture, spec: &QuerySpec) -> (QueryResult, usize) {
1201 let mut source = CountingSource {
1202 inner: LogRecordSource::new(&fixture.registry, &fixture.log),
1203 reads: 0,
1204 };
1205 let result = execute_query(&fixture.registry, &mut source, spec).unwrap();
1206 (result, source.reads)
1207 }
1208
1209 #[test]
1210 fn uses_the_exact_index_for_a_single_predicate() {
1211 let fixture = fixture(&[ania(), piotr(), maria()]);
1212 let spec = QueryBuilder::new()
1213 .r#where("role", ComparisonOp::Eq, json!("user"))
1214 .spec();
1215
1216 let (result, reads) = run(&fixture, &spec);
1217 assert_eq!(result.plan.strategy, QueryStrategy::Exact);
1218 assert_eq!(result.plan.field, Some("role".to_string()));
1219 assert_eq!(result.plan.candidates, 2);
1220 assert_eq!(reads, 0, "`role` is projected, so the predicate never leaves memory");
1221 let mut ids = result.keys.clone();
1222 ids.sort();
1223 assert_eq!(ids, vec!["user_2", "user_3"]);
1224 }
1225
1226 #[test]
1227 fn narrows_through_the_index_then_filters_the_rest_in_memory() {
1228 let fixture = fixture(&[ania(), piotr(), maria()]);
1229 let spec = QueryBuilder::new()
1230 .r#where("role", ComparisonOp::Eq, json!("user"))
1231 .r#where("age", ComparisonOp::Gt, json!(30))
1232 .spec();
1233
1234 let (result, reads) = run(&fixture, &spec);
1235 assert_eq!(result.plan.strategy, QueryStrategy::Exact);
1236 assert_eq!(result.plan.candidates, 2);
1237 assert_eq!(
1238 reads, 0,
1239 "`role` and `age` are both projected — the second filter runs on projections too"
1240 );
1241 assert_eq!(result.keys, vec!["user_3".to_string()]);
1242 }
1243
1244 #[test]
1245 fn picks_the_more_selective_index_when_more_than_one_applies() {
1246 let fixture = fixture(&[ania(), piotr(), maria()]);
1247 let spec = QueryBuilder::new()
1248 .r#where("role", ComparisonOp::Eq, json!("admin"))
1249 .r#where("age", ComparisonOp::Gt, json!(10))
1250 .spec();
1251
1252 let (result, _) = run(&fixture, &spec);
1253 assert_eq!(result.plan.strategy, QueryStrategy::Exact);
1254 assert_eq!(result.plan.field, Some("role".to_string()));
1255 assert_eq!(result.plan.candidates, 1);
1256 }
1257
1258 #[test]
1259 fn unions_the_branches_of_an_or_bracket() {
1260 let fixture = fixture(&[ania(), piotr(), maria()]);
1261 let spec = QueryBuilder::new()
1262 .where_any(|group| {
1263 group.r#where("role", ComparisonOp::Eq, json!("admin")).r#where(
1264 "tags",
1265 ComparisonOp::Contains,
1266 json!("go"),
1267 );
1268 })
1269 .spec();
1270
1271 let (result, _) = run(&fixture, &spec);
1272 assert_eq!(result.plan.strategy, QueryStrategy::Union);
1273 assert_eq!(result.plan.candidates, 2);
1274 assert_eq!(
1275 result.plan.branches,
1276 Some(vec![
1277 PlanBranch {
1278 strategy: QueryStrategy::Exact,
1279 field: Some("role".to_string()),
1280 candidates: 1
1281 },
1282 PlanBranch {
1283 strategy: QueryStrategy::Exact,
1284 field: Some("tags".to_string()),
1285 candidates: 1
1286 },
1287 ])
1288 );
1289 let mut ids = result.keys.clone();
1290 ids.sort();
1291 assert_eq!(ids, vec!["user_1", "user_3"]);
1292 }
1293
1294 #[test]
1295 fn nests_an_and_bracket_inside_an_or_bracket() {
1296 let fixture = fixture(&[ania(), piotr(), maria()]);
1297 let spec = QueryBuilder::new()
1298 .where_any(|group| {
1299 group
1300 .r#where("role", ComparisonOp::Eq, json!("admin"))
1301 .where_all(|inner| {
1302 inner.r#where("role", ComparisonOp::Eq, json!("user")).r#where(
1303 "age",
1304 ComparisonOp::Gt,
1305 json!(35),
1306 );
1307 });
1308 })
1309 .spec();
1310
1311 let (result, _) = run(&fixture, &spec);
1312 assert_eq!(result.plan.strategy, QueryStrategy::Union);
1313 assert_eq!(
1314 result.plan.branches,
1315 Some(vec![
1316 PlanBranch {
1317 strategy: QueryStrategy::Exact,
1318 field: Some("role".to_string()),
1319 candidates: 1
1320 },
1321 PlanBranch {
1322 strategy: QueryStrategy::Sorted,
1323 field: Some("age".to_string()),
1324 candidates: 1
1325 },
1326 ])
1327 );
1328 let mut ids = result.keys.clone();
1329 ids.sort();
1330 assert_eq!(ids, vec!["user_1", "user_3"]);
1331 }
1332
1333 #[test]
1334 fn degrades_the_whole_bracket_to_a_scan_when_one_branch_has_no_index() {
1335 let fixture = fixture(&[ania(), piotr(), maria()]);
1336 let spec = QueryBuilder::new()
1337 .where_any(|group| {
1338 group.r#where("role", ComparisonOp::Eq, json!("admin")).r#where(
1339 "name",
1340 ComparisonOp::StartsWith,
1341 json!("Piotr"),
1342 );
1343 })
1344 .spec();
1345
1346 let (result, _) = run(&fixture, &spec);
1347 assert_eq!(result.plan.strategy, QueryStrategy::Scan);
1348 assert_eq!(result.plan.candidates, 3);
1349 assert_eq!(result.plan.branches, None);
1350 let mut ids = result.keys.clone();
1351 ids.sort();
1352 assert_eq!(ids, vec!["user_1", "user_2"]);
1353 }
1354
1355 #[test]
1359 fn an_empty_index_bucket_never_falls_back_to_a_scan() {
1360 let fixture = fixture(&[ania(), piotr(), maria()]);
1361 let spec = QueryBuilder::new()
1362 .r#where("role", ComparisonOp::Eq, json!("ghost"))
1363 .spec();
1364
1365 let (result, reads) = run(&fixture, &spec);
1366 assert_eq!(result.plan.strategy, QueryStrategy::Exact);
1367 assert_eq!(result.plan.field, Some("role".to_string()));
1368 assert_eq!(result.plan.candidates, 0);
1369 assert_eq!(
1370 reads, 0,
1371 "the log must not be read when the index already proved an empty result"
1372 );
1373 assert!(result.keys.is_empty());
1374 }
1375
1376 #[test]
1377 fn falls_back_to_a_scan_when_no_registry_can_help() {
1378 let fixture = fixture(&[ania(), piotr(), maria()]);
1379 let spec = QueryBuilder::new()
1380 .r#where("name", ComparisonOp::StartsWith, json!("A"))
1381 .spec();
1382
1383 let (result, reads) = run(&fixture, &spec);
1384 assert_eq!(result.plan.strategy, QueryStrategy::Scan);
1385 assert_eq!(result.plan.candidates, 3);
1386 assert_eq!(reads, 3);
1387 assert_eq!(result.keys, vec!["user_1".to_string()]);
1388 }
1389
1390 #[test]
1391 fn reaches_the_primary_registry_from_a_top_level_equality() {
1392 let fixture = fixture(&[ania(), piotr(), maria()]);
1393 let spec = QueryBuilder::new()
1394 .r#where("id", ComparisonOp::Eq, json!("user_2"))
1395 .spec();
1396
1397 let (result, _) = run(&fixture, &spec);
1398 assert_eq!(result.plan.strategy, QueryStrategy::Primary);
1399 assert_eq!(result.plan.candidates, 1);
1400 assert_eq!(result.keys, vec!["user_2".to_string()]);
1401 }
1402
1403 #[test]
1404 fn uses_the_fulltext_index_for_a_search_query() {
1405 let fixture = fulltext_fixture();
1406 let spec = QueryBuilder::new().search("backend developer", SearchMode::All).spec();
1407
1408 let (result, _) = run(&fixture, &spec);
1409 assert_eq!(result.plan.strategy, QueryStrategy::Fulltext);
1410 assert_eq!(result.plan.field, None);
1411 assert_eq!(result.plan.candidates, 2);
1412 let mut ids = result.keys.clone();
1413 ids.sort();
1414 assert_eq!(ids, vec!["user_1", "user_3"]);
1415 }
1416
1417 #[test]
1421 fn narrows_a_fulltext_search_with_an_in_memory_predicate() {
1422 let fixture = fulltext_fixture();
1423 let spec = QueryBuilder::new()
1424 .search("backend developer", SearchMode::All)
1425 .r#where("name", ComparisonOp::StartsWith, json!("A"))
1426 .spec();
1427
1428 let (result, _) = run(&fixture, &spec);
1429 assert_eq!(result.plan.strategy, QueryStrategy::Fulltext);
1430 assert_eq!(result.plan.candidates, 2);
1431 assert_eq!(result.keys, vec!["user_1".to_string()]);
1432 }
1433
1434 #[test]
1435 fn matches_a_direct_registry_search_call() {
1436 let fixture = fulltext_fixture();
1437 let spec = QueryBuilder::new().search("backend developer", SearchMode::All).spec();
1438
1439 let (result, _) = run(&fixture, &spec);
1440 let mut via_planner = result.keys.clone();
1441 via_planner.sort();
1442
1443 let mut direct: Vec<String> = fixture
1444 .registry
1445 .search("backend developer", SearchMode::All)
1446 .into_keys()
1447 .collect();
1448 direct.sort();
1449
1450 assert_eq!(via_planner, direct);
1451 }
1452
1453 #[test]
1454 fn orders_the_result_by_a_field_ascending_and_descending() {
1455 let fixture = fixture(&[ania(), piotr(), maria()]);
1456 let ascending = QueryBuilder::new().order_by("age", SortDirection::Asc).spec();
1457 let descending = QueryBuilder::new().order_by("age", SortDirection::Desc).spec();
1458
1459 let (up, _) = run(&fixture, &ascending);
1460 let (down, _) = run(&fixture, &descending);
1461
1462 assert_eq!(up.keys, vec!["user_2", "user_1", "user_3"], "24, 31, 41");
1463 assert_eq!(down.keys, vec!["user_3", "user_1", "user_2"], "41, 31, 24");
1464 }
1465
1466 #[test]
1470 fn a_page_is_cut_out_of_the_ordered_result_only_after_the_predicate_narrowed_it() {
1471 let fixture = fixture(&[ania(), piotr(), maria()]);
1472 let spec = QueryBuilder::new()
1473 .r#where("age", ComparisonOp::Gt, json!(24))
1474 .order_by("age", SortDirection::Asc)
1475 .limit(1)
1476 .spec();
1477
1478 let (result, _) = run(&fixture, &spec);
1479
1480 assert_eq!(result.keys, vec!["user_1"], "31 is the youngest of those older than 24");
1481 assert_eq!(result.matched, 2);
1482 }
1483
1484 #[test]
1485 fn an_offset_and_a_limit_together_select_a_middle_window_of_the_order() {
1486 let fixture = fixture(&[ania(), piotr(), maria()]);
1487 let spec = QueryBuilder::new()
1488 .order_by("age", SortDirection::Asc)
1489 .offset(1)
1490 .limit(1)
1491 .spec();
1492
1493 let (result, _) = run(&fixture, &spec);
1494
1495 assert_eq!(result.keys, vec!["user_1"]);
1496 assert_eq!(
1497 result.matched, 3,
1498 "the page is one key wide, the match count counts all three"
1499 );
1500 }
1501
1502 #[test]
1503 fn an_offset_past_the_end_and_a_limit_of_zero_both_answer_an_empty_page() {
1504 let fixture = fixture(&[ania(), piotr(), maria()]);
1505 let past_the_end = QueryBuilder::new()
1506 .order_by("age", SortDirection::Asc)
1507 .offset(99)
1508 .spec();
1509 let nothing = QueryBuilder::new().order_by("age", SortDirection::Asc).limit(0).spec();
1510
1511 let (skipped, _) = run(&fixture, &past_the_end);
1512 let (none, _) = run(&fixture, ¬hing);
1513
1514 assert!(skipped.keys.is_empty());
1515 assert_eq!(skipped.matched, 3);
1516 assert!(none.keys.is_empty());
1517 assert_eq!(none.matched, 3);
1518 }
1519
1520 #[test]
1521 fn a_page_window_over_an_empty_result_stays_empty() {
1522 let fixture = fixture(&[ania(), piotr(), maria()]);
1523 let spec = QueryBuilder::new()
1524 .r#where("role", ComparisonOp::Eq, json!("ghost"))
1525 .order_by("age", SortDirection::Asc)
1526 .offset(2)
1527 .limit(5)
1528 .spec();
1529
1530 let (result, _) = run(&fixture, &spec);
1531
1532 assert!(result.keys.is_empty());
1533 assert_eq!(result.matched, 0);
1534 }
1535
1536 #[test]
1538 fn a_tie_on_the_sort_field_keeps_every_matched_key_exactly_once() {
1539 let mut twin = piotr();
1540 twin.insert("id".to_string(), json!("user_4"));
1541 twin.insert("age".to_string(), json!(41));
1542 let fixture = fixture(&[ania(), piotr(), maria(), twin]);
1543 let spec = QueryBuilder::new().order_by("age", SortDirection::Asc).spec();
1544
1545 let (result, _) = run(&fixture, &spec);
1546
1547 assert_eq!(result.keys.len(), 4);
1548 assert_eq!(
1549 &result.keys[..2],
1550 ["user_2".to_string(), "user_1".to_string()],
1551 "24 then 31"
1552 );
1553 let mut tied = result.keys[2..].to_vec();
1554 tied.sort();
1555 assert_eq!(
1556 tied,
1557 vec!["user_3", "user_4"],
1558 "both 41s are present, in whichever order"
1559 );
1560 }
1561
1562 #[test]
1563 fn a_fulltext_search_ranks_by_score_before_the_page_is_cut() {
1564 let fixture = fulltext_fixture();
1565 let ranked = QueryBuilder::new().search("backend developer", SearchMode::All).spec();
1566 let first = QueryBuilder::new()
1567 .search("backend developer", SearchMode::All)
1568 .limit(1)
1569 .spec();
1570
1571 let (all, _) = run(&fixture, &ranked);
1572 let (page, _) = run(&fixture, &first);
1573
1574 assert_eq!(
1575 page.keys,
1576 all.keys[..1],
1577 "the page is the head of the ranking, not an arbitrary hit"
1578 );
1579 assert_eq!(page.matched, 2);
1580 }
1581
1582 #[test]
1583 fn an_explicit_order_replaces_the_fulltext_ranking() {
1584 let fixture = fulltext_fixture();
1585 let spec = QueryBuilder::new()
1586 .search("backend developer", SearchMode::All)
1587 .order_by("age", SortDirection::Desc)
1588 .limit(1)
1589 .spec();
1590
1591 let (result, _) = run(&fixture, &spec);
1592
1593 assert_eq!(
1594 result.keys,
1595 vec!["user_3"],
1596 "Maria is 41 — the ranking would have led with user_1"
1597 );
1598 }
1599
1600 #[test]
1601 fn an_aggregate_folds_every_match_and_not_just_the_page() {
1602 use crate::query::aggregate::execute_aggregate;
1603
1604 let fixture = fixture(&[ania(), piotr(), maria()]);
1605 let (spec, aggregate) = QueryBuilder::new().count().limit(1).build_aggregate();
1606
1607 let mut source = LogRecordSource::new(&fixture.registry, &fixture.log);
1608 let folded = execute_aggregate(&fixture.registry, &mut source, &spec, &aggregate).unwrap();
1609
1610 assert_eq!(folded.count, 3);
1611 }
1612
1613 fn named_users_schema() -> crate::schema::TableSchema {
1614 parse_schema(&json!({
1615 "schemaVersion": 1,
1616 "table": "users",
1617 "primaryKey": "id",
1618 "fields": {
1619 "id": { "type": "string" },
1620 "name": { "type": "string", "index": "exact" },
1621 "age": { "type": "int", "index": "sorted" },
1622 "role": { "type": "string", "index": "exact" },
1623 "tags": { "type": "string[]", "index": "exact" },
1624 },
1625 }))
1626 .unwrap()
1627 }
1628
1629 fn ania_kowalska() -> HaysonRecord {
1630 record(&[
1631 ("id", json!("user_1")),
1632 ("name", json!("Ania Kowalska")),
1633 ("age", json!(31)),
1634 ("role", json!("admin")),
1635 ("tags", json!(["backend", "node"])),
1636 ])
1637 }
1638
1639 #[test]
1643 fn a_contains_predicate_on_a_string_field_with_an_exact_index_falls_back_to_a_scan() {
1644 let fixture = fixture_with_schema(named_users_schema(), &[ania_kowalska(), piotr(), maria()]);
1645 let spec = QueryBuilder::new()
1646 .r#where("name", ComparisonOp::Contains, json!("Kowals"))
1647 .spec();
1648
1649 let (result, _) = run(&fixture, &spec);
1650
1651 assert_eq!(result.plan.strategy, QueryStrategy::Scan);
1652 assert_eq!(result.keys, vec!["user_1".to_string()]);
1653 }
1654
1655 #[test]
1658 fn a_contains_predicate_on_an_array_field_keeps_using_the_exact_index() {
1659 let fixture = fixture(&[ania(), piotr(), maria()]);
1660 let spec = QueryBuilder::new()
1661 .r#where("tags", ComparisonOp::Contains, json!("go"))
1662 .spec();
1663
1664 let (result, _) = run(&fixture, &spec);
1665
1666 assert_eq!(result.plan.strategy, QueryStrategy::Exact);
1667 assert_eq!(result.plan.field, Some("tags".to_string()));
1668 assert_eq!(result.plan.candidates, 1);
1669 assert_eq!(result.keys, vec!["user_3".to_string()]);
1670 }
1671
1672 #[test]
1675 fn an_equality_against_an_array_value_falls_back_to_a_scan() {
1676 let fixture = fixture(&[ania(), piotr(), maria()]);
1677 let spec = QueryBuilder::new()
1678 .r#where("tags", ComparisonOp::Eq, json!(["backend", "node"]))
1679 .spec();
1680
1681 let (result, _) = run(&fixture, &spec);
1682
1683 assert_eq!(result.plan.strategy, QueryStrategy::Scan);
1684 assert_eq!(result.keys, vec!["user_1".to_string()]);
1685 }
1686
1687 fn admins_only_index() -> Vec<crate::catalog::ExpandedIndex> {
1688 vec![crate::catalog::ExpandedIndex {
1689 name: "users_admin_name".to_string(),
1690 fields: vec!["name".to_string()],
1691 kind: crate::catalog::CatalogIndexKind::Exact,
1692 unique: false,
1693 where_: Some(vec![crate::catalog::CatalogPredicate {
1694 field: "role".to_string(),
1695 op: "=".to_string(),
1696 value: json!("admin"),
1697 }]),
1698 expire_after_seconds: None,
1699 }]
1700 }
1701
1702 fn partial_fixture() -> Fixture {
1703 let schema = parse_schema(&json!({
1704 "schemaVersion": 1,
1705 "table": "users",
1706 "primaryKey": "id",
1707 "fields": {
1708 "id": { "type": "string" },
1709 "name": { "type": "string" },
1710 "age": { "type": "int" },
1711 "role": { "type": "string" },
1712 "tags": { "type": "string[]" },
1713 },
1714 }))
1715 .unwrap();
1716 let registry = TableRegistry::with_declared_indexes(schema, &admins_only_index());
1717 fixture_of(registry, &[ania(), piotr(), maria()])
1718 }
1719
1720 #[test]
1723 fn a_where_scoped_index_never_answers_a_query_that_omits_its_predicate() {
1724 let fixture = partial_fixture();
1725 let spec = QueryBuilder::new()
1726 .r#where("name", ComparisonOp::Eq, json!("Piotr"))
1727 .spec();
1728
1729 let (result, _) = run(&fixture, &spec);
1730
1731 assert_eq!(
1732 result.plan.strategy,
1733 QueryStrategy::Scan,
1734 "Piotr is not an admin, so the index never saw him"
1735 );
1736 assert_eq!(result.plan.candidates, 3);
1737 assert_eq!(result.keys, vec!["user_2".to_string()]);
1738 }
1739
1740 #[test]
1741 fn a_where_scoped_index_answers_a_query_that_repeats_its_predicate() {
1742 let fixture = partial_fixture();
1743 let spec = QueryBuilder::new()
1744 .r#where("name", ComparisonOp::Eq, json!("Ania"))
1745 .r#where("role", ComparisonOp::Eq, json!("admin"))
1746 .spec();
1747
1748 let (result, _) = run(&fixture, &spec);
1749
1750 assert_eq!(result.plan.strategy, QueryStrategy::Exact);
1751 assert_eq!(result.plan.index, Some("users_admin_name".to_string()));
1752 assert_eq!(
1753 result.plan.field, None,
1754 "a scoped index is named by the index, not by its one field"
1755 );
1756 assert_eq!(result.plan.candidates, 1);
1757 assert_eq!(result.keys, vec!["user_1".to_string()]);
1758 }
1759
1760 #[test]
1763 fn candidates_of_an_index_and_of_a_scan_alike_come_out_in_write_order() {
1764 let fixture = fixture(&[maria(), ania(), piotr()]);
1765 let indexed = QueryBuilder::new()
1766 .r#where("role", ComparisonOp::Eq, json!("user"))
1767 .spec();
1768 let scanned = QueryBuilder::new()
1769 .r#where("name", ComparisonOp::StartsWith, json!(""))
1770 .spec();
1771
1772 let (by_index, _) = run(&fixture, &indexed);
1773 let (by_scan, _) = run(&fixture, &scanned);
1774
1775 assert_eq!(
1776 by_index.keys,
1777 vec!["user_3".to_string(), "user_2".to_string()],
1778 "Maria was written first"
1779 );
1780 assert_eq!(
1781 by_scan.keys,
1782 vec!["user_3".to_string(), "user_1".to_string(), "user_2".to_string()],
1783 "the scan follows the log, not the hash of the key"
1784 );
1785 }
1786
1787 #[test]
1790 fn an_overwritten_record_keeps_its_place_on_an_unordered_page() {
1791 let mut fixture = fixture(&[ania(), piotr(), maria()]);
1792 let mut updated = ania();
1793 updated.insert("age".to_string(), json!(32));
1794 overwrite(&mut fixture, &updated, 4);
1795 let scanned = QueryBuilder::new()
1796 .r#where("name", ComparisonOp::StartsWith, json!(""))
1797 .spec();
1798
1799 let (result, _) = run(&fixture, &scanned);
1800
1801 assert_eq!(
1802 result.keys,
1803 vec!["user_1".to_string(), "user_2".to_string(), "user_3".to_string()],
1804 "the update rewrote Ania in place, so she stays first -- not last behind Maria"
1805 );
1806 }
1807
1808 #[test]
1809 fn a_record_written_after_an_overwrite_still_lands_at_the_end_of_the_page() {
1810 let mut fixture = fixture(&[ania(), piotr()]);
1811 let mut updated = ania();
1812 updated.insert("age".to_string(), json!(32));
1813 overwrite(&mut fixture, &updated, 3);
1814 overwrite(&mut fixture, &maria(), 4);
1815 let scanned = QueryBuilder::new()
1816 .r#where("name", ComparisonOp::StartsWith, json!(""))
1817 .spec();
1818
1819 let (result, _) = run(&fixture, &scanned);
1820
1821 assert_eq!(
1822 result.keys,
1823 vec!["user_1".to_string(), "user_2".to_string(), "user_3".to_string()],
1824 "Maria is genuinely new, so she goes last however recently Ania was rewritten"
1825 );
1826 }
1827
1828 #[test]
1829 fn an_empty_fulltext_result_never_falls_back_to_a_scan() {
1830 let fixture = fulltext_fixture();
1831 let spec = QueryBuilder::new().search("zzz_nowhere", SearchMode::All).spec();
1832
1833 let (result, reads) = run(&fixture, &spec);
1834 assert_eq!(result.plan.strategy, QueryStrategy::Fulltext);
1835 assert_eq!(result.plan.candidates, 0);
1836 assert_eq!(
1837 reads, 0,
1838 "the log must not be read when the search already proved an empty result"
1839 );
1840 assert!(result.keys.is_empty());
1841 }
1842
1843 #[test]
1844 fn a_search_left_to_its_own_order_reports_a_ranked_plan() {
1845 let fixture = fulltext_fixture();
1846 let spec = QueryBuilder::new().search("backend developer", SearchMode::All).spec();
1847
1848 let (result, _) = run(&fixture, &spec);
1849
1850 assert!(result.plan.ranked, "the order came out of the relevance scores");
1851 }
1852
1853 #[test]
1854 fn an_explicit_order_beside_a_search_reports_an_unranked_plan() {
1855 let fixture = fulltext_fixture();
1856 let spec = QueryBuilder::new()
1857 .search("backend developer", SearchMode::All)
1858 .order_by("age", SortDirection::Desc)
1859 .spec();
1860
1861 let (result, _) = run(&fixture, &spec);
1862
1863 assert!(
1864 !result.plan.ranked,
1865 "the scores were computed but the explicit order overrode them"
1866 );
1867 }
1868
1869 #[test]
1870 fn a_query_without_a_search_reports_an_unranked_plan() {
1871 let fixture = fixture(&[ania(), piotr(), maria()]);
1872 let spec = QueryBuilder::new()
1873 .r#where("role", ComparisonOp::Eq, json!("user"))
1874 .spec();
1875
1876 let (result, _) = run(&fixture, &spec);
1877
1878 assert!(!result.plan.ranked, "no search ran, so there is nothing to rank by");
1879 }
1880
1881 #[test]
1882 fn a_query_whose_every_field_is_projected_answers_without_reading_a_record() {
1883 let fixture = fixture(&[ania(), piotr(), maria()]);
1884 let spec = QueryBuilder::new()
1885 .r#where("role", ComparisonOp::Eq, json!("user"))
1886 .r#where("age", ComparisonOp::Gt, json!(30))
1887 .spec();
1888
1889 let (result, reads) = run(&fixture, &spec);
1890
1891 assert!(result.plan.filtered_on_projection);
1892 assert_eq!(result.plan.reads, 0);
1893 assert_eq!(reads, 0);
1894 assert_eq!(result.keys, vec!["user_3".to_string()]);
1895 }
1896
1897 #[test]
1898 fn a_query_touching_a_field_outside_the_projection_reads_every_candidate() {
1899 let fixture = fixture(&[ania(), piotr(), maria()]);
1900 let spec = QueryBuilder::new()
1901 .r#where("name", ComparisonOp::Eq, json!("Maria"))
1902 .spec();
1903
1904 let (result, reads) = run(&fixture, &spec);
1905
1906 assert!(
1907 !result.plan.filtered_on_projection,
1908 "`name` carries no index, so it is not projected"
1909 );
1910 assert_eq!(result.plan.reads, 3);
1911 assert_eq!(reads, 3);
1912 assert_eq!(result.keys, vec!["user_3".to_string()]);
1913 }
1914
1915 #[test]
1916 fn a_sort_on_a_field_outside_the_projection_takes_the_query_off_the_projections() {
1917 let fixture = fixture(&[ania(), piotr(), maria()]);
1918 let spec = QueryBuilder::new()
1919 .r#where("role", ComparisonOp::Eq, json!("user"))
1920 .order_by("name", SortDirection::Asc)
1921 .spec();
1922
1923 let (result, _) = run(&fixture, &spec);
1924
1925 assert!(
1926 !result.plan.filtered_on_projection,
1927 "the sort field counts as a field in play"
1928 );
1929 assert_eq!(result.plan.reads, 2);
1930 assert_eq!(
1931 result.keys,
1932 vec!["user_3".to_string(), "user_2".to_string()],
1933 "Maria before Piotr"
1934 );
1935 }
1936
1937 #[test]
1938 fn a_count_only_query_answers_with_the_match_count_and_no_page() {
1939 let fixture = fixture(&[ania(), piotr(), maria()]);
1940 let paged = QueryBuilder::new()
1941 .r#where("role", ComparisonOp::Eq, json!("user"))
1942 .spec();
1943 let counting = paged.clone().counting(true);
1944
1945 let (page, _) = run(&fixture, &paged);
1946 let (count, _) = run(&fixture, &counting);
1947
1948 assert_eq!(
1949 page.keys.len(),
1950 2,
1951 "the same query hands back a page when it is not counting"
1952 );
1953 assert_eq!(count.matched, page.matched);
1954 assert!(
1955 count.keys.is_empty(),
1956 "a count-only query names no page for a caller to load"
1957 );
1958 assert_eq!(
1959 count.plan, page.plan,
1960 "counting changes what is returned, not how it was planned"
1961 );
1962 }
1963
1964 #[test]
1965 fn a_count_only_query_counts_every_match_even_where_a_page_would_be_cut() {
1966 let fixture = fixture(&[ania(), piotr(), maria()]);
1967 let spec = QueryBuilder::new()
1968 .r#where("role", ComparisonOp::Eq, json!("user"))
1969 .spec()
1970 .with_limit(1);
1971
1972 let (count, _) = run(&fixture, &spec.clone().counting(true));
1973
1974 assert_eq!(
1975 count.matched, 2,
1976 "`matched` counts before `limit` cuts, counting or not"
1977 );
1978 assert!(count.keys.is_empty());
1979 }
1980
1981 #[test]
1982 fn an_aggregate_keeps_its_keys_when_the_spec_it_unpages_was_counting() {
1983 let spec = QueryBuilder::new()
1984 .r#where("role", ComparisonOp::Eq, json!("user"))
1985 .spec()
1986 .counting(true);
1987
1988 assert!(!spec.unpaged().count_only);
1990 }
1991}