use std::collections::{HashMap, HashSet};
use std::fmt;
use std::io;
use serde_json::Value;
use crate::errors::HaysonCipherError;
use crate::registry::composite_index::{RegistryIndex, RegistryIndexKind};
use crate::registry::exact_index::value_key;
use crate::registry::fulltext_index::SearchMode;
use crate::registry::sorted_index::{RangeBounds, SortValue};
use crate::registry::table_registry::TableRegistry;
use crate::schema::{HaysonRecord, UNPARTITIONED};
use crate::storage::record::parse_entry;
use crate::storage::{LogFile, LogSet, Slot};
use super::predicates::{all_leaves, leaves_of, same_value, test_node, ComparisonOp, Predicate, PredicateNode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryStrategy {
Primary,
Exact,
Sorted,
Fulltext,
Union,
Scan,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlanBranch {
pub strategy: QueryStrategy,
pub field: Option<String>,
pub candidates: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AccessPath {
pub strategy: QueryStrategy,
pub field: Option<String>,
pub index: Option<String>,
pub keys: Vec<String>,
pub branches: Option<Vec<PlanBranch>>,
pub scores: Option<HashMap<String, f64>>,
pub overlay: Option<PlanOverlay>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverlayStrategy {
Exact,
Fulltext,
Scan,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlanOverlay {
pub staged: usize,
pub suppressed: usize,
pub rescored: bool,
pub strategy: OverlayStrategy,
pub size: usize,
}
pub enum OverlayCandidates {
NotIndexed,
Narrowed(Vec<String>),
}
pub trait QueryOverlay {
fn records(&self) -> &HashMap<String, HaysonRecord>;
fn deleted(&self) -> &HashSet<String>;
fn candidates_for(&self, field: &str, encoded: &str) -> OverlayCandidates;
}
#[derive(Debug, Clone, PartialEq)]
pub struct SearchSpec {
pub text: String,
pub mode: SearchMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortDirection {
Asc,
Desc,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OrderBy {
pub field: String,
pub direction: SortDirection,
}
#[derive(Debug, Clone, PartialEq)]
pub struct QuerySpec {
pub root: PredicateNode,
pub search: Option<SearchSpec>,
pub order_by: Option<OrderBy>,
pub limit: Option<usize>,
pub offset: Option<usize>,
pub count_only: bool,
}
impl QuerySpec {
pub fn new(root: PredicateNode) -> Self {
QuerySpec {
root,
search: None,
order_by: None,
limit: None,
offset: None,
count_only: false,
}
}
pub fn with_search(mut self, text: impl Into<String>, mode: SearchMode) -> Self {
self.search = Some(SearchSpec {
text: text.into(),
mode,
});
self
}
pub fn with_order_by(mut self, field: impl Into<String>, direction: SortDirection) -> Self {
self.order_by = Some(OrderBy {
field: field.into(),
direction,
});
self
}
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
pub fn counting(mut self, count_only: bool) -> Self {
self.count_only = count_only;
self
}
pub fn unpaged(&self) -> Self {
QuerySpec {
limit: None,
offset: None,
count_only: false,
..self.clone()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct KeySet {
strategy: QueryStrategy,
field: Option<String>,
index: Option<String>,
keys: Vec<String>,
branches: Option<Vec<PlanBranch>>,
}
impl KeySet {
fn into_path(self, scores: Option<HashMap<String, f64>>) -> AccessPath {
AccessPath {
strategy: self.strategy,
field: self.field,
index: self.index,
keys: self.keys,
branches: self.branches,
scores,
overlay: None,
}
}
}
fn cheaper(best: Option<KeySet>, candidate: Option<KeySet>) -> Option<KeySet> {
match candidate {
None => best,
Some(candidate) => match best {
None => Some(candidate),
Some(best) => {
if candidate.keys.len() < best.keys.len() {
Some(candidate)
} else {
Some(best)
}
}
},
}
}
fn is_primitive(value: &Value) -> bool {
matches!(value, Value::String(_) | Value::Number(_) | Value::Bool(_))
}
fn is_primitive_equality(predicate: &Predicate) -> bool {
predicate.op == ComparisonOp::Eq && is_primitive(&predicate.value)
}
fn equality_on<'a>(leaves: &[&'a Predicate], field: &str) -> Option<&'a Predicate> {
let usable: Vec<&'a Predicate> = leaves
.iter()
.copied()
.filter(|leaf| {
leaf.field == field
&& (leaf.op == ComparisonOp::Eq || leaf.op == ComparisonOp::Contains)
&& leaf.value != Value::Null
})
.collect();
usable
.iter()
.find(|leaf| leaf.op == ComparisonOp::Eq)
.copied()
.or_else(|| usable.first().copied())
}
struct ExactMatch {
keys: Vec<String>,
width: usize,
}
fn in_write_order(registry: &TableRegistry, mut keys: Vec<String>) -> Vec<String> {
keys.sort_by(|left, right| {
let earlier = registry.arrival_of(left).unwrap_or(0);
let later = registry.arrival_of(right).unwrap_or(0);
earlier.cmp(&later).then_with(|| left.cmp(right))
});
keys
}
fn repeats_predicate(registry: &TableRegistry, index: &RegistryIndex, leaves: &[&Predicate]) -> bool {
let Some(clauses) = registry.where_of(index) else {
return true;
};
clauses.iter().all(|clause| {
leaves
.iter()
.any(|leaf| leaf.field == clause.field && leaf.op == clause.op && same_value(&leaf.value, &clause.value))
})
}
fn exact_match(registry: &TableRegistry, index: &RegistryIndex, leaves: &[&Predicate]) -> Option<ExactMatch> {
if index.kind != RegistryIndexKind::Exact && index.kind != RegistryIndexKind::Sorted {
return None;
}
let mut values: Vec<Value> = Vec::new();
let mut proves = registry.where_of(index).is_none();
for field in &index.fields {
match equality_on(leaves, field) {
Some(predicate) => {
values.push(predicate.value.clone());
if !is_primitive_equality(predicate) {
proves = false;
}
}
None => break,
}
}
if values.is_empty() {
return None;
}
let width = values.len();
match registry.candidates_of(index, &values) {
Some(bucket) => Some(ExactMatch {
keys: in_write_order(registry, bucket.iter().cloned().collect()),
width,
}),
None => proves.then(|| ExactMatch {
keys: Vec::new(),
width,
}),
}
}
#[derive(Default)]
struct RawBounds {
gt: Option<Value>,
gte: Option<Value>,
lt: Option<Value>,
lte: Option<Value>,
}
fn bounds_of(leaves: &[&Predicate], field: &str) -> Option<RawBounds> {
let mut bounds = RawBounds::default();
let mut found = false;
for predicate in leaves {
if predicate.field != field {
continue;
}
if !matches!(predicate.value, Value::Number(_) | Value::String(_)) {
continue;
}
match predicate.op {
ComparisonOp::Gt => {
bounds.gt = Some(predicate.value.clone());
found = true;
}
ComparisonOp::Gte => {
bounds.gte = Some(predicate.value.clone());
found = true;
}
ComparisonOp::Lt => {
bounds.lt = Some(predicate.value.clone());
found = true;
}
ComparisonOp::Lte => {
bounds.lte = Some(predicate.value.clone());
found = true;
}
_ => {}
}
}
found.then_some(bounds)
}
fn with_prefix(prefix: &[SortValue], bounds: &RawBounds) -> RangeBounds {
let vector_of = |edge: &Option<Value>| -> Option<Vec<SortValue>> {
let value = edge.as_ref()?;
let sortable = SortValue::from_json(value)?;
let mut vector = prefix.to_vec();
vector.push(sortable);
Some(vector)
};
RangeBounds {
gt: vector_of(&bounds.gt),
gte: vector_of(&bounds.gte),
lt: vector_of(&bounds.lt),
lte: vector_of(&bounds.lte),
}
}
fn sorted_keys(registry: &TableRegistry, index: &RegistryIndex, leaves: &[&Predicate]) -> Option<Vec<String>> {
if index.kind != RegistryIndexKind::Sorted {
return None;
}
let mut prefix: Vec<SortValue> = Vec::new();
for field in &index.fields {
let predicate = leaves
.iter()
.find(|leaf| leaf.field == *field && is_primitive_equality(leaf));
match predicate {
Some(predicate) => prefix.push(SortValue::from_json(&predicate.value)?),
None => break,
}
}
let open = index.fields.get(prefix.len())?;
let bounds = bounds_of(leaves, open)?;
let extended = with_prefix(&prefix, &bounds);
registry.range_of(index, &extended)
}
fn key_set_of(registry: &TableRegistry, index: &RegistryIndex, strategy: QueryStrategy, keys: Vec<String>) -> KeySet {
let named = index.fields.len() > 1 || registry.where_of(index).is_some();
KeySet {
strategy,
field: (!named).then(|| index.fields[0].clone()),
index: named.then(|| index.name.clone()),
keys,
branches: None,
}
}
fn primary_keys_of(leaves: &[&Predicate], primary_key: &str) -> Option<Vec<String>> {
for predicate in leaves {
if predicate.field != primary_key {
continue;
}
match (predicate.op, &predicate.value) {
(ComparisonOp::Eq, Value::String(s)) => return Some(vec![s.clone()]),
(ComparisonOp::Eq, Value::Number(n)) => return Some(vec![n.to_string()]),
(ComparisonOp::In, Value::Array(items)) => {
return Some(items.iter().map(value_as_key).collect());
}
_ => {}
}
}
None
}
fn value_as_key(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn primary_path(registry: &TableRegistry, leaves: &[&Predicate]) -> Option<KeySet> {
let wanted = primary_keys_of(leaves, ®istry.schema.primary_key)?;
let keys: Vec<String> = wanted
.into_iter()
.filter(|key| registry.slot_of(key).is_some())
.collect();
Some(KeySet {
strategy: QueryStrategy::Primary,
field: Some(registry.schema.primary_key.clone()),
index: None,
keys,
branches: None,
})
}
fn fulltext_path(scores: &HashMap<String, f64>) -> KeySet {
let mut keys: Vec<String> = scores.keys().cloned().collect();
keys.sort_by(|a, b| rank(scores, a, b));
KeySet {
strategy: QueryStrategy::Fulltext,
field: None,
index: None,
keys,
branches: None,
}
}
fn rank(scores: &HashMap<String, f64>, a: &str, b: &str) -> std::cmp::Ordering {
let left = scores.get(a).copied().unwrap_or(0.0);
let right = scores.get(b).copied().unwrap_or(0.0);
right
.partial_cmp(&left)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.cmp(b))
}
fn indexed_path(
registry: &TableRegistry,
nodes: &[PredicateNode],
scores: Option<&HashMap<String, f64>>,
) -> Option<KeySet> {
let leaves = leaves_of(nodes);
let mut best: Option<KeySet> = None;
let mut widest = 0usize;
let usable: Vec<&RegistryIndex> = registry
.indexes()
.iter()
.filter(|index| repeats_predicate(registry, index, &leaves))
.collect();
for index in &usable {
let Some(matched) = exact_match(registry, index, &leaves) else {
continue;
};
let tied = best.as_ref().is_some_and(|b| matched.keys.len() == b.keys.len());
let cheaper_than_best = best.as_ref().is_none_or(|b| matched.keys.len() < b.keys.len());
if tied {
if matched.width > widest {
widest = matched.width;
best = Some(key_set_of(registry, index, QueryStrategy::Exact, matched.keys));
}
} else if cheaper_than_best {
widest = matched.width;
best = Some(key_set_of(registry, index, QueryStrategy::Exact, matched.keys));
}
}
if let Some(scores) = scores {
best = cheaper(best, Some(fulltext_path(scores)));
}
for index in &usable {
let Some(keys) = sorted_keys(registry, index, &leaves) else {
continue;
};
best = cheaper(best, Some(key_set_of(registry, index, QueryStrategy::Sorted, keys)));
}
for node in nodes {
if matches!(node, PredicateNode::Group { .. }) {
best = cheaper(best, plan(registry, node, None));
}
}
best
}
fn and_path(
registry: &TableRegistry,
nodes: &[PredicateNode],
scores: Option<&HashMap<String, f64>>,
) -> Option<KeySet> {
let leaves = leaves_of(nodes);
cheaper(primary_path(registry, &leaves), indexed_path(registry, nodes, scores))
}
fn branches_of(path: &KeySet) -> Vec<PlanBranch> {
path.branches.clone().unwrap_or_else(|| {
vec![PlanBranch {
strategy: path.strategy,
field: path.field.clone(),
candidates: path.keys.len(),
}]
})
}
fn or_path(registry: &TableRegistry, nodes: &[PredicateNode]) -> Option<KeySet> {
if nodes.is_empty() {
return None;
}
let mut seen = HashSet::new();
let mut keys = Vec::new();
let mut branches = Vec::new();
for node in nodes {
let path = plan(registry, node, None)?;
for key in &path.keys {
if seen.insert(key.clone()) {
keys.push(key.clone());
}
}
branches.extend(branches_of(&path));
}
Some(KeySet {
strategy: QueryStrategy::Union,
field: None,
index: None,
keys,
branches: Some(branches),
})
}
fn plan(registry: &TableRegistry, node: &PredicateNode, scores: Option<&HashMap<String, f64>>) -> Option<KeySet> {
match node {
PredicateNode::Leaf(_) => and_path(registry, std::slice::from_ref(node), scores),
PredicateNode::Group {
combine: super::predicates::Combine::And,
nodes,
} => and_path(registry, nodes, scores),
PredicateNode::Group {
combine: super::predicates::Combine::Or,
nodes,
} => or_path(registry, nodes),
}
}
fn committed_path(registry: &TableRegistry, spec: &QuerySpec, scores: Option<&HashMap<String, f64>>) -> KeySet {
plan(registry, &spec.root, scores).unwrap_or_else(|| KeySet {
strategy: QueryStrategy::Scan,
field: None,
index: None,
keys: in_write_order(registry, registry.keys().cloned().collect()),
branches: None,
})
}
pub fn plan_access(registry: &TableRegistry, spec: &QuerySpec) -> AccessPath {
let scores = spec
.search
.as_ref()
.filter(|_| registry.has_fulltext())
.map(|search| registry.search(&search.text, search.mode));
committed_path(registry, spec, scores.as_ref()).into_path(scores)
}
fn top_level_leaves(spec: &QuerySpec) -> Vec<&Predicate> {
match &spec.root {
PredicateNode::Leaf(leaf) => vec![leaf],
PredicateNode::Group {
combine: super::predicates::Combine::And,
nodes,
} => nodes
.iter()
.filter_map(|node| match node {
PredicateNode::Leaf(leaf) => Some(leaf),
PredicateNode::Group { .. } => None,
})
.collect(),
PredicateNode::Group { .. } => Vec::new(),
}
}
fn indexed_staged(overlay: &dyn QueryOverlay, spec: &QuerySpec) -> Option<Vec<String>> {
let mut best: Option<Vec<String>> = None;
for leaf in top_level_leaves(spec) {
if !is_primitive_equality(leaf) {
continue;
}
let OverlayCandidates::Narrowed(keys) = overlay.candidates_for(&leaf.field, &value_key(&leaf.value)) else {
continue;
};
if best.as_ref().is_none_or(|found| keys.len() < found.len()) {
best = Some(keys);
}
}
best
}
fn staged_candidates(
path: &KeySet,
overlay: &dyn QueryOverlay,
spec: &QuerySpec,
scores: Option<&HashMap<String, f64>>,
) -> (Vec<String>, OverlayStrategy) {
if path.strategy == QueryStrategy::Fulltext {
if let Some(scores) = scores {
let mut keys: Vec<String> = overlay
.records()
.keys()
.filter(|key| scores.contains_key(*key))
.cloned()
.collect();
keys.sort_by(|a, b| rank(scores, a, b));
return (keys, OverlayStrategy::Fulltext);
}
}
match indexed_staged(overlay, spec) {
Some(keys) => (keys, OverlayStrategy::Exact),
None => (overlay.records().keys().cloned().collect(), OverlayStrategy::Scan),
}
}
pub fn plan_access_overlaid(
registry: &TableRegistry,
spec: &QuerySpec,
scores: Option<HashMap<String, f64>>,
overlay: &dyn QueryOverlay,
) -> AccessPath {
let path = committed_path(registry, spec, scores.as_ref());
let kept: Vec<String> = path
.keys
.iter()
.filter(|key| !overlay.records().contains_key(*key) && !overlay.deleted().contains(*key))
.cloned()
.collect();
let suppressed = path.keys.len() - kept.len();
let (staged, strategy) = staged_candidates(&path, overlay, spec, scores.as_ref());
let summary = PlanOverlay {
staged: staged.len(),
suppressed,
rescored: scores.is_some(),
strategy,
size: overlay.records().len(),
};
let mut keys = kept;
keys.extend(staged);
AccessPath {
keys,
overlay: Some(summary),
..path.into_path(scores)
}
}
pub fn order_and_page(
entries: Vec<(String, Value)>,
spec: &QuerySpec,
scores: Option<&HashMap<String, f64>>,
) -> Vec<String> {
let mut entries = entries;
match (&spec.order_by, scores) {
(Some(order), _) => {
let descending = order.direction == SortDirection::Desc;
entries.sort_by(|(_, left), (_, right)| {
let ordering = super::predicates::compare_values(left, right);
if descending {
ordering.reverse()
} else {
ordering
}
});
}
(None, Some(scores)) => entries.sort_by(|(left, _), (right, _)| rank(scores, left, right)),
(None, None) => {}
}
page_of(entries.into_iter().map(|(key, _)| key).collect(), spec)
}
pub fn page_of(keys: Vec<String>, spec: &QuerySpec) -> Vec<String> {
if spec.offset.is_none() && spec.limit.is_none() {
return keys;
}
let from = spec.offset.unwrap_or(0).min(keys.len());
let to = match spec.limit {
Some(limit) => from.saturating_add(limit).min(keys.len()),
None => keys.len(),
};
keys[from..to].to_vec()
}
#[derive(Debug)]
pub enum RecordSourceError {
Cipher(Box<HaysonCipherError>),
Io(io::Error),
}
impl fmt::Display for RecordSourceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RecordSourceError::Cipher(error) => error.fmt(f),
RecordSourceError::Io(error) => error.fmt(f),
}
}
}
impl std::error::Error for RecordSourceError {}
impl From<io::Error> for RecordSourceError {
fn from(value: io::Error) -> Self {
RecordSourceError::Io(value)
}
}
impl From<HaysonCipherError> for RecordSourceError {
fn from(value: HaysonCipherError) -> Self {
RecordSourceError::Cipher(Box::new(value))
}
}
pub trait RecordSource {
fn load(&mut self, key: &str) -> Result<Option<HaysonRecord>, RecordSourceError>;
}
pub type OpenRecordFn<'a> = dyn Fn(&str, HaysonRecord) -> Result<HaysonRecord, RecordSourceError> + 'a;
#[allow(dead_code)]
pub struct LogRecordSource<'a> {
registry: &'a TableRegistry,
log: &'a LogFile,
open: Option<&'a OpenRecordFn<'a>>,
}
#[allow(dead_code)]
impl<'a> LogRecordSource<'a> {
pub fn new(registry: &'a TableRegistry, log: &'a LogFile) -> Self {
LogRecordSource {
registry,
log,
open: None,
}
}
pub fn with_open(registry: &'a TableRegistry, log: &'a LogFile, open: &'a OpenRecordFn<'a>) -> Self {
LogRecordSource {
registry,
log,
open: Some(open),
}
}
}
impl RecordSource for LogRecordSource<'_> {
fn load(&mut self, key: &str) -> Result<Option<HaysonRecord>, RecordSourceError> {
let Some(entry) = self.registry.slot_of(key) else {
return Ok(None);
};
let slot = Slot {
offset: entry.offset,
length: entry.length,
};
let line = self.log.read_slice(&slot)?;
let path = self.log.path().to_string_lossy().into_owned();
let parsed =
parse_entry(&line, &path, entry.offset).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
match (parsed.data, self.open) {
(Some(record), Some(open)) => Ok(Some(open(key, record)?)),
(data, _) => Ok(data),
}
}
}
pub struct LogSetRecordSource<'a> {
registry: &'a TableRegistry,
logs: &'a LogSet,
open: Option<&'a OpenRecordFn<'a>>,
}
impl<'a> LogSetRecordSource<'a> {
pub fn new(registry: &'a TableRegistry, logs: &'a LogSet) -> Self {
LogSetRecordSource {
registry,
logs,
open: None,
}
}
pub fn with_open(registry: &'a TableRegistry, logs: &'a LogSet, open: &'a OpenRecordFn<'a>) -> Self {
LogSetRecordSource {
registry,
logs,
open: Some(open),
}
}
}
impl RecordSource for LogSetRecordSource<'_> {
fn load(&mut self, key: &str) -> Result<Option<HaysonRecord>, RecordSourceError> {
let Some(entry) = self.registry.slot_of(key) else {
return Ok(None);
};
let part = self.registry.part_of(key).unwrap_or(UNPARTITIONED);
let slot = Slot {
offset: entry.offset,
length: entry.length,
};
let line = self.logs.read_slice(part, &slot)?;
let path = self.logs.path_of(part).to_string_lossy().into_owned();
let parsed =
parse_entry(&line, &path, entry.offset).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
match (parsed.data, self.open) {
(Some(record), Some(open)) => Ok(Some(open(key, record)?)),
(data, _) => Ok(data),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryResult {
pub keys: Vec<String>,
pub matched: usize,
pub plan: AccessPathSummary,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccessPathSummary {
pub strategy: QueryStrategy,
pub field: Option<String>,
pub index: Option<String>,
pub candidates: usize,
pub ranked: bool,
pub filtered_on_projection: bool,
pub reads: usize,
pub branches: Option<Vec<PlanBranch>>,
pub overlay: Option<PlanOverlay>,
}
fn fields_in_play(spec: &QuerySpec) -> Vec<&str> {
let mut fields: Vec<&str> = all_leaves(std::slice::from_ref(&spec.root))
.iter()
.map(|leaf| leaf.field.as_str())
.collect();
if let Some(order) = &spec.order_by {
fields.push(order.field.as_str());
}
fields
}
fn scored_in(access: &AccessPath, spec: &QuerySpec, key: &str) -> bool {
if spec.search.is_none() {
return true;
}
access.scores.as_ref().is_some_and(|scores| scores.contains_key(key))
}
fn matched_entry(
record: &HaysonRecord,
key: &str,
spec: &QuerySpec,
sort_field: Option<&str>,
) -> Option<(String, Value)> {
if !test_node(record, &spec.root) {
return None;
}
let sort_value = sort_field.map_or(Value::Null, |field| record.get(field).cloned().unwrap_or(Value::Null));
Some((key.to_string(), sort_value))
}
pub fn execute_query<S: RecordSource + ?Sized>(
registry: &TableRegistry,
source: &mut S,
spec: &QuerySpec,
) -> Result<QueryResult, RecordSourceError> {
let access = plan_access(registry, spec);
execute_access(registry, source, spec, access, None)
}
pub fn execute_access<S: RecordSource + ?Sized>(
registry: &TableRegistry,
source: &mut S,
spec: &QuerySpec,
access: AccessPath,
overlay: Option<&dyn QueryOverlay>,
) -> Result<QueryResult, RecordSourceError> {
let sort_field = spec.order_by.as_ref().map(|order| order.field.as_str());
let on_projection = fields_in_play(spec)
.into_iter()
.all(|field| registry.is_projected(field));
let mut matched: Vec<(String, Value)> = Vec::new();
let mut reads = 0usize;
for key in &access.keys {
let staged = overlay.and_then(|overlay| overlay.records().get(key));
let entry = match staged {
Some(_) if !scored_in(&access, spec, key) => None,
Some(record) => matched_entry(record, key, spec, sort_field),
None if on_projection => registry
.projection_of(key)
.and_then(|projection| matched_entry(projection, key, spec, sort_field)),
None => match source.load(key)? {
Some(record) => {
reads += 1;
matched_entry(&record, key, spec, sort_field)
}
None => None,
},
};
if let Some(entry) = entry {
matched.push(entry);
}
}
let total = matched.len();
let keys = if spec.count_only {
Vec::new()
} else {
order_and_page(matched, spec, access.scores.as_ref())
};
Ok(QueryResult {
keys,
matched: total,
plan: AccessPathSummary {
strategy: access.strategy,
field: access.field,
index: access.index,
candidates: access.keys.len(),
ranked: access.scores.is_some() && spec.order_by.is_none(),
filtered_on_projection: on_projection,
reads,
branches: access.branches,
overlay: access.overlay,
},
})
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::query::builder::QueryBuilder;
use crate::schema::parse_schema;
use crate::storage::record::{serialize_entry, LogMeta, LogOp};
struct CountingSource<'a> {
inner: LogRecordSource<'a>,
reads: usize,
}
impl RecordSource for CountingSource<'_> {
fn load(&mut self, key: &str) -> Result<Option<HaysonRecord>, RecordSourceError> {
self.reads += 1;
self.inner.load(key)
}
}
fn users_schema() -> crate::schema::TableSchema {
parse_schema(&json!({
"schemaVersion": 1,
"table": "users",
"primaryKey": "id",
"fields": {
"id": { "type": "string" },
"name": { "type": "string" },
"age": { "type": "int", "index": "sorted" },
"role": { "type": "string", "index": "exact" },
"tags": { "type": "string[]", "index": "exact" },
},
}))
.unwrap()
}
fn record(pairs: &[(&str, Value)]) -> HaysonRecord {
pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
}
fn ania() -> HaysonRecord {
record(&[
("id", json!("user_1")),
("name", json!("Ania")),
("age", json!(31)),
("role", json!("admin")),
("tags", json!(["backend", "node"])),
])
}
fn piotr() -> HaysonRecord {
record(&[
("id", json!("user_2")),
("name", json!("Piotr")),
("age", json!(24)),
("role", json!("user")),
("tags", json!(["frontend"])),
])
}
fn maria() -> HaysonRecord {
record(&[
("id", json!("user_3")),
("name", json!("Maria")),
("age", json!(41)),
("role", json!("user")),
("tags", json!(["backend", "go"])),
])
}
struct Fixture {
_dir: tempfile::TempDir,
registry: TableRegistry,
log: LogFile,
}
fn fixture_with_schema(schema: crate::schema::TableSchema, records: &[HaysonRecord]) -> Fixture {
fixture_of(TableRegistry::new(schema), records)
}
fn fixture_of(registry: TableRegistry, records: &[HaysonRecord]) -> Fixture {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("users.jsonl");
let mut log = LogFile::open(&path, false).unwrap();
let mut registry = registry;
let mut seq = 0u64;
for entry in records {
seq += 1;
let key = entry.get("id").and_then(Value::as_str).unwrap().to_string();
let line = serialize_entry(
&LogMeta {
key: key.clone(),
op: LogOp::Put,
seq,
ts: seq as i64,
},
Some(entry),
);
let slots = log.append(&[line]).unwrap();
registry.apply_put(&key, entry, slots[0], seq);
}
Fixture {
_dir: dir,
registry,
log,
}
}
fn fixture(records: &[HaysonRecord]) -> Fixture {
fixture_with_schema(users_schema(), records)
}
fn fulltext_users_schema() -> crate::schema::TableSchema {
parse_schema(&json!({
"schemaVersion": 1,
"table": "users",
"primaryKey": "id",
"fields": {
"id": { "type": "string" },
"name": { "type": "string" },
"bio": { "type": "string", "fulltext": true },
"age": { "type": "int", "index": "sorted" },
"role": { "type": "string", "index": "exact" },
},
}))
.unwrap()
}
fn ania_bio() -> HaysonRecord {
record(&[
("id", json!("user_1")),
("name", json!("Ania")),
("bio", json!("Backend developer, Łódź")),
("age", json!(31)),
("role", json!("admin")),
])
}
fn piotr_bio() -> HaysonRecord {
record(&[
("id", json!("user_2")),
("name", json!("Piotr")),
("bio", json!("Frontend developer from Warsaw")),
("age", json!(24)),
("role", json!("user")),
])
}
fn maria_bio() -> HaysonRecord {
record(&[
("id", json!("user_3")),
("name", json!("Maria")),
("bio", json!("Backend developer, Kraków")),
("age", json!(41)),
("role", json!("user")),
])
}
fn fulltext_fixture() -> Fixture {
fixture_with_schema(fulltext_users_schema(), &[ania_bio(), piotr_bio(), maria_bio()])
}
fn overwrite(fixture: &mut Fixture, entry: &HaysonRecord, seq: u64) {
let key = entry.get("id").and_then(Value::as_str).unwrap().to_string();
let line = serialize_entry(
&LogMeta {
key: key.clone(),
op: LogOp::Put,
seq,
ts: seq as i64,
},
Some(entry),
);
let slots = fixture.log.append(&[line]).unwrap();
fixture.registry.apply_put(&key, entry, slots[0], seq);
}
fn run(fixture: &Fixture, spec: &QuerySpec) -> (QueryResult, usize) {
let mut source = CountingSource {
inner: LogRecordSource::new(&fixture.registry, &fixture.log),
reads: 0,
};
let result = execute_query(&fixture.registry, &mut source, spec).unwrap();
(result, source.reads)
}
#[test]
fn uses_the_exact_index_for_a_single_predicate() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("user"))
.spec();
let (result, reads) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Exact);
assert_eq!(result.plan.field, Some("role".to_string()));
assert_eq!(result.plan.candidates, 2);
assert_eq!(reads, 0, "`role` is projected, so the predicate never leaves memory");
let mut ids = result.keys.clone();
ids.sort();
assert_eq!(ids, vec!["user_2", "user_3"]);
}
#[test]
fn narrows_through_the_index_then_filters_the_rest_in_memory() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("user"))
.r#where("age", ComparisonOp::Gt, json!(30))
.spec();
let (result, reads) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Exact);
assert_eq!(result.plan.candidates, 2);
assert_eq!(
reads, 0,
"`role` and `age` are both projected — the second filter runs on projections too"
);
assert_eq!(result.keys, vec!["user_3".to_string()]);
}
#[test]
fn picks_the_more_selective_index_when_more_than_one_applies() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("admin"))
.r#where("age", ComparisonOp::Gt, json!(10))
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Exact);
assert_eq!(result.plan.field, Some("role".to_string()));
assert_eq!(result.plan.candidates, 1);
}
#[test]
fn unions_the_branches_of_an_or_bracket() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.where_any(|group| {
group.r#where("role", ComparisonOp::Eq, json!("admin")).r#where(
"tags",
ComparisonOp::Contains,
json!("go"),
);
})
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Union);
assert_eq!(result.plan.candidates, 2);
assert_eq!(
result.plan.branches,
Some(vec![
PlanBranch {
strategy: QueryStrategy::Exact,
field: Some("role".to_string()),
candidates: 1
},
PlanBranch {
strategy: QueryStrategy::Exact,
field: Some("tags".to_string()),
candidates: 1
},
])
);
let mut ids = result.keys.clone();
ids.sort();
assert_eq!(ids, vec!["user_1", "user_3"]);
}
#[test]
fn nests_an_and_bracket_inside_an_or_bracket() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.where_any(|group| {
group
.r#where("role", ComparisonOp::Eq, json!("admin"))
.where_all(|inner| {
inner.r#where("role", ComparisonOp::Eq, json!("user")).r#where(
"age",
ComparisonOp::Gt,
json!(35),
);
});
})
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Union);
assert_eq!(
result.plan.branches,
Some(vec![
PlanBranch {
strategy: QueryStrategy::Exact,
field: Some("role".to_string()),
candidates: 1
},
PlanBranch {
strategy: QueryStrategy::Sorted,
field: Some("age".to_string()),
candidates: 1
},
])
);
let mut ids = result.keys.clone();
ids.sort();
assert_eq!(ids, vec!["user_1", "user_3"]);
}
#[test]
fn degrades_the_whole_bracket_to_a_scan_when_one_branch_has_no_index() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.where_any(|group| {
group.r#where("role", ComparisonOp::Eq, json!("admin")).r#where(
"name",
ComparisonOp::StartsWith,
json!("Piotr"),
);
})
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Scan);
assert_eq!(result.plan.candidates, 3);
assert_eq!(result.plan.branches, None);
let mut ids = result.keys.clone();
ids.sort();
assert_eq!(ids, vec!["user_1", "user_2"]);
}
#[test]
fn an_empty_index_bucket_never_falls_back_to_a_scan() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("ghost"))
.spec();
let (result, reads) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Exact);
assert_eq!(result.plan.field, Some("role".to_string()));
assert_eq!(result.plan.candidates, 0);
assert_eq!(
reads, 0,
"the log must not be read when the index already proved an empty result"
);
assert!(result.keys.is_empty());
}
#[test]
fn falls_back_to_a_scan_when_no_registry_can_help() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("name", ComparisonOp::StartsWith, json!("A"))
.spec();
let (result, reads) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Scan);
assert_eq!(result.plan.candidates, 3);
assert_eq!(reads, 3);
assert_eq!(result.keys, vec!["user_1".to_string()]);
}
#[test]
fn reaches_the_primary_registry_from_a_top_level_equality() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("id", ComparisonOp::Eq, json!("user_2"))
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Primary);
assert_eq!(result.plan.candidates, 1);
assert_eq!(result.keys, vec!["user_2".to_string()]);
}
#[test]
fn uses_the_fulltext_index_for_a_search_query() {
let fixture = fulltext_fixture();
let spec = QueryBuilder::new().search("backend developer", SearchMode::All).spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Fulltext);
assert_eq!(result.plan.field, None);
assert_eq!(result.plan.candidates, 2);
let mut ids = result.keys.clone();
ids.sort();
assert_eq!(ids, vec!["user_1", "user_3"]);
}
#[test]
fn narrows_a_fulltext_search_with_an_in_memory_predicate() {
let fixture = fulltext_fixture();
let spec = QueryBuilder::new()
.search("backend developer", SearchMode::All)
.r#where("name", ComparisonOp::StartsWith, json!("A"))
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Fulltext);
assert_eq!(result.plan.candidates, 2);
assert_eq!(result.keys, vec!["user_1".to_string()]);
}
#[test]
fn matches_a_direct_registry_search_call() {
let fixture = fulltext_fixture();
let spec = QueryBuilder::new().search("backend developer", SearchMode::All).spec();
let (result, _) = run(&fixture, &spec);
let mut via_planner = result.keys.clone();
via_planner.sort();
let mut direct: Vec<String> = fixture
.registry
.search("backend developer", SearchMode::All)
.into_keys()
.collect();
direct.sort();
assert_eq!(via_planner, direct);
}
#[test]
fn orders_the_result_by_a_field_ascending_and_descending() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let ascending = QueryBuilder::new().order_by("age", SortDirection::Asc).spec();
let descending = QueryBuilder::new().order_by("age", SortDirection::Desc).spec();
let (up, _) = run(&fixture, &ascending);
let (down, _) = run(&fixture, &descending);
assert_eq!(up.keys, vec!["user_2", "user_1", "user_3"], "24, 31, 41");
assert_eq!(down.keys, vec!["user_3", "user_1", "user_2"], "41, 31, 24");
}
#[test]
fn a_page_is_cut_out_of_the_ordered_result_only_after_the_predicate_narrowed_it() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("age", ComparisonOp::Gt, json!(24))
.order_by("age", SortDirection::Asc)
.limit(1)
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.keys, vec!["user_1"], "31 is the youngest of those older than 24");
assert_eq!(result.matched, 2);
}
#[test]
fn an_offset_and_a_limit_together_select_a_middle_window_of_the_order() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.order_by("age", SortDirection::Asc)
.offset(1)
.limit(1)
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.keys, vec!["user_1"]);
assert_eq!(
result.matched, 3,
"the page is one key wide, the match count counts all three"
);
}
#[test]
fn an_offset_past_the_end_and_a_limit_of_zero_both_answer_an_empty_page() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let past_the_end = QueryBuilder::new()
.order_by("age", SortDirection::Asc)
.offset(99)
.spec();
let nothing = QueryBuilder::new().order_by("age", SortDirection::Asc).limit(0).spec();
let (skipped, _) = run(&fixture, &past_the_end);
let (none, _) = run(&fixture, ¬hing);
assert!(skipped.keys.is_empty());
assert_eq!(skipped.matched, 3);
assert!(none.keys.is_empty());
assert_eq!(none.matched, 3);
}
#[test]
fn a_page_window_over_an_empty_result_stays_empty() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("ghost"))
.order_by("age", SortDirection::Asc)
.offset(2)
.limit(5)
.spec();
let (result, _) = run(&fixture, &spec);
assert!(result.keys.is_empty());
assert_eq!(result.matched, 0);
}
#[test]
fn a_tie_on_the_sort_field_keeps_every_matched_key_exactly_once() {
let mut twin = piotr();
twin.insert("id".to_string(), json!("user_4"));
twin.insert("age".to_string(), json!(41));
let fixture = fixture(&[ania(), piotr(), maria(), twin]);
let spec = QueryBuilder::new().order_by("age", SortDirection::Asc).spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.keys.len(), 4);
assert_eq!(
&result.keys[..2],
["user_2".to_string(), "user_1".to_string()],
"24 then 31"
);
let mut tied = result.keys[2..].to_vec();
tied.sort();
assert_eq!(
tied,
vec!["user_3", "user_4"],
"both 41s are present, in whichever order"
);
}
#[test]
fn a_fulltext_search_ranks_by_score_before_the_page_is_cut() {
let fixture = fulltext_fixture();
let ranked = QueryBuilder::new().search("backend developer", SearchMode::All).spec();
let first = QueryBuilder::new()
.search("backend developer", SearchMode::All)
.limit(1)
.spec();
let (all, _) = run(&fixture, &ranked);
let (page, _) = run(&fixture, &first);
assert_eq!(
page.keys,
all.keys[..1],
"the page is the head of the ranking, not an arbitrary hit"
);
assert_eq!(page.matched, 2);
}
#[test]
fn an_explicit_order_replaces_the_fulltext_ranking() {
let fixture = fulltext_fixture();
let spec = QueryBuilder::new()
.search("backend developer", SearchMode::All)
.order_by("age", SortDirection::Desc)
.limit(1)
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(
result.keys,
vec!["user_3"],
"Maria is 41 — the ranking would have led with user_1"
);
}
#[test]
fn an_aggregate_folds_every_match_and_not_just_the_page() {
use crate::query::aggregate::execute_aggregate;
let fixture = fixture(&[ania(), piotr(), maria()]);
let (spec, aggregate) = QueryBuilder::new().count().limit(1).build_aggregate();
let mut source = LogRecordSource::new(&fixture.registry, &fixture.log);
let folded = execute_aggregate(&fixture.registry, &mut source, &spec, &aggregate).unwrap();
assert_eq!(folded.count, 3);
}
fn named_users_schema() -> crate::schema::TableSchema {
parse_schema(&json!({
"schemaVersion": 1,
"table": "users",
"primaryKey": "id",
"fields": {
"id": { "type": "string" },
"name": { "type": "string", "index": "exact" },
"age": { "type": "int", "index": "sorted" },
"role": { "type": "string", "index": "exact" },
"tags": { "type": "string[]", "index": "exact" },
},
}))
.unwrap()
}
fn ania_kowalska() -> HaysonRecord {
record(&[
("id", json!("user_1")),
("name", json!("Ania Kowalska")),
("age", json!(31)),
("role", json!("admin")),
("tags", json!(["backend", "node"])),
])
}
#[test]
fn a_contains_predicate_on_a_string_field_with_an_exact_index_falls_back_to_a_scan() {
let fixture = fixture_with_schema(named_users_schema(), &[ania_kowalska(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("name", ComparisonOp::Contains, json!("Kowals"))
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Scan);
assert_eq!(result.keys, vec!["user_1".to_string()]);
}
#[test]
fn a_contains_predicate_on_an_array_field_keeps_using_the_exact_index() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("tags", ComparisonOp::Contains, json!("go"))
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Exact);
assert_eq!(result.plan.field, Some("tags".to_string()));
assert_eq!(result.plan.candidates, 1);
assert_eq!(result.keys, vec!["user_3".to_string()]);
}
#[test]
fn an_equality_against_an_array_value_falls_back_to_a_scan() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("tags", ComparisonOp::Eq, json!(["backend", "node"]))
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Scan);
assert_eq!(result.keys, vec!["user_1".to_string()]);
}
fn admins_only_index() -> Vec<crate::catalog::ExpandedIndex> {
vec![crate::catalog::ExpandedIndex {
name: "users_admin_name".to_string(),
fields: vec!["name".to_string()],
kind: crate::catalog::CatalogIndexKind::Exact,
unique: false,
where_: Some(vec![crate::catalog::CatalogPredicate {
field: "role".to_string(),
op: "=".to_string(),
value: json!("admin"),
}]),
expire_after_seconds: None,
}]
}
fn partial_fixture() -> Fixture {
let schema = parse_schema(&json!({
"schemaVersion": 1,
"table": "users",
"primaryKey": "id",
"fields": {
"id": { "type": "string" },
"name": { "type": "string" },
"age": { "type": "int" },
"role": { "type": "string" },
"tags": { "type": "string[]" },
},
}))
.unwrap();
let registry = TableRegistry::with_declared_indexes(schema, &admins_only_index());
fixture_of(registry, &[ania(), piotr(), maria()])
}
#[test]
fn a_where_scoped_index_never_answers_a_query_that_omits_its_predicate() {
let fixture = partial_fixture();
let spec = QueryBuilder::new()
.r#where("name", ComparisonOp::Eq, json!("Piotr"))
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(
result.plan.strategy,
QueryStrategy::Scan,
"Piotr is not an admin, so the index never saw him"
);
assert_eq!(result.plan.candidates, 3);
assert_eq!(result.keys, vec!["user_2".to_string()]);
}
#[test]
fn a_where_scoped_index_answers_a_query_that_repeats_its_predicate() {
let fixture = partial_fixture();
let spec = QueryBuilder::new()
.r#where("name", ComparisonOp::Eq, json!("Ania"))
.r#where("role", ComparisonOp::Eq, json!("admin"))
.spec();
let (result, _) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Exact);
assert_eq!(result.plan.index, Some("users_admin_name".to_string()));
assert_eq!(
result.plan.field, None,
"a scoped index is named by the index, not by its one field"
);
assert_eq!(result.plan.candidates, 1);
assert_eq!(result.keys, vec!["user_1".to_string()]);
}
#[test]
fn candidates_of_an_index_and_of_a_scan_alike_come_out_in_write_order() {
let fixture = fixture(&[maria(), ania(), piotr()]);
let indexed = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("user"))
.spec();
let scanned = QueryBuilder::new()
.r#where("name", ComparisonOp::StartsWith, json!(""))
.spec();
let (by_index, _) = run(&fixture, &indexed);
let (by_scan, _) = run(&fixture, &scanned);
assert_eq!(
by_index.keys,
vec!["user_3".to_string(), "user_2".to_string()],
"Maria was written first"
);
assert_eq!(
by_scan.keys,
vec!["user_3".to_string(), "user_1".to_string(), "user_2".to_string()],
"the scan follows the log, not the hash of the key"
);
}
#[test]
fn an_overwritten_record_keeps_its_place_on_an_unordered_page() {
let mut fixture = fixture(&[ania(), piotr(), maria()]);
let mut updated = ania();
updated.insert("age".to_string(), json!(32));
overwrite(&mut fixture, &updated, 4);
let scanned = QueryBuilder::new()
.r#where("name", ComparisonOp::StartsWith, json!(""))
.spec();
let (result, _) = run(&fixture, &scanned);
assert_eq!(
result.keys,
vec!["user_1".to_string(), "user_2".to_string(), "user_3".to_string()],
"the update rewrote Ania in place, so she stays first -- not last behind Maria"
);
}
#[test]
fn a_record_written_after_an_overwrite_still_lands_at_the_end_of_the_page() {
let mut fixture = fixture(&[ania(), piotr()]);
let mut updated = ania();
updated.insert("age".to_string(), json!(32));
overwrite(&mut fixture, &updated, 3);
overwrite(&mut fixture, &maria(), 4);
let scanned = QueryBuilder::new()
.r#where("name", ComparisonOp::StartsWith, json!(""))
.spec();
let (result, _) = run(&fixture, &scanned);
assert_eq!(
result.keys,
vec!["user_1".to_string(), "user_2".to_string(), "user_3".to_string()],
"Maria is genuinely new, so she goes last however recently Ania was rewritten"
);
}
#[test]
fn an_empty_fulltext_result_never_falls_back_to_a_scan() {
let fixture = fulltext_fixture();
let spec = QueryBuilder::new().search("zzz_nowhere", SearchMode::All).spec();
let (result, reads) = run(&fixture, &spec);
assert_eq!(result.plan.strategy, QueryStrategy::Fulltext);
assert_eq!(result.plan.candidates, 0);
assert_eq!(
reads, 0,
"the log must not be read when the search already proved an empty result"
);
assert!(result.keys.is_empty());
}
#[test]
fn a_search_left_to_its_own_order_reports_a_ranked_plan() {
let fixture = fulltext_fixture();
let spec = QueryBuilder::new().search("backend developer", SearchMode::All).spec();
let (result, _) = run(&fixture, &spec);
assert!(result.plan.ranked, "the order came out of the relevance scores");
}
#[test]
fn an_explicit_order_beside_a_search_reports_an_unranked_plan() {
let fixture = fulltext_fixture();
let spec = QueryBuilder::new()
.search("backend developer", SearchMode::All)
.order_by("age", SortDirection::Desc)
.spec();
let (result, _) = run(&fixture, &spec);
assert!(
!result.plan.ranked,
"the scores were computed but the explicit order overrode them"
);
}
#[test]
fn a_query_without_a_search_reports_an_unranked_plan() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("user"))
.spec();
let (result, _) = run(&fixture, &spec);
assert!(!result.plan.ranked, "no search ran, so there is nothing to rank by");
}
#[test]
fn a_query_whose_every_field_is_projected_answers_without_reading_a_record() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("user"))
.r#where("age", ComparisonOp::Gt, json!(30))
.spec();
let (result, reads) = run(&fixture, &spec);
assert!(result.plan.filtered_on_projection);
assert_eq!(result.plan.reads, 0);
assert_eq!(reads, 0);
assert_eq!(result.keys, vec!["user_3".to_string()]);
}
#[test]
fn a_query_touching_a_field_outside_the_projection_reads_every_candidate() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("name", ComparisonOp::Eq, json!("Maria"))
.spec();
let (result, reads) = run(&fixture, &spec);
assert!(
!result.plan.filtered_on_projection,
"`name` carries no index, so it is not projected"
);
assert_eq!(result.plan.reads, 3);
assert_eq!(reads, 3);
assert_eq!(result.keys, vec!["user_3".to_string()]);
}
#[test]
fn a_sort_on_a_field_outside_the_projection_takes_the_query_off_the_projections() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("user"))
.order_by("name", SortDirection::Asc)
.spec();
let (result, _) = run(&fixture, &spec);
assert!(
!result.plan.filtered_on_projection,
"the sort field counts as a field in play"
);
assert_eq!(result.plan.reads, 2);
assert_eq!(
result.keys,
vec!["user_3".to_string(), "user_2".to_string()],
"Maria before Piotr"
);
}
#[test]
fn a_count_only_query_answers_with_the_match_count_and_no_page() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let paged = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("user"))
.spec();
let counting = paged.clone().counting(true);
let (page, _) = run(&fixture, &paged);
let (count, _) = run(&fixture, &counting);
assert_eq!(
page.keys.len(),
2,
"the same query hands back a page when it is not counting"
);
assert_eq!(count.matched, page.matched);
assert!(
count.keys.is_empty(),
"a count-only query names no page for a caller to load"
);
assert_eq!(
count.plan, page.plan,
"counting changes what is returned, not how it was planned"
);
}
#[test]
fn a_count_only_query_counts_every_match_even_where_a_page_would_be_cut() {
let fixture = fixture(&[ania(), piotr(), maria()]);
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("user"))
.spec()
.with_limit(1);
let (count, _) = run(&fixture, &spec.clone().counting(true));
assert_eq!(
count.matched, 2,
"`matched` counts before `limit` cuts, counting or not"
);
assert!(count.keys.is_empty());
}
#[test]
fn an_aggregate_keeps_its_keys_when_the_spec_it_unpages_was_counting() {
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("user"))
.spec()
.counting(true);
assert!(!spec.unpaged().count_only);
}
}