use std::collections::HashMap;
use std::sync::Arc;
use nodedb_types::result::QueryResult;
use nodedb_types::value::Value;
use crate::engine::document::history::ops::{is_bitemporal, versioned_get_current};
use crate::error::LiteError;
use crate::index::document::{matches_probe, row_value};
use crate::index::key::key_of_doc_id;
use crate::index::{IndexDef, IndexEngine, canonical_field, field_spec};
use crate::query::document_rows;
use crate::query::engine::LiteQueryEngine;
use crate::query::kv_ops::sql_read::kv_select_keys;
use crate::storage::engine::StorageEngine;
fn lookup_index<S: StorageEngine>(
engine: &LiteQueryEngine<S>,
collection: &str,
field: &str,
) -> Result<Arc<IndexDef>, LiteError> {
let (path, is_array) = canonical_field(field);
let def = engine
.indexes
.def_on_field(collection, &field_spec(&path, is_array))
.ok_or_else(|| LiteError::BadRequest {
detail: format!("collection '{collection}' has no index on {path}"),
})?;
if def.engine == IndexEngine::KeyValue {
return Err(LiteError::Unsupported {
detail: format!(
"index '{}' on key-value collection '{collection}' answers scans, \
not document index lookups",
def.name
),
});
}
Ok(def)
}
async fn strict_candidate_rows<S: StorageEngine>(
engine: &LiteQueryEngine<S>,
collection: &str,
ids: Vec<String>,
keep: impl Fn(&Value) -> bool,
) -> Result<(Vec<String>, Vec<(String, Vec<Value>)>), LiteError> {
let schema = engine
.strict
.schema(collection)
.ok_or_else(|| LiteError::collection_not_found("strict", collection))?;
let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
let mut rows = Vec::with_capacity(ids.len());
for id in ids {
let Some(values) = engine.strict.row_by_doc_id(collection, &id).await? else {
continue;
};
let doc = Value::Object(
columns
.iter()
.cloned()
.zip(values.iter().cloned())
.collect::<HashMap<_, _>>(),
);
if keep(&doc) {
rows.push((id, values));
}
}
Ok((columns, rows))
}
async fn engine_candidate_rows<S: StorageEngine>(
engine: &LiteQueryEngine<S>,
def: &IndexDef,
ids: Vec<String>,
keep: impl Fn(&Value) -> bool,
) -> Result<(Vec<String>, Vec<(String, Vec<Value>)>), LiteError> {
match def.engine {
IndexEngine::Document => Ok((
document_rows::columns(),
candidate_rows(engine, &def.collection, ids, keep).await?,
)),
IndexEngine::Strict => strict_candidate_rows(engine, &def.collection, ids, keep).await,
IndexEngine::KeyValue => {
let keys: Vec<Vec<u8>> = ids.iter().filter_map(|id| key_of_doc_id(id)).collect();
let result = kv_select_keys(engine, &def.collection, &keys).await?;
let rows = result
.rows
.into_iter()
.map(|row| (String::new(), row))
.collect();
Ok((result.columns, rows))
}
}
}
async fn candidate_rows<S: StorageEngine>(
engine: &LiteQueryEngine<S>,
collection: &str,
ids: Vec<String>,
keep: impl Fn(&Value) -> bool,
) -> Result<Vec<(String, Vec<Value>)>, LiteError> {
let mut rows = Vec::with_capacity(ids.len());
if is_bitemporal(&*engine.storage, collection).await? {
for id in ids {
let Some(version) = versioned_get_current(&*engine.storage, collection, &id).await?
else {
continue;
};
if !version.is_live() {
continue;
}
let doc = if version.body.is_empty() {
Value::Object(Default::default())
} else {
nodedb_types::json_msgpack::value_from_msgpack(&version.body).map_err(|e| {
LiteError::Serialization {
detail: format!(
"live version of '{collection}'/'{id}' does not decode: {e}"
),
}
})?
};
if keep(&doc) {
let row = document_rows::value_row(&id, doc);
rows.push((id, row));
}
}
return Ok(rows);
}
let crdt = engine.crdt.lock().map_err(|_| LiteError::LockPoisoned)?;
for id in ids {
let Some(doc) = crdt.read(collection, &id) else {
continue;
};
if keep(&row_value(&doc)) {
let row = document_rows::crdt_row(&id, &doc);
rows.push((id, row));
}
}
Ok(rows)
}
pub async fn indexed_fetch<S: StorageEngine>(
engine: &LiteQueryEngine<S>,
collection: &str,
field: &str,
probe: &Value,
limit: usize,
offset: usize,
) -> Result<QueryResult, LiteError> {
let def = lookup_index(engine, collection, field)?;
let ids = engine.indexes.lookup_eq(&def, probe);
let (columns, rows) =
engine_candidate_rows(engine, &def, ids, |doc| matches_probe(&def, doc, probe)).await?;
let rows = rows
.into_iter()
.skip(offset)
.take(limit)
.map(|(_, row)| row)
.collect();
Ok(QueryResult {
columns,
rows,
rows_affected: 0,
command: None,
})
}
pub async fn index_lookup<S: StorageEngine>(
engine: &LiteQueryEngine<S>,
collection: &str,
field: &str,
probe: &Value,
) -> Result<QueryResult, LiteError> {
let def = lookup_index(engine, collection, field)?;
let ids = engine.indexes.lookup_eq(&def, probe);
let (_, rows) =
engine_candidate_rows(engine, &def, ids, |doc| matches_probe(&def, doc, probe)).await?;
let rows = rows
.into_iter()
.map(|(id, _)| vec![Value::String(id)])
.collect();
Ok(QueryResult {
columns: vec!["document_id".into()],
rows,
rows_affected: 0,
command: None,
})
}
pub async fn index_range_fetch<S: StorageEngine>(
engine: &LiteQueryEngine<S>,
def: &IndexDef,
lower: Option<&Value>,
upper: Option<&Value>,
) -> Result<Option<QueryResult>, LiteError> {
let Some(ids) = engine.indexes.lookup_range(def, lower, upper) else {
return Ok(None);
};
let (columns, rows) = engine_candidate_rows(engine, def, ids, |_| true).await?;
Ok(Some(QueryResult {
columns,
rows: rows.into_iter().map(|(_, row)| row).collect(),
rows_affected: 0,
command: None,
}))
}